Должен ли я когда-либо использовать `vec3` внутри однородного буфера или шейдерного хранилища?

Тип vec3 - очень красивый тип. Он занимает всего 3 поплавка, и у меня есть данные, для которых требуется только 3 поплавка. И я хочу использовать его в структуре в UBO и/или SSBO:

layout(std140) uniform UBO
{
  vec4 data1;
  vec3 data2;
  float data3;
};

layout(std430) buffer SSBO
{
  vec4 data1;
  vec3 data2;
  float data3;
};

Затем в моем коде C или С++ я могу сделать это, чтобы создать соответствующие структуры данных:

struct UBO
{
  vector4 data1;
  vector3 data2;
  float data3;
};

struct SSBO
{
  vector4 data1;
  vector3 data2;
  float data3;
};

Это хорошая идея?

Ответы

Ответ 1

НЕТ! Никогда не делай этого!

При объявлении UBOs/SSBOs притворитесь, что все трехмерные векторные и матричные типы не существуют. Представьте, что единственными типами являются скаляры, 2 и 4 вектора элементов (и матрицы). Если вы это сделаете, вы сэкономите себя очень много.

Если вам нужен эффект vec3 + float, вы должны его вручную упаковать:

layout(std140) uniform UBO
{
  vec4 data1;
  vec4 data2and3;
};

Да, вам нужно будет использовать data2and3.w, чтобы получить другое значение. Поговорите с ним.

Если вы хотите массивы vec3 s, тогда создайте их массивы vec4 s. То же самое касается матриц, которые используют 3-элементные векторы. Просто изгоните всю концепцию 3-элементных векторов от ваших SSBOs/UBOs; вам будет намного лучше в долгосрочной перспективе.

Есть две причины, по которым вам следует избегать vec3:

Он не будет делать то, что C/С++ делает

Если вы используете макет std140, то вы, вероятно, захотите определить структуры данных на C или С++, которые соответствуют определению в GLSL. Это упрощает смешивание и совпадение между ними. А макет std140 делает это, по крайней мере, возможным в большинстве случаев. Но его правила компоновки не соответствуют обычным правилам компоновки для компиляторов C и С++, когда речь заходит о vec3 s.

Рассмотрим следующие определения С++ для типа vec3:

struct vec3a { float a[3]; };
struct vec3f { float x, y, z; };

Оба они являются совершенно законными типами. sizeof и расположение этих типов будут соответствовать размеру и макету, который требуется std140. Но это не соответствует поведению выравнивания, которое налагает std140.

Рассмотрим это:

//GLSL
layout(std140) uniform Block
{
    vec3 a;
    vec3 b;
} block;

//C++
struct Block_a
{
    vec3a a;
    vec3a b;
};

struct Block_f
{
    vec3f a;
    vec3f b;
};

В большинстве компиляторов С++ sizeof для Block_a и Block_b будет 24. Это означает, что offsetof b будет 12.

Однако в макете std140 vec3 всегда совпадает с 4 словами. И поэтому Block.b будет иметь смещение 16.

Теперь вы можете исправить это, используя функциональность С++ 11 alignas (или C11 аналогичную функцию _Alignas):

struct alignas(16) vec3a_16 { float a[3]; };
struct alignas(16) vec3f_16 { float x, y, z; };

struct Block_a
{
    vec3a_16 a;
    vec3a_16 b;
};

struct Block_f
{
    vec3f_16 a;
    vec3f_16 b;
};

Если компилятор поддерживает выравнивание по 16 байт, это будет работать. Или, по крайней мере, он будет работать в случае Block_a и Block_f.

Но в этом случае это не сработает:

//GLSL
layout(std140) Block2
{
    vec3 a;
    float b;
} block2;

//C++
struct Block2_a
{
    vec3a_16 a;
    float b;
};

struct Block2_f
{
    vec3f_16 a;
    float b;
};

По правилам std140 каждый vec3 должен начинаться с 16-байтовой границы. Но vec3 не потребляет 16 байт памяти; он потребляет всего 12. И поскольку float может начинаться с 4-байтовой границы, a vec3, за которой следует float, будет занимать 16 байт.

Но правила выравнивания С++ не допускают такой вещи. Если тип привязан к границе X-байта, то использование этого типа будет потреблять кратное X байтам.

Таким образом, соответствие макета std140 требует, чтобы вы выбирали тип, основанный на том, где именно он используется. Если за ним следует float, вы должны использовать vec3a; если за ним следует какой-то тип, который выровнен более чем на 4 байта, вы должны использовать vec3a_16.

Или вы можете просто не использовать vec3 в ваших шейдерах и избегать всей этой сложной сложности.

Обратите внимание, что на alignas(8) на основе vec2 не будет этой проблемы. Также не будут построены структуры C/С++ и массивы с использованием соответствующего спецификатора выравнивания (хотя массивы меньших типов имеют свои собственные проблемы). Эта проблема возникает только при использовании голой vec3.

Поддержка реализации нечеткая

Даже если вы все сделаете правильно, реализации, как известно, неправильно реализуют правила компоновки vec3 oddball. Некоторые реализации эффективно налагают правила выравнивания С++ на GLSL. Поэтому, если вы используете vec3, он обрабатывает его, так как С++ будет обрабатывать 16-байтовый выровненный тип. В этих реализациях a vec3, за которым следует float, будет работать как a vec4, за которым следует float.

Да, это ошибка исполнителей. Но так как вы не можете исправить реализацию, вам нужно ее обойти. И самый разумный способ сделать это - просто избегать vec3 вообще.

Обратите внимание, что для Vulkan компилятор SDK GLSL получает это право, поэтому вам не нужно беспокоиться об этом.