Sn.Math design decisions
2018-04-15 Permalink
The majority of Sn.Math design revolves around the guidelines I wrote before. Here I describe some of the choices I made which are more of a personal taste rather than a universal principle.
Vector times scalar
The library enforces multiplying scalars on the left by excluding operator *(vec, T)
. Due to the left-associativity of operator *
, calculating v*a*b
is slower than a*b*v
(here v
is vec
and a
and b
are scalars). Removing such an operator ensures that constants are multiplied on the left. operator /(vec, T)
is included, however, because there’s no other way to divide integer vectors by integers, and because converting v/a
to (1./a)*v
is deemed too verbose to be practical. Chaining divisions like this is also observed to be less common.
Use of templates and specializations
I once was convinced that defining separate classes for each dimensionality is the correct way to go, and that they should be templates parametrized only by the data type. This was driven mostly by the need of specifying different number of parameters in the constructors. The ability to name the components by conventional alphabetic names (like xyz) was a lesser but existing concern.
However, as initializer lists were added to the language, I decided to merge the vec2
, vec3
and vec4
into a single vec
template (and so with the other accompanying types). Sure, providing specializations with members named appropriately is an option, but I decided to go with the more tidy approach.[1] Even though at first I had to get used to the absence of the named components, once I did, I found that indices work much better.
The reason is that the alphabetic names are artificial names chosen for what is otherwise homogeneous. The vector types are frequently used for values other than points in an N-dimensional world, and every such use brings its own naming conventions to the picture. For that purpose, GLSL introduces synonymous names for each component, including ‘xyzw’, ‘rgba’ and ‘stpq’. Although convenient in some cases like component swizzling[2], this approach does not generalize well. At times one works with a BGRA component order, at times with a completely different color space like YUV.
Overall it seems that named components have only marginal value. Using indices solves this madness and discourages manual per-component arithmetics.
Automatic type conversions
An advantage of using templates instead of distinct types is that we can write templated versions for many of our functions. There is a tradeoff though: when using templates as opposed to distinct types the compiler cannot figure out the type conversions during overload resolution. This is a consequence of how overload resolution in C++ works: the argument conversions are considered only after template argument deduction had already taken place. However, automatic type conversions are generally considered a bad thing, especially the narrowing ones. Sure, I have to pay with extra verbosity and insert correct casts where appropriate, but I’m also able to see where I should rather adjust the types of the variables.
Type naming schemes
I considered three major naming conventions for math types: GLSL, OpenGL and CUDA.
GLSL prefixless types map to single precision floats. This is convenient for real-time graphics uses where the precision is more than enough for those purposes. However:
- CPU code just as frequently uses double precision in calculations.
- The suffixless floating point literals in C are of double precision.
- Polyglots are not a consideration: GLSL code can only rarely be compiled as C++ without refactoring, even with C++ libraries that try to mimick GLSL as close as possible. Nor do I want to follow GLSL in every stupid decision it made (like column major matrices).
- For me,
dvec2
awkwardly looks like a differential. - GLSL might be less relevant in the future as we could compile other languages into SPIR-V.
- Sn.Math goals are more diverse than interfacing with the GPU.
As for CUDA, there’s simply no obvious way to generalize their scheme for non-vector types, like boxes or quaternions.
Therefore, weighting all of the above, I chose to loosely follow OpenGL suffixes, as they are the most straight forward of the above schemes.
Why use the same type for colors and vectors but not for vectors and quaternions?
What else, vectors and boxes? Quaternions and boxes? The decision is not based on the number of components. Instead it’s what the operators are supposed to do.
The operations performed on colors are identical to those of vectors. This is because linear color spaces are vector spaces where color addition and intensity amplifications naturally correspond to vector addition and scalar multiplication. Boxes and quaternions, on the other hand, have a different mathematical structure:
- Quaternions are an associative normed division algebra. They are intrinsically 4-dimensional objects (over reals).
- Boxes are a lattice having no meaningful multiplication at all.
Footnotes
Sn.Math in fact provides specializations with alphabetic names in a separate header. Due to the ODR, however, a program using those specializations in one place must include them in all other translation units that use the library, including in the Sn.Math implementation files. This is only formally speaking. In practice, as long as the ABI of the specializations is compatible as in this case, we can ignore the formalities.
Which cannot be properly implemented in a C++ library.