Tutorial 33: Matrices and Transformations
What you’ll learn
- What the world, view and projection matrices each do.
CreateTranslation,CreateRotation*,CreateScale,CreateLookAtandCreatePerspectiveFieldOfView.- Why multiplication order matters, and the usual scale-rotate-translate convention.
- Undoing a transform with
Matrix::Invert.
Before you start — Tutorial 31: Your First 3D Triangle — you have already set these three matrices; this explains them. Requires a 3D-capable renderer such as OPENGLES3 or VULKAN; the 2D-only renderer (SDL_RENDERER) throws on 3D calls by default, and STUB reports no 3D capability at all.
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; after the perspective divide x and y lie in [-1, 1] and depth in [0, 1] (the Direct3D/XNA convention).
The full transform applied to each vertex is:
// XNA and CNA use ROW vectors, so the vertex is on the left and the matrices
// are applied left to right: World first, then View, then Projection.
clip_pos = vertex_pos * World * View * Projection;
Beware of OpenGL and GLSL texts that write the same chain as Projection * View * World * vertex: that is the column-vector spelling of the same transform, and it is not valid here. 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, equivalently, using a Vector3
Matrix world2 = 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 is far too large a rotation: CreateRotationY(45.0f) rotates by 45 radians, about 57× more than the 45° you meant.
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.
// Rotate first, then move to the camera position (row-vector order, like TRS above)
Matrix cameraWorld = Matrix::CreateRotationY(yaw)
* Matrix::CreateTranslation(cameraPos);
// View matrix = inverse of camera's world matrix
Matrix view = Matrix::Invert(cameraWorld);
// Output-parameter overload: returns void, exactly as in XNA
Matrix result;
Matrix::Invert(someMatrix, result);
Neither overload reports failure. There is no "singular matrix" return value, so if the input may be degenerate (a scale of zero, for example) test someMatrix.Determinant() yourself before inverting.
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/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_.setPreferredBackBufferWidthProperty(800);
graphics_.setPreferredBackBufferHeightProperty(600);
}
protected:
void LoadContent() override {
auto& gd = getGraphicsDeviceProperty();
effect_ = std::make_unique<BasicEffect>(gd);
effect_->VertexColorEnabled = true;
// Sun — yellow
BuildCube(sunVerts_, Color::Yellow);
sunVB_ = std::make_unique<VertexBuffer>(gd,
VertexPositionColor::getVertexDeclarationStatic(),
(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::getVertexDeclarationStatic(),
(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::getVertexDeclarationStatic(),
(int)moonVerts_.size(), BufferUsage::None);
moonVB_->SetData(moonVerts_.data(), (int)moonVerts_.size());
}
void Update(GameTime& gameTime) override {
float dt = (float)gameTime.getElapsedGameTimeProperty().getTotalSecondsProperty();
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_->setViewProperty(view);
effect_->setProjectionProperty(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 a child of Earth: local placement → orbit around the earth's centre
// → (below) the earth's translation and the earth's orbit around the sun
// Row-vector order: orbit around the earth's centre first, then move to the
// earth's offset from the sun, then apply the earth's own orbit around the sun.
Matrix moonWorld = moonLocal * moonOrbitMat
* Matrix::CreateTranslation(6.0f, 0.0f, 0.0f)
* earthOrbitMat;
DrawMesh(*moonVB_, moonWorld);
// No gd.Present(): Game presents in EndDraw, after Draw() returns.
}
private:
void DrawMesh(VertexBuffer& vb, const Matrix& world) {
auto& gd = getGraphicsDeviceProperty();
effect_->setWorldProperty(world);
gd.SetVertexBuffer(&vb);
for (auto& pass : effect_->getCurrentTechniqueProperty()->getPassesProperty()) {
pass.Apply();
gd.DrawPrimitives(PrimitiveType::TriangleList, 0,
vb.getVertexCountProperty() / 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
- Tutorial 34: 3D Camera Setup and Control — build a flyable FPS camera using these same matrices.
- Tutorial 32: BasicEffect and 3D Lighting — add lights to your transformed objects.
- Math Types reference — full Matrix API.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Coordinate and composition conventions: handedness, row vectors, depth and clip space — CNA's right-handed basis, row-vector matrices applied in reading order, quaternion products that run the other way, the [0,1] depth range derived three times, clip-space W and the ToColumnMajor bridge.