В шейдере мы можем написать vec3 v0 = v1.xxy * 2
, а также любую другую комбинацию x, y, z и w в зависимости от длины вектора. Я рассматриваю только размеры вектора до 4, как самые распространенные для использования. Полученный вектор может иметь не только ту же самую размерность, но и меньшую или большую, причем его компоненты могут быть скопированы в произвольном порядке. Эта операция называется 'swizzle', и она чертовски удобна для различных операций с малоразмерными векторами, особенно если они представляют игровые сущности в виде позиций, размера или цветов. Вектора используются повсюду в игровых проектах (да и не только в игровых), а не только в шейдерах. В какой-то момент было решено добавить 'swizzle' в наш игровой движок в базовые классы vec2
, vec3
и vec4
. Возникли вопросы: как добиться такого же синтаксического и семантического поведения в C++ коде, при этом минимизируя потери производительности.
Что в свизлинге тебе моем...
Лучше всего понять как работает swizzle - это показать на примерах:
vec3 vec{1.0, 2.0, 3.0};
vec4 a;
vec3 b;
vec2 c;
float d;
b = vec.xyz; // b is now (1.0, 2.0, 3.0)
d = vec[2]; // d is now 3.0
a = vec.xxxx; // a is now (1.0, 1.0, 1.0, 1.0)
c = vec.zx; // c is now (3.0, 1.0)
b = vec.rgb; // b is now (1.0, 2.0, 3.0)
a = vec.yy; // error; incostistent size
Есть несколько вариантов решения: использовать код из библиотеки glm, СxxSwizzle или написать свою реализацию. Библиотеку glm советую всем, кто начинает писать свой или хочет разобраться с игровыми движками. Однако, код в ней очень специфичный и многословный на мой взгляд, он является наследием трудных 90-х (первый код был написан в июне 1992 года, это больше 31 года назад).
Еще можно посмотреть реализацию здесь (https://github.com/gwiazdorrr/CxxSwizzle). Там реализовано много дополнительных функций, но в игровом движке обычно уже присутствует большая часть логики, хоть и в другом виде. Это решение хорошее, но, опять же, код там очень многословный. Мы же хотели получить простое и красивое решение, которое можно записать в пяти строчках кода.
Задача свизлинга сама по себе не очень сложная и обычно включается в список вопросов на собеседовании не только в игровых компаниях, правда в несколько другом виде. Перед переходом к коду хочу обратить ваше внимание на несколько моментов, которые мы учитывали при реализации нашего решения
Скорость выполнения, решение не должно вносить вычислительной нагрузки по сравнению с эквивалентными скалярными операциями. Решение не должно жертвовать вычислительной эффективностью ради реализации свизлинга, по возможности сделать на интринсиках SSE
Использование несмежных компонентов вектора, например, v0.xxy + v1.xzy. Это требует возможности доступа и выполнения операций над компонентами вектора в пользовательском порядке.
Запись в измененный вектор с ограничениями: (от этого потом отказались, чтобы не раздувать логику, в 99% этим не пользуются) например
v1.yxwy = v0;
с условием, что повторные элементы запрещены явно, то есть каждому элементу должно быть присвоено значение только один раз, и ни один элемент не должен получить несколько присваиваний.Отсутствие дополнительных накладных расходов по памяти: базовый тип не должен измениться в размерах. Это означает, что vec3 будет занимать пространство, эквивалентное хранению трех элементов своего типа данных без дополнительных накладных расходов.
Есть несколько различных способов добиться синтаксиса v1.yxwz = v0;
без скобок: макросы, использование union и прокси объекты. В случае с макросами можно скрыть функции, например:
#define yxwz _yxwz()
vec4 vecb = veca.yxwz
Проблема вообще любых макросов заключается в том, что они усложняют отладку, засоряют код и имеют специфику использования. Можно попробовать решить эту проблему с помощью объектов-прокси, содержащих ссылку на исходный вектор, но решение уже выглядит сложным. Плюс, операции с ними могут выполняться через указатель, что снизит общую производительность при работе с такими данными. Ну и, кроме того, я вообще не люблю использовать макросы, если есть возможность обойтись кодом.
Так что, после обсуждения в курилке, остановились на реализации через union - псевдокод такого union'a будет выглядеть так:
union {
float v[2];
??? xx;
??? xy;
??? yx;
??? yy;
...
};
Чтобы не писать много оберток и уложить новые типы данных в union, они должны быть plain only data, так проще будет применить фокус со свизлингом. Осталось сделать такую реализацию, чтобы не пришлось писать эти классы руками - пусть трудится компилятор. Выглядеть это будет как-то так (псевдокод):
template<T, X, Y>
struct proxy {
template<T, X2, Y2>
proxy operator = (const SwizzleProxy2<T, X2, Y2> &o) {
((T*)this)[X] = ((T*)&o)[X2];
((T*)this)[Y] = ((T*)&o)[Y2];
return this;
}
};
Тут мы ссылаемся на this
т.е. начало класса, подразумевая, что как самостоятельный тип данных swizzling существовать не будет, а будет только отображаться на память базового класса. Попытка сделать реализацию в лоб, как выяснилось, вызывает приступы ворнингов почти у всех компиляторов и false positive срабатывания статических анализаторов. Но псевдокод выше демонстрирует основную идею реализации операторов для свизлинга с использованием двух элементов: xx, xy, wx и так далее. Аргументы шаблона X и Y могут быть любыми индексами элементов вектора.
Если вы заметили, то собственных данных у класса нет, а такое вольное обращение с указателем на начало класса вызывало не только кучу ворнингов при компиляции первых вариантов под ps5 сlang, но и увеличивало время компиляции почти на 10%. С условных 5 до шести минут. Но если задать, хотя бы минимальный объект в классе, то время компиляции возвращалось в норму.
template<T, int X, int Y>
struct proxy {
byte v;
...
};
В итоге в класс был добавлен локальный массив, совпадающий по размерам с базовым. Так как все основные операторы уже были реализованы в классах vec2/3/4, то на долю swizzle класса остается только оператор приведения типа к вектору и базовая логика. Итак первая реализация, как она ушла в движок.
template<template<typename> class TT, typename T, size_t X, size_t Y>
struct swizzle_vec2{
T v[2];
inline TT<T>& operator= (const TT<T>& rhs) {
v[X] = rhs[0];
v[Y] = rhs[1];
return *(TT<T>*)this;
}
inline auto &operator= (const swizzle_vec2& rhs) {
v[X] = rhs.v[X];
v[Y] = rhs.v[Y];
return *this;
}
inline operator TT<T> () const { return TT<T>{v[X], v[Y]}; }
};
И копипаста для каждого из классов vec3/vec4. В движке придерживаются принципа DRY, но для обкатки фичи и быстрого запуска, решили улучшайзинг отложить на попозже, собрав отзывы о работе с новым функционалом.
template<template<typename> class TT, typename T, size_t X, size_t Y, size_t Z>
struct swizzle_vec3{
T v[3];
inline TT<T>& operator= (const TT<T>& rhs) {
v[X] = rhs[0];
v[Y] = rhs[1];
v[Z] = rhs[2];
return *(TT<T>*)this;
}
...
inline operator TT<T> () const { return TT<T>{v[X], v[Y], v[Z]}; } // unpack
};
Чтобы свизлинг заработал прозрачно в коде, в каждый класс надо было еще добавить union-структуры, которые бы это обеспечивали:
// v2 swizzles
#define swizzle_v2 \
swizzle_vec2<T, 2, 0, 0> xx; \
swizzle_vec2<T, 2, 0, 1> xy; \
swizzle_vec2<T, 2, 1, 0> yx; \
swizzle_vec2<T, 2, 1, 1> yy; \
...
template <class T>
struct vec2 {
union {
struct { T x, y; };
swizzle_v2
};
};
Финальный вариант
template<template<typename> class TT, typename T, size_t X, size_t Y>
struct swizzle_vec2 {
T v[2];
inline TT<T>& operator= (const TT<T>& rhs) {
v[X] = rhs[0]; // access pack element 0
v[Y] = rhs[1]; // access pack element 1
return *(TT<T>*)this;
}
inline auto &operator= (const swizzle_vec2& rhs) {
v[X] = rhs.v[X];
v[Y] = rhs.v[Y];
return *this;
}
inline operator TT<T> () const { return TT<T>{v[X], v[Y]}; } // unpack
};
#define swizzle_v2(TT) \
swizzle_vec2<TT, T, 0, 0> xx; \
swizzle_vec2<TT, T, 0, 1> xy; \
swizzle_vec2<TT, T, 1, 0> yx; \
swizzle_vec2<TT, T, 1, 1> yy;
template <class T>
struct vec2 {
union {
struct { T x, y; };
swizzle_v2(vec2)
};
};
int main() {
vec2<float> veca{0, 1};
printf("veca{%f, %f}\n", veca.x, veca.y);
vec2<float> vecb = veca.yx;
printf("vecb{%f, %f}", vecb.x, vecb.y);
return 0;
}
Попробовать можно тут (https://godbolt.org/z/rhcocMods)
Реализация для vec3/vec4 не отличалась сильно ничем, кроме большого макроса финальных swizzle-стуктур, но такова была цена - захотели упростить жизнь коллегам, значит больше логики в коде.
#define swizzle_v4 \
Swizzle_vec2<TT, T, 0, 0> xx; \
Swizzle_vec2<TT, T, 0, 1> xy; \
Swizzle_vec2<TT, T, 0, 2> xz; \
Swizzle_vec2<TT, T, 0, 3> xw; \
....
Swizzle_vec3<TT, T, 0, 0, 0> xxx; \
Swizzle_vec3<TT, T, 0, 0, 1> xxy; \
Swizzle_vec3<TT, T, 0, 0, 2> xxz; \
Swizzle_vec3<TT, T, 0, 0, 3> xxw; \
Swizzle_vec3<TT, T, 0, 1, 0> xyx; \
.....
Swizzle<TT, T, 0, 0, 0, 0> xxxx; \
Swizzle<TT, T, 0, 0, 0, 1> xxxy; \
Swizzle<TT, T, 0, 0, 0, 2> xxxz; \
Swizzle<TT, T, 0, 0, 0, 3> xxxw; \
Swizzle<TT, T, 0, 0, 1, 0> xxyx; \
Копипаста очень плохое решение в любом случае, но позволило выпустить этот функционал быстро и собрать фидбек. Решение работало, но знание что можно сделать лучше и меньшим количеством кода, и убрать копипасту не давало спокойно спать. Позже, на рефакторе было сделано общее решение и убраны простыни макросов.
Утро добрым не бывает
Однажды утром стали падать тесты. Падать стало вот на таком моменте:
...
vec3<float> vecc{1, 2, 3};
printf("vecc{%f, %f, %f}\n", vecc.x, vecc.y, vecc.z);
vec3<float> vecd{0, 0, 0};
vecd.xz = vecc.xz;
printf("vecd{%f, %f, %f}\n", vecd.x, vecd.y, vecd.z); <<<<<<<
...
output:
vecc{1.000000, 2.000000, 3.000000}
vecd{1.000000, 2.000000, 0.000000} -> vecd должен быть {1.0, 0.0, 2.0}
Блейм по исходникам привел вот к такому изменению в коде:
template<template<typename> class TT, typename T, size_t X, size_t Y>
struct swizzle_vec2{
T v[2];
inline TT<T>& operator= (const TT<T>& rhs) {
v[X] = rhs[0];
v[Y] = rhs[1];
return *(TT<T>*)this;
}
!!!! inline auto &operator= (const swizzle_vec2& rhs) = default; !!!!
inline operator TT<T> () const { return TT<T>{v[X], v[Y]}; }
};
```
на первый взгляд все хорошо - это код верен, если бы swizzle_vec2 был полноценным классом, со своими данными, а не маппингом на память чужих. Получается - дефолтный оператор копирования выполняет то для чего предназначен - копирует память из одного места в другое.
inline auto &operator= (const swizzle_vec2& rhs) = default;
->
call memset@PLTS // vecd.xz = vecc.xz
mov rax, qword ptr [rbp - 36] // vecd.xz = vecc.xz
mov qword ptr [rbp - 48], rax // vecd.xz = vecc.xz
Ошибку быстро поправили, заодно выделили время на рефактор кода. В итоге все классы получилось свести к одному шаблонному
template <typename T, int RSIZE, int... INDEXES>
struct swizzle {
T v[RSIZE];
static constexpr int indexes[] = {INDEXES...};
template <int... INDEXES2>
inline auto &operator=(const swizzle<T, RSIZE, INDEXES2...> &rhs) {
static_assert(sizeof...(INDEXES) == RSIZE, "error: assigning swizzle of different dimensions");
constexpr int rindexes[] = {INDEXES2...};
for(int i = 0; i < sizeof...(INDEXES); ++i) { v[indexes + i]] = rhs.v[rindexes[i]]; }
return *this;
}
inline auto &operator=(const swizzle &rhs) {
for(int i = 0; i < sizeof...(INDEXES); ++i) { v[indexes[i]] = rhs.v[indexes[i]]; }
return *this;
}
};
а для конкретных реализация под вектора осталось только операция приведения к конкретному типу:
template<class T> struct vec2;
template<class T> struct vec3;
template <typename T, int RSIZE, int... SWIZZLES>
struct swizzle2 : public swizzle<T, RSIZE, SWIZZLES...> {
inline auto &operator= (const vec2<T>& l) {
static_assert (sizeof...(SWIZZLES) == 2, "error: assigning swizzle2 not from vec2f");
v[indexes[0]] = l.x;
v[indexes[1]] = l.y;
return *this;
}
inline operator vec2<T> () const {
static_assert(sizeof...(SWIZZLES) > 1, "error: no data that convert to vec2");
return vec2<T>{v[indexes[0]], v[indexes[1]]};
}
};
template <typename T, int RSIZE, int... SWIZZLES>
struct swizzle3 : public swizzle<T, RSIZE, SWIZZLES...> {
inline auto &operator= (const vec3<T>& l) {
static_assert (sizeof...(SWIZZLES) == 3, "error: assigning swizzle2 not from vec2f");
v[indexes[0]] = l.x;
v[indexes[1]] = l.y;
v[indexes[2]] = l.z;
return *this;
}
inline operator vec3<T> () const {
static_assert(sizeof...(SWIZZLES) > 2, "error: no data that convert to vec3");
return vec3<T>{v[indexes[0]], v[indexes[1]], v[indexes[2]]};
}
};
для полного счастья остается свернуть простыню прокси-структур, которая все равно присутствует в каждом классе vec2/3/4
#define swizzle_v2 \
swizzle2<T, 2, 0, 0> xx; \
swizzle2<T, 2, 0, 1> xy; \
swizzle2<T, 2, 1, 0> yx; \
swizzle2<T, 2, 1, 1> yy; \
swizzle3<T, 3, 0, 0, 0> xxx; \
swizzle3<T, 3, 0, 0, 1> xxy; \
swizzle3<T, 3, 0, 1, 0> xyx; \
swizzle3<T, 3, 0, 1, 1> xyy; \
swizzle3<T, 3, 1, 0, 0> yxx; \
swizzle3<T, 3, 1, 0, 1> yxy; \
swizzle3<T, 3, 1, 1, 0> yyx; \
swizzle3<T, 3, 1, 1, 1> yyy; \
swizzle4<T, 4, 0, 0, 0, 0> xxxx; \
swizzle4<T, 4, 0, 0, 0, 1> xxxy; \
swizzle4<T, 4, 0, 0, 1, 0> xxyx; \
swizzle4<T, 4, 0, 0, 1, 1> xxyy; \
swizzle4<T, 4, 0, 1, 0, 0> xyxx; \
swizzle4<T, 4, 0, 1, 0, 1> xyxy; \
swizzle4<T, 4, 0, 1, 1, 0> xyyx; \
swizzle4<T, 4, 0, 1, 1, 1> xyyy; \
swizzle4<T, 4, 1, 0, 0, 0> yxxx; \
swizzle4<T, 4, 1, 0, 0, 1> yxxy; \
swizzle4<T, 4, 1, 0, 1, 0> yxyx; \
swizzle4<T, 4, 1, 0, 1, 1> yxyy; \
swizzle4<T, 4, 1, 1, 0, 0> yyxx; \
swizzle4<T, 4, 1, 1, 0, 1> yyxy; \
swizzle4<T, 4, 1, 1, 1, 0> yyyx; \
swizzle4<T, 4, 1, 1, 1, 1> yyyy;
...
К сожалению убрать макросы без новых макросов не удалось, поэтому приходится пользоваться темной стороны силы.
Если посмотреть на приведенный код, то можно заметить что получаемые значения имеют ограниченный набор входных данных x, y, z, w, более того - каждое положение внутри набора соответствуют конкретному состоянию класса свизлинга
xx -> swizzle2<T, 2, 0, 0> -> {0, 0}
xy -> swizzle2<T, 2, 0, 1> -> {0, 1}
yx -> swizzle2<T, 2, 1, 0> -> {1, 0}
...
эта зависимость может быть выражена через constexpr функцию, например так
constexpr int inline swizzle_idx__(pcstr x, int offset) {
switch(*(x+offset)) { case 'x': return 0; case 'y': return 1;
case 'z': return 2; case 'w': return 3; }
return -1;
}
swizzle2<T, 2, 0, 0> xx
-> swizzle2<T, 2, swizzle_idx__("xx", 0), swizzle_idx__("xx", 0)>
пробуем свернуть это выражение в макрос
swizzle2<T, 2, 0, 0> xx;
swizzle2<T, 2, 0, 1> xy;
swizzle2<T, 2, 1, 0> yx;
swizzle2<T, 2, 1, 1> yy;
->
#define $(name) swizzle2<T, 2, swizzle_idx__(#name, 0), swizzle_idx__(#name, 1)> name;
$(xx) $(xy) $(yx) $(yy)
с vec3 и vec4 не должно возникнуть сложностей, принцип похожий - определяем макрос для свизлинга в конкретную последовательность:
#define $(name) swizzle3<T, 3,
swizzle_idx__(#name, 0),
swizzle_idx__(#name, 1),
swizzle_idx__(#name, 2)> name;
А вот самих комбинаций тут будет уже больше, поэтому придется поправить макрос для перебора всех возможных вариантов. Финальный код получается вот таким и, чтобы не загромождать хедеры лишними дефайнами, может быть размещен прямо в теле union
#define _(x) $(x ## xx) $(x ## xy) $(x ## xz) $(x ## yx) $(x ## yy) $(x ## yz) $(x ## zx) $(x ## zy) $(x ## zz)
_(x) _(y) _(z) _(w)
финальный код классов векторов после этого будет выглядеть вот так:
template <class T>
struct vec2 {
union {
struct { T x, y; };
#define $(name) swizzle2<T, 2, swizzle_idx__(#name, 0), swizzle_idx__(#name, 1)> name;
$(xx) $(xy) $(yx) $(yy)
#undef $
};
};
код примера положу под спойлер, он не очень интересен, просто рабочая реализация (https://godbolt.org/z/ff55c4YnP)
Пример
template <typename T, int RSIZE, int... INDEXES>
struct swizzle {
T v[RSIZE]; // тут нужен отдельный размер, потому что свизлинг
// может быть замаплен на класс большего размера
// и компилятор будет выдавать ворниг в этом месте
static constexpr int indexes[] = {INDEXES...};
template <int... INDEXES2>
inline auto &operator=(const swizzle<T, RSIZE, INDEXES2...>& rhs) {
static_assert(sizeof...(INDEXES) == RSIZE, "error: assigning swizzle of different dimensions");
constexpr int rindexes[] = {INDEXES2...};
for(int i = 0; i < sizeof...(INDEXES); ++i) { v[indexes[i]] = rhs.v[rindexes[i]]; }
return *this;
}
inline auto &operator=(const swizzle& rhs) {
for(int i = 0; i < sizeof...(INDEXES); ++i) { v[indexes[i]] = rhs.v[indexes[i]]; }
return *this;
}
};
template<class T> struct vec2;
template<class T> struct vec3;
template <typename T, int RSIZE, int... SWIZZLES>
struct swizzle2 : public swizzle<T, RSIZE, SWIZZLES...> {
inline auto &operator= (const vec2<T>& l) {
static_assert (sizeof...(SWIZZLES) == 2, "error: assigning swizzle2 not from vec2f");
this->v[this->indexes[0]] = l.x;
this->v[this->indexes[1]] = l.y;
return *this;
}
inline operator vec2<T> () const {
static_assert(sizeof...(SWIZZLES) > 1, "error: no data that convert to vec2");
return vec2<T>{this->v[this->indexes[0]], this->v[this->indexes[1]]};
}
};
template <typename T, int... SWIZZLES>
struct swizzle3 : public swizzle<T, SWIZZLES...> {
inline operator vec3<T> () const {
static_assert(sizeof...(SWIZZLES) > 2, "error: no data that convert to vec3");
return vec3<T>{this->v[this->indexes[0]], this->v[this->indexes[1]], this->v[this->indexes[2]]};
}
};
constexpr int inline swizzle_idx__(const char *x, int offset) { switch(*(x+offset)) { case 'x': return 0; case 'y': return 1; case 'z': return 2; case 'w': return 3; } return -1; }
template <typename T, int RSIZE, int... SWIZZLES>
struct swizzle3 : public swizzle<T, RSIZE, SWIZZLES...> {
inline auto &operator= (const vec3<T>& l) {
static_assert (sizeof...(SWIZZLES) == 3, "error: assigning swizzle2 not from vec2f");
this->v[this->indexes[0]] = l.x;
this->v[this->indexes[1]] = l.y;
this->v[this->indexes[2]] = l.z;
return *this;
}
inline operator vec3<T> () const {
static_assert(sizeof...(SWIZZLES) > 2, "error: no data that convert to vec3");
return vec3<T>{this->v[this->indexes[0]], this->v[this->indexes[1]], this->v[this->indexes[2]]};
}
};
template <class T>
struct vec3 {
union {
struct { T x, y, z; };
#define $(name) swizzle2<T, 3, swizzle_idx__(#name, 0), swizzle_idx__(#name, 1)> name;
$(xx) $(xy) $(xz) $(yx) $(yy) $(yz) $(zx) $(zy) $(zz)
#undef $
#define $(name) swizzle3<T, 3, swizzle_idx__(#name, 0), swizzle_idx__(#name, 1), swizzle_idx__(#name, 2)> name;
#define _(x) $(x ## xx) $(x ## xy) $(x ## xz) $(x ## yx) $(x ## yy) $(x ## yz) $(x ## zx) $(x ## zy) $(x ## zz)
_(x) _(y) _(z) _(w)
#undef _
#undef $
};
};
int main() {
vec2<float> veca{0, 1};
printf("veca{%f, %f}\n", veca.x, veca.y);
vec2<float> vecb = veca.yx;
printf("vecb{%f, %f}\n", vecb.x, vecb.y);
vec3<float> vecc{1, 2, 3};
printf("vecc{%f, %f, %f}\n", vecc.x, vecc.y, vecc.z);
vec3<float> vecd{0, 0, 0};
vecd.xz = vecc.xz;
printf("vecd{%f, %f, %f}\n", vecd.x, vecd.y, vecd.z);
return 0;
}
Бенчмарки
Если мы посмотрим, что делает наш свизлинг в асме, то делает он ровно то, что мы написали - перекидывает байты из одного места в другое (https://godbolt.org/z/9EPe3q4EE)
для реализованного класса, разница между 2/3 свизлингом вполне ожидаема и зависит только от количества операций внутри.
https://quick-bench.com/q/XYSn8lcsfWbp4B5qAuL
К сожалению, веб версия не позволяет затащить код из glm и cxxswizzle, пришлось разворачивать локально. volatile здесь сделан, чтобы компилятор не смог намухлевать с присвоением констант.
float volatile xx;
float volatile x = xx + 1;
float volatile y = xx + 2;
float volatile z = xx + 3;
static void Vec3Swizzling(benchmark::State& state) {
for (auto _ : state) {
vec3<float> veca{x, y, z};
vec3<float> vecb{1, 2, 3};
vecb.xz = veca.xz;
benchmark::DoNotOptimize(veca);
benchmark::DoNotOptimize(vecb);
}
}
BENCHMARK(Vec3Swizzling);
static void Vec3SwizzlingGlm(benchmark::State& state) {
for (auto _ : state) {
glm::vec3<float> vecc{x, y, z};
glm::vec3<float> vecd{0, 0, 0};
vecd.xz = vecc.xz;
benchmark::DoNotOptimize(vecc);
benchmark::DoNotOptimize(vecd);
}
}
BENCHMARK(Vec3SwizzlingGlm);
static void Vec3SwizzlingCxxSwizzle(benchmark::State& state) {
for (auto _ : state) {
cxx::vec3<float> vecc{x, y, z};
cxx::vec3<float> vecd{0, 0, 0};
vecd.xz = vecc.xz;
benchmark::DoNotOptimize(vecc);
benchmark::DoNotOptimize(vecd);
}
}
BENCHMARK(Vec3SwizzlingCxxSwizzle);
По бенчам текущая реализация быстрее реализации в glm в 1.2 раза и CxxSwizzle в два раза. Ускорение достигается за счет отсутствие лишних проверок, которые есть в этих либах, и лучшей адаптации к структурам данных движка.
Можно еще ускорить свизлинг, если воспользоваться интринсиками SSE для перемешивания, тогда в асме это будет вообще одна операция. Есть вопрос как это все ляжет в памяти, но это будет тема следующего рефатокторинга.