Chapter 7 Math Types and Coordinate Conventions
CNA’s math module carries the values that connect game code, content, effects, and renderers. Its names follow XNA, but correct use depends on conventions that method names do not reveal: row-vector matrix composition, a right-handed world basis, packed-color layout, interval boundaries, and several incomplete geometric paths. This chapter explains those contracts. Appendix A is the compact API inventory; the module headers remain the signature authority.
7.1 Mental model
Except for BoundingFrustum, the core types are C++ value types with public components or fields. Copying a Vector3, Matrix, Rectangle, or Color copies its value; no framework identity or heap ownership is involved. BoundingFrustum instead owns derived planes and corners behind its matrix property, so it behaves more like a small object than a plain aggregate.
Most arithmetic families provide two spellings:
-
•
a value-returning form, such as Vector3::Normalize(value);
-
•
an output-reference form with a trailing result&, mirroring XNA’s ref/out overloads.
They are separate public declarations. Do not assume the output-reference form is alias-safe: some implementations write fields while still reading the input. The live Plane::Transform defect later in this chapter is the consequential example.
7.2 Common value surface
The vectors, matrices, quaternions, planes, rays, integer geometry, bounding volumes, and CurveKey expose equality and hashing; most also expose ToString. Hash return types are not uniform: the port uses int, intcs, or std::size_t according to the type. Code that abstracts over these values should use auto or its own normalized hash type.
Several types retain private debugger-display helpers, and the floating-point vectors, Matrix, and Quaternion define CheckForNaNs. No external call site or debugger visualizer was located at the pin. Vector checks are compiled unconditionally; matrix and quaternion checks disappear under NDEBUG. This difference has no current runtime effect because none of the helpers is called.
7.3 Coordinate and composition contract
Four rules carry into every graphics chapter:
-
1.
Vector3::Forward is and Vector3::Backward is : CNA follows XNA’s right-handed world basis.
-
2.
CNA uses row-vector transforms. A position is conceptually multiplied on the left: .
-
3.
Matrix products read in application order. With row vectors, rotation * translation rotates first and translates second.
-
4.
Perspective clip space needs four components. Preserve clip-space until clipping and the perspective divide have completed.
This explains the standard world/view/projection product:
The public depth convention is . A renderer may translate that convention for its native API, but application matrices and effects remain XNA-shaped.
7.4 Vectors
Vector2, Vector3, and Vector4 provide public components, zero/one and unit constants, arithmetic, dot products, distances, normalization, reflection, clamping, and the XNA interpolation family. Vector3 adds directions and Cross; Vector4 adds lower-dimensional transform inputs. Operations such as Lerp are not clamped, so an amount outside extrapolates.
7.4.1 Operation groups
| Group | Public families and boundary |
|---|---|
| Arithmetic | Add, Subtract, component/scalar Multiply and Divide, Negate, operators, Min, Max, and Clamp. |
| Measurement | Length, LengthSquared, Distance, DistanceSquared, and Dot; Vector3 adds Cross. |
| Interpolation | Lerp, SmoothStep, Barycentric, CatmullRom, and Hermite. Amounts are caller-controlled. |
| Transforms | Matrix and quaternion Transform, plus array/range forms. TransformNormal omits translation and exists for Vector2/ Vector3. |
Normalizing a zero vector divides by zero; CNA does not substitute a fallback direction. Validate input when non-finite output would escape into gameplay or rendering.
Cross-product order sets the normal’s sign. For a triangle , Cross(b - a, c - a) reverses if and are exchanged; the same winding choice later controls face culling.
7.4.2 Why Vector4 matters
Vector3::Transform(position, projection) returns three components and therefore cannot carry the projected needed by a software rasterizer or a manual clipping stage. Vector4 accepts a Vector3 input directly:
The Software renderer uses this four-component route before clipping and division. The snippet is illustrative; production code must also handle the clip volume and the sign and magnitude of W.
7.5 Matrix
Matrix stores M11 through M44. Translation occupies the fourth row, and direction properties expose the first three rows according to the right-handed basis. The principal factory groups are translation, scale, rotations, quaternion conversion, look-at/world, orthographic and perspective projections, billboards, planar shadow, and reflection.
7.5.1 Factory and arithmetic groups
| Purpose | Representative methods |
|---|---|
| Object transform | CreateTranslation, CreateScale, CreateRotationX/Y/Z, CreateFromAxisAngle, CreateFromQuaternion, CreateFromYawPitchRoll. |
| Camera/projection | CreateLookAt, CreateWorld, CreateOrthographic, CreateOrthographicOffCenter, CreatePerspective*. |
| Special geometry | CreateBillboard, CreateConstrainedBillboard, CreateShadow, CreateReflection. |
| Algebra | Add, Subtract, matrix/scalar Multiply and Divide, Negate, Invert, Transpose, and component-wise Lerp. |
Matrix::Lerp interpolates all sixteen fields. It is not a substitute for decomposing a transform and interpolating its rotation with Quaternion::Slerp. Likewise, Invert does not report a singular input; callers that may receive degenerate transforms need an independent validity check.
Decompose(scale, rotation, translation) returns false if any recovered axis scale is near zero and sets rotation to identity. Scale and translation remain useful, but no unique rotation can be extracted from the degenerate matrix.
7.5.2 The ToColumnMajor bridge
CNAEXT Matrix::ToColumnMajor(float[16]) copies the sixteen fields in declaration order. It does not numerically transpose the matrix. The bridge relies on the receiving shader API interpreting those bytes as column-major storage: GLSL column corresponds to HLSL row . The dedicated identity test cannot distinguish a copy from a transpose, so renderer code and non-identity tests remain the evidence for this convention.
7.6 Quaternion
Quaternion provides axis-angle, rotation-matrix, and yaw/pitch/roll construction; normalization, conjugate and inverse; arithmetic; Lerp; and Slerp.
7.6.1 Composition and interpolation
Quaternion multiplication is easy to read backwards: q1 * q2 represents q2’s rotation followed by q1’s. The explicitly named Quaternion::Concatenate(a, b) instead promises followed by . Use the named form when operation order should be obvious at the call site.
Lerp chooses the short arc, linearly mixes components, and normalizes the result. Slerp follows the sphere and gives uniform angular progress. For small turns the difference is usually negligible; a long slow rotation can expose Lerp’s ease-in/ease-out rate.
The retained executable tools/cna-screenshot-infra/quaternion_vs_matrix_demo.cpp compares a Z-axis matrix rotation with the corresponding axis-angle quaternion route. At the pinned build used for the artifact, the matrix components, transformed point, and rendered image agreed.
7.7 Planes, rays, and bounding volumes
Plane stores a normal and distance; DotCoordinate includes the distance term and classifies a point’s side, whereas DotNormal tests a direction only. Ray stores an origin and direction and returns std::optional<float> for intersection distance. BoundingBox, BoundingSphere, and BoundingFrustum expose containment and intersection families.
7.7.1 Operational summary
| Type | Useful operations | Boundary |
|---|---|---|
| Plane | point/direction dot tests, normalization, volume intersection, matrix or quaternion transform | Matrix transform is defective at the pin; see below. |
| Ray | box, sphere, plane, and frustum intersection | Tolerances differ by shape; a zero direction is not a useful point query. |
| BoundingBox | create from points/sphere, merge, corners, containment/intersection | Touching and empty-input behavior follows individual implementations and exception types. |
| BoundingSphere | create from points/box, merge, transform, containment/intersection | A non-uniform transform remains a conservative sphere, not an ellipsoid. |
| BoundingFrustum | matrix-derived planes/corners, containment/intersection | Ray intersection currently checks origin containment rather than computing entry distance. |
7.7.2 Current geometry defects
Limitation. Plane matrix transform. The intended inverse-transpose path calls Matrix::Transpose(transformedMatrix, transformedMatrix). That output-reference overload is not alias-safe: it overwrites fields that later reads still need. Identity and diagonal scale hide the error; a translation or other nonsymmetric inverse exposes it. Avoid Plane::Transform(plane, matrix) for nontrivial matrices at 1bb2145d. The quaternion overload does not use this path.
Limitation. Frustum/ray intersection. The pinned implementation classifies only the ray origin. An origin outside the frustum returns nullopt, even when its direction enters the volume; an origin inside returns zero. The remaining Intersects branch throws NotImplementedException. This is not a general ray/frustum intersection algorithm.
Ray/box, ray/plane, and ray/sphere paths also use different parallel and behind-origin tolerances. A zero-direction ray inside a box currently returns nullopt; validate and normalize picking directions before querying several shapes.
7.7.3 Transforming a bounding sphere
BoundingSphere::Transform transforms the center and scales the radius by the largest length of the matrix’s three basis rows. Under a non-uniform scale , a radius of becomes . The result encloses the transformed ellipsoid, which is safe for broad-phase culling but looser than an ellipsoid or oriented bound.
7.8 Integer geometry and Color
Point is the integer coordinate pair. Rectangle adds bounds, containment, intersection/union, offset, and inflation. Rectangle right and bottom edges are exclusive for point containment.
Rectangle::IsEmpty means exactly X == Y == Width == Height == 0. A zero-area rectangle at another position is not empty by that property. Use Width > 0 && Height > 0 when the question is whether a region has area.
Color stores byte channels and a packed UInt32. Its numeric packed form is AABBGGRR; on little-endian systems the four packed bytes appear as RGBA. That statement does not describe the layout of the polymorphic C++ object itself, so renderer uploads must use the supported conversion path instead of reinterpreting an array of Color.
Color::FromNonPremultiplied scales straight RGB channels by alpha for CNA’s default premultiplied-alpha blend path:
The type can hold either convention; the selected BlendState determines how the channels are interpreted. Named color constants are generated API surface, not evidence about blend behavior.
7.9 MathHelper and degenerate inputs
MathHelper collects angle constants, unit conversion, clamping, interpolation, distance, wrapping, and epsilon comparison. Lerp is unclamped; SmoothStep clamps its amount before applying the cubic curve. WrapAngle maps to a principal range around zero.
Most lower-level math routines follow floating-point arithmetic instead of throwing on degenerate inputs. Zero-vector normalization, singular inversion, coincident look-at inputs, and invalid projection ranges can therefore yield infinities or NaNs. This policy is not uniform: some factory methods validate selected range conditions while adjacent operations do not. Validate at the boundary where bad values can affect gameplay, content, or a native API.
7.10 Curves
Curve evaluates an ordered CurveKeyCollection. Keys contain position, value, incoming/outgoing tangents, and smooth/step continuity. Add inserts by ascending position. ComputeTangents supports flat, linear, and smooth modes. Before and after the key span, Constant, Linear, Cycle, CycleOffset, and Oscillate choose clamping, extrapolation, wrapping, accumulated wrapping, or ping-pong behavior.
This compiles against the public shape when the math headers and namespace aliases used by the application are present; it is an illustrative fragment rather than a complete program.
7.10.1 Current curve deviations
Three source-level findings matter for nontrivial key positions:
-
•
Step evaluation compares the absolute query position with 1.0f, not with the next key’s position. Step intervals away from the canonical span can switch to the next value too early or too late.
-
•
Post-loop linear extrapolation uses the first key’s outgoing tangent while extending from the last key.
-
•
Smooth tangent computation uses materially different near-zero thresholds for incoming and outgoing spans, so duplicate or nearly duplicate positions can produce asymmetric tangents.
7.11 Compiled world/view/projection example
The retained source tools/cna-screenshot-infra/math_rotation_demo.cpp constructs a complete game, builds a world/view/projection chain, applies BasicEffect, and draws a colored triangle. Its central sequence is:
Evidence. The image is a pixel artifact for one complete path, not a conformance oracle for every math operation. The math test suite supplies analytic expectations at several tolerances, but this edition located no captured XNA numeric corpus covering composition order, singular inputs, corner ordering, or non-identity plane transforms. Porting tests should compare the original program’s observable results: projected positions, collision classifications, curve samples, or captured reference values.
7.12 Summary
CNA’s math API is broadly XNA-shaped, but its useful contract is the combination of value semantics, row-vector composition, right-handed directions, and operation-specific edge cases. Keep clip-space , do not reinterpret polymorphic values as packed GPU bytes, validate degenerate inputs, and avoid the current plane-matrix and frustum-ray paths where their missing behavior matters.