Skip to content

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.

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.

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 lefta * b applies b, then a Standard mathematical notation
Handedness Right-handedCross(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 w first. Storing w last makes Quat bit-compatible with Vec4, 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.

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 projection
Vec3 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.

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 coincide

IsNormalized 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.

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.

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.

Signatures, preconditions and assertions are documented in basalt/core/Vector.h, Matrix.h and Quaternion.h.

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.
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.
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.
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.