Skip to content
The CNA BibleCNA 0.1.0-alpha.1 Edition

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

    Vector3::Forward is (0,0,1) and Vector3::Backward is (0,0,1): CNA follows XNA’s right-handed world basis.

  2. 2.

    CNA uses row-vector transforms. A position is conceptually multiplied on the left: p=pM.

  3. 3.

    Matrix products read in application order. With row vectors, rotation * translation rotates first and translates second.

  4. 4.

    Perspective clip space needs four components. Preserve clip-space W until clipping and the perspective divide have completed.

This explains the standard world/view/projection product:

pclip=pobjectMworldMviewMprojection.

The public depth convention is [0,1]. 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 [0,1] 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.

1 // Illustrative: move toward a target without overshooting it.
2 Vector2 toTarget = target - position;
3 const float distance = toTarget.Length();
4 if (distance > 0.0001f)
5 {
6 toTarget.Normalize();
7 position = position + toTarget *
8 std::min(speed * deltaSeconds, distance);
9 }

Cross-product order sets the normal’s sign. For a triangle (a,b,c), Cross(b - a, c - a) reverses if b and c 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 W needed by a software rasterizer or a manual clipping stage. Vector4 accepts a Vector3 input directly:

1 const Matrix combined = world * view * projection;
2 const Vector4 clip = Vector4::Transform(position, combined);
3 if (clip.W != 0.0f)
4 {
5 const Vector3 ndc(
6 clip.X / clip.W, clip.Y / clip.W, clip.Z / clip.W);
7 }

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 i corresponds to HLSL row i. 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 a followed by b. 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.

1 Quaternion facing = Quaternion::CreateFromYawPitchRoll(
2 0.0f, 0.0f, 0.0f);
3 const Quaternion target = Quaternion::CreateFromYawPitchRoll(
4 MathHelper::PiOver2, 0.0f, 0.0f);
5
6 facing = Quaternion::Slerp(
7 facing, target, std::min(1.0f, turnSpeed * deltaSeconds));
8 const Matrix orientation = Matrix::CreateFromQuaternion(facing);

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.

1 // Illustrative: choose the nearest successfully reported hit.
2 const SceneObject* nearest = nullptr;
3 float nearestDistance = std::numeric_limits<float>::max();
4 for (const SceneObject& candidate : candidates)
5 {
6 if (const auto hit = pickRay.Intersects(candidate.GetBoundingBox());
7 hit && *hit < nearestDistance)
8 {
9 nearestDistance = *hit;
10 nearest = &candidate;
11 }
12 }

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 (2,3,4), a radius of 2 becomes 8. 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:

1 const Color straight(200, 80, 40, 128);
2 const Color premultiplied =
3 Color::FromNonPremultiplied(200, 80, 40, 128);

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.

1 Curve height;
2 height.getKeysProperty().Add(CurveKey(0.0f, 10.0f));
3 height.getKeysProperty().Add(CurveKey(2.0f, 40.0f));
4 height.getKeysProperty().Add(CurveKey(4.0f, 10.0f));
5 height.ComputeTangents(CurveTangent::Smooth);
6 height.setPreLoopProperty(CurveLoopType::Constant);
7 height.setPostLoopProperty(CurveLoopType::Constant);
8
9 const float cameraHeight = height.Evaluate(elapsedSeconds);

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 [0,1] 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.

Both XNB and CNJ readers can construct curves. Their file-format and resolution contracts live in Chapters 35 and 36; successful in-memory evaluation does not establish either loading route.

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:

1 const float angle = MathHelper::ToRadians(30.0f);
2 const Matrix world = Matrix::CreateRotationZ(angle) *
3 Matrix::CreateTranslation(Vector3::Zero);
4 const Matrix view = Matrix::CreateLookAt(
5 Vector3(0.0f, 0.0f, 3.0f), Vector3::Zero, Vector3::Up);
6 const Matrix projection = Matrix::CreatePerspectiveFieldOfView(
7 MathHelper::PiOver4, 1.0f, 0.1f, 100.0f);
8
9 BasicEffect fx(dev);
10 fx.VertexColorEnabled = true;
11 fx.World = world;
12 fx.View = view;
13 fx.Projection = projection;
14 fx.Apply();
Software-renderer matrix demonstration showing a colored three-dimensional object after world rotation, camera view, and projection transforms.
Figure 7.1: Software-renderer output from the retained matrix demo. The image verifies the composed rotation, view, projection, effect application, draw, and pixel path on the pinned configuration.

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

Type at least three characters. Results are ranked by how often and where the words occur.