Compile Time Swizzling
Published 2020-10-25. Code is not tested and omits many details, intentionally or not.
C++ Sugar Templates
Motivation
In GLSL code, swizzling offers a handy syntax to manipulate vectors. Inside a cubemap vertex shader for instance, you may use it like this:
vec3 reverse_z = vec3(model_position.xy, -model_position.z);
vec4 projected = camera_proj * vec4(mat3(camera_view) * reverse_z, 1.0);
gl_Position = projected.xyww;
Solutions were found to reproduce this in C++, that each have their shortcomings.
- Explicitly declaring data members
vec.xyz
or functionsvec.xyz()
in the vector class. While close to the GLSL syntax, language is the limit here: accounting for all possible combinations ofxyzw
,rgba
,stpq
, anduv
requires 5106 members total. - Boost QVM proposes the
XYZ(vec)
syntax, with much the same issue. - GLM has three approaches to the problem.
As above, macros can be leveraged to generate thousands of members; additionally, the
swizzle
function does the job two ways:
glm::swizzle(vec, glm::X, glm::Y, glm::Z); // at run time
glm::swizzle<glm::X, glm::Y, glm::Z>(vec); // at compile time
Unsatisfied with the previous, I put my two cents on the question and settled on a tradeoff using vec["xyz"]
.
The underlying code has some limitations, but nice properties too:
- Only standard C++ features are used, and there is no macro magic at work.
- Swizzles can span arbitrary dimensions, i.e. output vectors of any length.
- Extra subscript characters can be added in a single line of code.
- The operator works at compile time.
- Syntax is reasonably short and clear:
Vec<int, 4> constexpr sequence{1, 4, 9, 16};
Vec<int, 4> constexpr reversed = sequence["wzyx"];
std::cout << sequence << reversed;
// Outputs:
// 1 4 9 16
// 16 9 4 1
A Simple Vector Class
Let's start off by building a simple Vec
data structure.
The template parameters will be its Type
and Size
, mimicking std::array
; in fact, I will use an std::array
to store the vector coefficients:
template<class Type, std::size_t Size>
struct Vec
{
std::array<Type, Size> coefficients;
/* ... */
};
We then create an output stream operator for the vector structure that simply folds over the pack of coefficients provided by std::apply
:
friend std::ostream& operator<<(std::ostream& os, Vec const& vec)
{
std::apply(
[&os](auto&&... c)
{
((os << c << ' '), ...) << '\n';
},
vec.coefficients
);
return os;
}
Another version that does not print final separation characters can be found on cppreference.
Swizzling Operator
To swizzle using a string, we must first transform characters into their corresponding axis; a simple switch
statement will do.
For brevity, I shrunk the number of lines and omitted [[fallthrough]]
but admittedly, this utility function could be more legible.
constexpr std::size_t axisIndex(char axis)
{
switch (axis)
{
case 'x': case 'r': case 's': case 'u': return 0;
case 'y': case 'g': case 't': case 'v': return 1;
case 'z': case 'b': case 'p': return 2;
case 'w': case 'a': case 'q': return 3;
}
return 0;
}
The swizzling operator takes strings as char
array references so that their length can be deduced as template parameter N
.
It returns a vector holding the same Type
with length N - 1
, because of the null termination character.
template<std::size_t N>
constexpr auto operator[](char const (&axes)[N]) const
{
return [this, &axes]<std::size_t... I>(std::index_sequence<I...>)
{
return Vec<Type, N - 1>{coefficients.at(axisIndex(axes[I]))...};
}(
std::make_index_sequence<N - 1>{}
);
}
Done!
The templated lambda deduces a pack of std::size_t
from its parameter, and is invoked right away on an std::index_sequence
instance.
Inside the lambda, we iterate over the characters and fetch their corresponding axis index.
One caveat: besides using at()
, there is no bounds checking on the axes...
What About Assignment?
This swizzling operator quickly shows its limits as it only copies the subscripted coefficients into a new Vec
.
Enabling assignment is actually straightforward: the swizzling operator should form an std::tuple
of references to coefficients when called on non-const instances.
Then in a wrapper class, you can add proper assignment operators and reach a working prototype in no time.
However, a number of subtleties - mostly related to move semantics - have to be taken into account, leading to rather lenghty code.
Also, rebind vs. assign-through is a consideration.
Suggestions welcome! Header file here: swizzling.h.