std140 – самый платформонезависимый лейаут uniform-блоков. Если вы передаете параметры в шейдер структурами, а не по одному, то лучше использовать именно его. Конечно, у переносимости есть своя цена: стандарт предусматривает несколько ограничений, самое известное из которых – выравнивание по 16 байтам. То есть, все поля структуры должны быть по размеру кратны 16 байтам. Например, если это вектор – то vec4, если матрица – то mat4. Некратные 16 байтам типы (float, int и др.) также можно использовать, но их необходимо выравнивать вручную – то есть, одиночный float пойдет все равно как vec4. Использование невыровненных данных приводит к тому, что в шейдере считываются неправильные значения, и это вызывает недоумение у начинающих.
Благодаря CTFE и статической интроспекции в D можно проверять структуры на соответствие этому правилу на этапе компиляции – все необходимое есть в std.traits
:
import std.traits;
bool fieldsAligned(T, alias numBytes)()
{
static if (is(T == struct))
{
alias FieldTypes = Fields!T;
static foreach(FT; FieldTypes)
{
static if (FT.sizeof % numBytes != 0)
return false;
}
return true;
}
else return false;
}
alias Std140Compliant(T) = fieldsAligned!(T, 16);
Теперь можно делать так:
import dlib.math.vector;
import dlib.math.matrix;
struct Uniforms
{
Matrix4x4f modelViewMatrix;
Matrix4x4f normalMatrix;
Vector4f worldPosition;
float someParameter; // Will not compile, use Vector4f instead
}
static assert(Std140Compliant!Uniforms,
Uniforms.stringof ~ " does not conform to std140 layout");
Возникает вопрос, почему бы просто не делать align(16)? Можно!
struct AlignedUniforms
{
align(16):
float a;
Vector3f b;
Matrix4x4f c;
}
Преимуществом такого подхода является право использовать любые типы для полей, но это неэффктивно: например, вместо того, чтобы впустую расходовать три байта после a, можно было бы упаковать a вместе с b в один 16-байтный вектор. Поэтому я лично не фанат автоматического компиляторного выравнивания – лучше это делать вручную, защитившись от ошибок необходимыми статическими проверками.