Tutorial 33: Matrices and Transformations

3D Math  ·  Intermediate

Every 3D object you draw passes through three matrix transforms before it reaches the screen. Understanding these matrices — and how to compose them — is the foundation of all 3D rendering in CNA.

World / View / Projection Explained

Three coordinate spaces are involved in rendering a 3D scene:

  • Object space — vertices are defined relative to the mesh origin.
  • World space — the World matrix places the object in the shared scene.
  • View space — the View matrix repositions everything relative to the camera.
  • Clip space — the Projection matrix applies perspective and maps to [-1, 1].

The full transform applied to each vertex is:

clip_pos = Projection * View * World * vertex_pos;

In CNA (following XNA convention) you pass these three matrices separately to BasicEffect and the effect combines them internally.

Matrix::CreateTranslation

Matrix::CreateTranslation produces a matrix that moves an object to a position in world space.

// Move an object to (5, 0, 0) — e.g. position a planet
Matrix world = Matrix::CreateTranslation(5.0f, 0.0f, 0.0f);

// Or using a Vector3
Matrix world = Matrix::CreateTranslation(Vector3(5.0f, 0.0f, 0.0f));

Matrix::CreateRotationX / Y / Z

Rotates around the X, Y, or Z axis. Angles are always in radians.

float angle = MathHelper::ToRadians(45.0f); // 45 degrees → radians

Matrix rotX = Matrix::CreateRotationX(angle); // pitch
Matrix rotY = Matrix::CreateRotationY(angle); // yaw  — most common "spin"
Matrix rotZ = Matrix::CreateRotationZ(angle); // roll

Radians everywhere. CNA follows XNA: all angle parameters use radians. Convert with MathHelper::ToRadians(degrees). Passing degrees directly gives a rotation 57× smaller than expected.

Matrix::CreateScale

Scales the mesh. Pass a single float for uniform scaling or a Vector3 for non-uniform.

Matrix bigSun  = Matrix::CreateScale(2.0f);           // 2× in all axes
Matrix tinyMoon = Matrix::CreateScale(0.3f);           // 30% size
Matrix stretched = Matrix::CreateScale(Vector3(2, 1, 0.5f));

Matrix::CreateLookAt

CreateLookAt builds a view matrix from a camera position, a target point, and an up direction.

Matrix view = Matrix::CreateLookAt(
    Vector3(0.0f, 10.0f, 20.0f),   // camera position
    Vector3::Zero,                  // look at origin
    Vector3::Up                     // world up (0,1,0)
);

Matrix::CreatePerspectiveFieldOfView

Builds a perspective projection. The nearPlane should be as large as your scene allows — too small causes depth precision problems (see Tutorial 39).

float aspect = (float)screenWidth / (float)screenHeight;

Matrix proj = Matrix::CreatePerspectiveFieldOfView(
    MathHelper::PiOver4,   // 45° field of view (Pi/4 radians)
    aspect,                // e.g. 800/600 ≈ 1.333
    0.1f,                  // near plane
    1000.0f                // far plane
);

Matrix Multiplication Order (TRS)

CNA uses the same row-major convention as XNA. When composing Scale, Rotation, and Translation, the order is:

// Correct TRS order: Scale first, Rotate second, Translate last
Matrix world = Matrix::CreateScale(scale)
             * Matrix::CreateRotationY(rotation)
             * Matrix::CreateTranslation(position);

Reversing the order gives wrong results — for example, translating before rotating causes the object to orbit the origin instead of spinning in place. Think of it as: first shrink/grow the mesh, then spin it, then move it to its final position.

Matrix::Invert

The inverse of a matrix undoes its transform. Common uses: converting world-space coordinates back to object space, or deriving a view matrix from a camera's world transform.

Matrix cameraWorld = Matrix::CreateTranslation(cameraPos)
                   * Matrix::CreateRotationY(yaw);

// View matrix = inverse of camera's world matrix
Matrix view = Matrix::Invert(cameraWorld);

// Some overloads return a bool (false if matrix is singular/non-invertible)
Matrix result;
bool ok = Matrix::Invert(someMatrix, result);
if (!ok) { /* degenerate matrix — scale is zero or similar */ }

Solar System Demo

This demo draws three objects — Sun, Earth, Moon — using parent-child transform composition. Each child's world matrix is built by multiplying its local transform by its parent's world matrix.

#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/Graphics/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Graphics/BasicEffect.hpp"
#include "Microsoft/Xna/Framework/Graphics/VertexBuffer.hpp"
#include "Microsoft/Xna/Framework/Graphics/VertexPositionColor.hpp"
#include "Microsoft/Xna/Framework/MathHelper.hpp"

using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Graphics;

// Build a simple flat cube (8 vertices, triangle list — 36 indices drawn as 12 triangles)
// Returns vertex count; caller passes verts array of size >=8.
static void BuildCube(std::vector<VertexPositionColor>& verts, Color col) {
    float h = 0.5f;
    Vector3 corners[8] = {
        {-h,-h,-h},{+h,-h,-h},{+h,+h,-h},{-h,+h,-h},
        {-h,-h,+h},{+h,-h,+h},{+h,+h,+h},{-h,+h,+h}
    };
    int faces[36] = {
        0,1,2, 0,2,3,  // back
        4,6,5, 4,7,6,  // front
        0,3,7, 0,7,4,  // left
        1,5,6, 1,6,2,  // right
        3,2,6, 3,6,7,  // top
        0,4,5, 0,5,1   // bottom
    };
    verts.clear();
    for (int i : faces)
        verts.push_back({corners[i], col});
}

class SolarSystemGame final : public Game {
public:
    SolarSystemGame() : graphics_(this) {
        graphics_.setPreferredBackBufferWidth(800);
        graphics_.setPreferredBackBufferHeight(600);
    }

protected:
    void LoadContent() override {
        auto& gd = getGraphicsDeviceProperty();
        effect_ = std::make_unique<BasicEffect>(gd);
        effect_->setVertexColorEnabled(true);

        // Sun — yellow
        BuildCube(sunVerts_, Color::Yellow);
        sunVB_ = std::make_unique<VertexBuffer>(gd,
            VertexPositionColor::VertexDeclaration,
            (int)sunVerts_.size(), BufferUsage::None);
        sunVB_->SetData(sunVerts_.data(), (int)sunVerts_.size());

        // Earth — blue-green
        BuildCube(earthVerts_, Color(0, 100, 200));
        earthVB_ = std::make_unique<VertexBuffer>(gd,
            VertexPositionColor::VertexDeclaration,
            (int)earthVerts_.size(), BufferUsage::None);
        earthVB_->SetData(earthVerts_.data(), (int)earthVerts_.size());

        // Moon — light grey
        BuildCube(moonVerts_, Color(180, 180, 180));
        moonVB_ = std::make_unique<VertexBuffer>(gd,
            VertexPositionColor::VertexDeclaration,
            (int)moonVerts_.size(), BufferUsage::None);
        moonVB_->SetData(moonVerts_.data(), (int)moonVerts_.size());
    }

    void Update(GameTime& gameTime) override {
        float dt = (float)gameTime.getElapsedGameTime().TotalSeconds();
        sunAngle_   += dt * 0.3f;                   // slow spin
        earthOrbit_ += dt * 1.0f;                   // orbit speed
        earthSpin_  += dt * 2.5f;                   // self-rotation
        moonOrbit_  += dt * 3.0f;                   // moon orbits earth
    }

    void Draw(const GameTime&) override {
        auto& gd = getGraphicsDeviceProperty();
        gd.Clear(Color(5, 5, 20));                   // near-black sky

        Matrix view = Matrix::CreateLookAt(
            Vector3(0.0f, 12.0f, 18.0f),
            Vector3::Zero,
            Vector3::Up);
        Matrix proj = Matrix::CreatePerspectiveFieldOfView(
            MathHelper::PiOver4, 800.0f / 600.0f, 0.1f, 1000.0f);

        effect_->setView(view);
        effect_->setProjection(proj);

        // --- Sun (center, large, slow spin) ---
        Matrix sunWorld = Matrix::CreateScale(2.0f)
                        * Matrix::CreateRotationY(sunAngle_);
        DrawMesh(*sunVB_, sunWorld);

        // --- Earth (orbits sun, spins on own axis) ---
        Matrix earthLocal = Matrix::CreateScale(0.7f)
                          * Matrix::CreateRotationY(earthSpin_)
                          * Matrix::CreateTranslation(6.0f, 0.0f, 0.0f);
        Matrix earthOrbitMat = Matrix::CreateRotationY(earthOrbit_);
        Matrix earthWorld = earthLocal * earthOrbitMat;  // child * parent
        DrawMesh(*earthVB_, earthWorld);

        // --- Moon (orbits earth, inherits earth's position) ---
        Matrix moonLocal = Matrix::CreateScale(0.3f)
                         * Matrix::CreateTranslation(1.8f, 0.0f, 0.0f);
        Matrix moonOrbitMat = Matrix::CreateRotationY(moonOrbit_);
        // Moon is child of Earth: local orbit → orbit around earth centre → earth world
        Matrix moonWorld = moonLocal * moonOrbitMat * earthOrbitMat
                         * Matrix::CreateTranslation(6.0f, 0.0f, 0.0f);
        DrawMesh(*moonVB_, moonWorld);

        gd.Present();
    }

private:
    void DrawMesh(VertexBuffer& vb, const Matrix& world) {
        auto& gd = getGraphicsDeviceProperty();
        effect_->setWorld(world);
        gd.setVertexBuffer(vb);
        for (auto& pass : effect_->getCurrentTechnique().Passes) {
            pass.Apply();
            gd.DrawPrimitives(PrimitiveType::TriangleList, 0,
                              vb.getVertexCount() / 3);
        }
    }

    GraphicsDeviceManager graphics_;
    std::unique_ptr<BasicEffect> effect_;

    std::vector<VertexPositionColor> sunVerts_, earthVerts_, moonVerts_;
    std::unique_ptr<VertexBuffer> sunVB_, earthVB_, moonVB_;

    float sunAngle_   = 0.0f;
    float earthOrbit_ = 0.0f;
    float earthSpin_  = 0.0f;
    float moonOrbit_  = 0.0f;
};

int main() { SolarSystemGame game; game.Run(); }

Next Steps