Geometry types
Vec2, Vec3, Vec4, Vec2i, Mat2, Mat3, Mat4 and Quat are packed aggregates of f32. They have no constructors, no padding and no hidden state. Operations on them are overloaded free functions and operators, so a + b and Dot(a, b) read the way the mathematics does.
A geometry library’s real contract is its conventions — handedness, matrix order, depth range, quaternion component order. Each one is invisible while your code is self-consistent, and produces a mirrored or inverted result the moment it meets code that assumed the other choice.
Packed plain data, guaranteed
Section titled “Packed plain data, guaranteed”Every type’s size is asserted at compile time:
| Type | Size | Note |
|---|---|---|
Vec2, Vec2i |
8 bytes | Two packed components |
Vec3 |
12 bytes | Three packed f32 — no fourth padding lane |
Vec4, Quat |
16 bytes | Quat is layout-compatible with Vec4 |
Mat2 |
16 bytes | |
Mat3 |
36 bytes | Nine packed f32 — not three padded columns |
Mat4 |
64 bytes |
sizeof(Vec3) == 12 is a deliberate commitment. A padded 16-byte Vec3 would load into an SSE register in a single instruction, which is a real advantage and the reason some libraries pad it.
Basalt keeps the layout tight because the padding leaks outward. An array of padded Vec3 wastes a quarter of its memory and its cache lines, and it cannot be uploaded to a GPU vertex buffer or written to a file without repacking. These types cross those boundaries constantly, so the tight layout is worth more than the free register load.
What the static_assert guarantees is tight packing: an array of Vec3 is a continuous run of three-float triples with no padding between elements. So arena.Push<Vec3>(n) gives you a buffer you can upload or write out without repacking it first, provided the consumer’s layout is declared as tightly packed too.
The conventions
Section titled “The conventions”These are the decisions that cause silent, hard-to-find bugs when they differ between two pieces of code.
| Convention | Basalt’s choice | Who else uses it |
|---|---|---|
| Matrix storage | Column-major, indexed m[column][row] |
GLSL, HLSL, Vulkan, WebGPU, glm, DirectXMath, Blender |
| Composition order | Right to left — a * b applies b, then a |
Standard mathematical notation |
| Handedness | Right-handed — Cross(x, y) == z |
OpenGL, glm, Blender, Maya |
| Camera direction | Looks down −Z | Right-handed convention |
| Projection depth | Maps to [0, 1] | Vulkan, Direct3D, WebGPU |
| Angles | Radians, everywhere | — |
| Quaternion order | (x, y, z, w), w scalar last |
Graphics convention; Vec4-compatible |
Two of these deserve emphasis, because they are the ones most likely to differ from what you last worked with:
- Depth maps to [0, 1], not [−1, 1]. OpenGL uses [−1, 1]; Vulkan, D3D and WebGPU use [0, 1]. Basalt takes the latter because OpenGL is not a planned backend. If you port a projection matrix from OpenGL code, this is the line that will be wrong.
- Quaternion storage is not the textbook order. Mathematical texts usually write
wfirst. Storingwlast makesQuatbit-compatible withVec4, which is what lets it upload and serialise like one.
Column-major storage means a Mat4 uploads to a shader uniform with no transposition. It also means the array index order reads backwards from the mathematical subscript: m[c][r] is row r of column c.
No Euler angles as storage
Section titled “No Euler angles as storage”Basalt provides Mat4FromAxisAngle, QuatFromAxisAngle and QuatFromToRotation, but no type that stores a rotation as three Euler angles.
Two reasons, both concrete. Euler angles gimbal-lock: at certain orientations two of the three axes become degenerate and the rotation loses a degree of freedom. And their meaning depends on an axis-order convention — XYZ, ZYX, YXZ and others are all in use, with no agreement between tools.
Rotations are stored as a Quat or a matrix. Euler angles are fine as a user-interface input, converted at the boundary.
Operators, because the notation already exists
Section titled “Operators, because the notation already exists”Arithmetic on these types uses operators, which is a deliberate exception to Basalt’s preference for free functions over methods:
Vec3 offset = position + velocity * dt;Mat4 transform = projection * view * model; // applied model, then view, then projectionVec3 rotated = Rotate(orientation, direction);Named free functions cover everything that is not arithmetic: Dot, Cross, Length, Normalize, Lerp, Slerp, Transpose, Inverse.
The reason operators earn their place here is that the notation predates the code. projection * view * model is how the operation is written in every graphics text, and spelling it Mat4Multiply(projection, Mat4Multiply(view, model)) would obscure a formula the reader already knows.
Note the order: a * b applies b first. So the rightmost matrix is closest to the model.
Normalising, and the zero-length case
Section titled “Normalising, and the zero-length case”Normalize asserts on a zero-length vector, because dividing by zero there is a programming error rather than a condition to handle.
NormalizeOrZero returns a zero vector instead. Use it when the input genuinely can be degenerate — a direction between two coincident points, for example.
Vec3 direction = NormalizeOrZero(target - origin); // safe when they coincideIsNormalized tests within a tolerance, which is what you want in an assertion. Exact comparison against 1.0 fails on values that are normalised for every practical purpose.
Two inverses for matrices
Section titled “Two inverses for matrices”Inverse is the general matrix inverse. InverseRigid inverts a matrix that is known to contain only rotation and translation — no scale, no shear.
InverseRigid is substantially cheaper, because for a rigid transform the inverse is a transpose of the rotation part plus a negated, rotated translation. It gives a wrong answer on a matrix that has scale in it, so the name states the precondition rather than trying to detect it.
Camera view matrices are rigid, and that is the common case where this matters.
Where the SIMD is, and where it is not
Section titled “Where the SIMD is, and where it is not”Mat4 products carry a hand-written SSE implementation, measured at 2.3×–2.6× faster than the plain arithmetic. It sits behind a compile-time check so the same function remains usable in a constexpr context, and both paths are verified to produce bit-identical results.
TransformPoint and TransformDirection also have span overloads for transforming many vectors at once, measured at 11.75× the per-call version.
What is deliberately not vectorised is as informative. Reductions like Dot and Length take a vector and return one number, and deinterleaving an array to feed the vector units costs more than the arithmetic saves — the “optimised” version measured slower than the plain loop. So no batch overload exists for them.
A batch overload ships only where it measured faster. An API that is slower than the obvious loop is worse than no API.
API summary
Section titled “API summary”Signatures, preconditions and assertions are documented in basalt/core/Vector.h, Matrix.h and Quaternion.h.
Vector operations
Section titled “Vector operations”| Signature | Purpose |
|---|---|
Dot(a, b) |
Dot product. |
Cross(a, b) |
Cross product. Vec3 only. |
Length(v) / LengthSquared(v) |
Magnitude. Prefer the squared form for comparisons. |
Distance(a, b) / DistanceSquared(a, b) |
Separation between two points. |
Normalize(v) |
Unit vector. Asserts on zero length. |
NormalizeOrZero(v) |
Unit vector, or zero when degenerate. |
IsNormalized(v) |
Whether the length is 1 within a tolerance. |
Lerp(a, b, t) |
Linear interpolation. |
Reflect(v, normal) |
Mirror across a plane. |
Project(v, onto) / ProjectToVec3(v) |
Vector projection; Vec4 to Vec3 division by w. |
Min(a, b) / Max(a, b) / Abs(v) |
Component-wise. |
Matrix construction
Section titled “Matrix construction”| Signature | Purpose |
|---|---|
Mat4Identity() / Mat4Zero() |
Constants. Also Mat2Identity, Mat3Identity. |
Mat4FromTranslation(v) / Mat4FromScale(v) |
Single-component transforms. |
Mat4FromAxisAngle(axis, radians) |
Rotation about an arbitrary axis. |
Mat4FromTransform(translation, rotation, scale) |
A full transform in one call. |
Mat4FromQuat(q) |
Rotation matrix from a quaternion. |
Mat4FromLookAt(eye, target, up) |
View matrix. Looks down −Z. |
Mat4Perspective(...) / Mat4Orthographic(...) |
Projection. Depth maps to [0, 1]. |
Mat2FromRotation(radians) / Mat2FromScale(v) |
2D transforms. |
Matrix operations
Section titled “Matrix operations”| Signature | Purpose |
|---|---|
a * b |
Composition. Applies b, then a. |
Transpose(m) |
Reflect across the diagonal. |
Determinant(m) |
Scalar determinant. |
Inverse(m) |
General inverse. |
InverseRigid(m) |
Cheaper inverse for rotation-plus-translation only. |
TransformPoint(m, v) |
Transform including translation. |
TransformDirection(m, v) |
Transform ignoring translation. |
TransformPoint(m, in, out) |
Span overload, for many vectors at once. |
Quaternions
Section titled “Quaternions”| Signature | Purpose |
|---|---|
QuatIdentity() |
No rotation. |
QuatFromAxisAngle(axis, radians) |
Rotation about an axis. |
QuatFromToRotation(from, to) |
The shortest rotation between two directions. |
QuatFromMat4(m) |
Extract rotation from a matrix. |
Rotate(q, v) |
Apply a rotation to a vector. |
Slerp(a, b, t) |
Spherical interpolation. Constant angular velocity. |
Conjugate(q) |
The inverse rotation, for a unit quaternion. |
Normalize(q) / IsNormalized(q) |
Keep a quaternion unit-length. |