Tutorial 42: Matrix Operations and Transforms

CNA — C++ XNA 4.0 reimplementation

The CNA Matrix type is a 4×4 row-major float matrix. It covers the full XNA 4.0 matrix API including factory methods for common transforms, decomposition, shadow projection, reflection planes, and billboards.

Identity and basic construction

#include "Microsoft/Xna/Framework/Matrix.hpp"
using namespace Microsoft::Xna::Framework;

Matrix I = Matrix::Identity; // no-op transform

// Build a world matrix from TRS components
Matrix world = Matrix::CreateScale(2.0f)
             * Matrix::CreateRotationY(MathHelper::PiOver4)
             * Matrix::CreateTranslation(5.0f, 0.0f, 0.0f);

// Non-uniform scale
Matrix stretch = Matrix::CreateScale(1.0f, 2.0f, 1.0f);

Multiply

Matrix multiplication in CNA follows the XNA row-vector convention. Transforms are applied left-to-right: Scale * Rotation * Translation applies scale first, then rotation, then translation.

Matrix scale = Matrix::CreateScale(3.0f);
Matrix rot   = Matrix::CreateRotationZ(MathHelper::PiOver2);
Matrix trans = Matrix::CreateTranslation(10, 0, 0);

// SRT order (common for world matrices)
Matrix srt   = scale * rot * trans;

// Equivalent static helper
Matrix srt2  = Matrix::Multiply(scale, Matrix::Multiply(rot, trans));

Transpose

Matrix m = Matrix::CreateRotationX(0.5f);
Matrix t = Matrix::Transpose(m);
// For orthonormal rotation matrices, Transpose == Inverse

Invert

Matrix view = Matrix::CreateLookAt(
    Vector3(0, 5, 10), Vector3::Zero, Vector3::Up);

Matrix invView;
bool ok = Matrix::Invert(view, invView);
if (ok) {
    // invView transforms from clip space back to world space
    // (used for ray casting from screen coordinates)
}

Decompose — extract translation, rotation, scale

Matrix world2 = Matrix::CreateScale(2, 1, 3)
              * Matrix::CreateRotationY(1.0f)
              * Matrix::CreateTranslation(7, -2, 4);

Vector3    scale2;
Quaternion rotation;
Vector3    translation;

bool ok2 = world2.Decompose(scale2, rotation, translation);
// scale2      ≈ (2, 1, 3)
// rotation    encodes the Y-axis rotation
// translation ≈ (7, -2, 4)

CreateShadow — project geometry onto a plane

// Directional light pointing down-left
Vector3 lightDir = Vector3::Normalize(Vector3(-1, -1, 0));

// Ground plane (Y=0, normal pointing up)
Plane ground(Vector3::Up, 0.0f);

Matrix shadow = Matrix::CreateShadow(lightDir, ground);
// Multiply an object's world matrix by shadow to draw its flat projection

CreateReflection — mirror geometry

// Mirror plane: X=0 (the YZ plane)
Plane mirror(Vector3::Right, 0.0f);
Matrix reflection = Matrix::CreateReflection(mirror);

// Typically set as the world matrix of the mirrored scene pass
effect_->setWorld(reflection * objectWorld);

CreateBillboard — always facing the camera

Vector3 objectPosition(3, 0, -2);
Vector3 cameraPosition(0, 5,  5);
Vector3 cameraUp = Vector3::Up;

Matrix billboard = Matrix::CreateBillboard(
    objectPosition,
    cameraPosition,
    cameraUp,
    nullptr // optional camera-forward override
);

// Also available: CreateConstrainedBillboard for axis-locked billboards
Matrix axisLocked = Matrix::CreateConstrainedBillboard(
    objectPosition, cameraPosition,
    Vector3::Up,   // rotate only around this axis
    nullptr, nullptr);

Transform points and normals

Matrix xform = Matrix::CreateRotationY(1.0f)
             * Matrix::CreateTranslation(5, 0, 0);

// Transform a position (w=1): translation IS applied
Vector3 worldPos = Vector3::Transform(Vector3::Zero, xform);

// Transform a direction / normal (w=0): translation is NOT applied
// Use TransformNormal so normals stay perpendicular after scaling
Vector3 localNormal(0, 1, 0);
Vector3 worldNormal = Vector3::TransformNormal(localNormal, xform);
worldNormal = Vector3::Normalize(worldNormal); // renormalize if scaled

Code example: billboard sprite always facing the camera

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

protected:
    void LoadContent() override {
        effect_   = std::make_unique<BasicEffect>(getGraphicsDeviceProperty());
        texture_  = std::make_unique<Texture2D>("assets/particle.png",
                        getGraphicsDeviceProperty());
        effect_->setTextureEnabled(true);
        effect_->setTexture(texture_.get());

        // A simple quad: two triangles, four vertices
        VertexPositionTexture quad[4] = {
            { Vector3(-0.5f,  0.5f, 0.0f), Vector2(0, 0) },
            { Vector3( 0.5f,  0.5f, 0.0f), Vector2(1, 0) },
            { Vector3(-0.5f, -0.5f, 0.0f), Vector2(0, 1) },
            { Vector3( 0.5f, -0.5f, 0.0f), Vector2(1, 1) },
        };
        vb_ = std::make_unique<VertexBuffer>(getGraphicsDeviceProperty(),
            VertexPositionTexture::VertexDeclaration, 4, BufferUsage::None);
        vb_->SetData(quad, 4);

        uint16_t idx[] = { 0,1,2, 1,3,2 };
        ib_ = std::make_unique<IndexBuffer>(getGraphicsDeviceProperty(),
            IndexElementSize::SixteenBits, 6, BufferUsage::None);
        ib_->SetData(idx, 6);
    }

    void Update(GameTime& gameTime) override {
        float t = static_cast<float>(gameTime.TotalGameTime.TotalSeconds());
        // Camera orbits around Y axis
        camPos_ = Vector3(5 * std::cos(t * 0.5f), 2, 5 * std::sin(t * 0.5f));
    }

    void Draw(const GameTime&) override {
        auto& gd = getGraphicsDeviceProperty();
        gd.Clear(Color::CornflowerBlue);

        Matrix view = Matrix::CreateLookAt(camPos_, Vector3::Zero, Vector3::Up);
        Matrix proj = Matrix::CreatePerspectiveFieldOfView(
            MathHelper::PiOver4, 800.0f / 600.0f, 0.1f, 100.0f);

        // Billboard always faces the camera
        Matrix bill = Matrix::CreateBillboard(
            Vector3::Zero, camPos_, Vector3::Up, nullptr);

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

        gd.setVertexBuffer(*vb_);
        gd.setIndexBuffer(*ib_);
        for (auto& pass : effect_->getCurrentTechnique().Passes) {
            pass.Apply();
            gd.DrawIndexedPrimitives(PrimitiveType::TriangleList, 0, 0, 2);
        }
        gd.Present();
    }

private:
    GraphicsDeviceManager               graphics_;
    std::unique_ptr<BasicEffect>        effect_;
    std::unique_ptr<Texture2D>          texture_;
    std::unique_ptr<VertexBuffer>       vb_;
    std::unique_ptr<IndexBuffer>        ib_;
    Vector3                             camPos_;
};