Tutorial 42: Matrix Operations and Transforms
What you’ll learn
Multiply,Transpose,InvertandDecompose.CreateShadow,CreateReflectionandCreateBillboard.- Why points and normals transform differently.
Before you start — Tutorial 33: Matrices and Transformations (the construction helpers) and Tutorial 41: Vector2, Vector3, Vector4 Math (vector math).
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 <optional>
#include "Microsoft/Xna/Framework/Matrix.hpp"
#include "Microsoft/Xna/Framework/MathHelper.hpp"
#include "Microsoft/Xna/Framework/Plane.hpp" // Plane and Quaternion are only forward-declared by Matrix.hpp
#include "Microsoft/Xna/Framework/Quaternion.hpp"
using namespace Microsoft::Xna::Framework;
Matrix I = Matrix::getIdentityProperty(); // 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));
Each entry of a product is a four-term dot product. CNA accumulates it in double and narrows to float once when the entry is stored, which is what the original 32-bit XNA runtime does on the x87 unit; a hand-written float loop can differ in the last bit. Never test a composed matrix for equality with == (a rotation multiplied by its own inverse is only close to the identity); compare entries with a small tolerance.
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 = Matrix::Invert(view);
// invView transforms from view space back to world space
// (used for ray casting from screen coordinates)
// The two-argument overload exists too, but it returns void:
// Matrix::Invert(view, invView);
Invert has no success flag and does not throw. As in XNA, a singular matrix simply produces a result full of infinities or NaNs, so test view.Determinant() yourself when the input can be degenerate (for example a matrix with a zero scale).
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);
// ok2 is false when a scale component is (nearly) zero: the rotation cannot be recovered
// scale2 ≈ (2, 1, 3)
// rotation encodes the Y-axis rotation
// translation ≈ (7, -2, 4)
Decompose expects a scale-rotation-translation matrix with positive scales. At this snapshot it follows FNA's algorithm, not XNA 4.0's: a mirrored matrix (a negative scale on one axis) comes back with positive scales and a quaternion that is not a rotation, and a shear is not detected (CNA-BUG-259). Test the sign of the upper 3×3's Determinant() first when the input can be mirrored.
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_->setWorldProperty(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,
std::nullopt // optional camera-forward override
);
// Also available: CreateConstrainedBillboard for axis-locked billboards
Matrix axisLocked = Matrix::CreateConstrainedBillboard(
objectPosition, cameraPosition,
Vector3::Up, // rotate only around this axis
std::nullopt, std::nullopt);
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_.setPreferredBackBufferWidthProperty(800);
graphics_.setPreferredBackBufferHeightProperty(600);
}
protected:
void LoadContent() override {
effect_ = std::make_unique<BasicEffect>(getGraphicsDeviceProperty());
// CNAEXT convenience constructor that reads an image file directly. The XNA route is
// getContentProperty().Load<Texture2D>("particle") (returns the texture by value).
texture_ = std::make_unique<Texture2D>("assets/particle.png",
getGraphicsDeviceProperty());
effect_->setTextureEnabledProperty(true);
effect_->setTextureProperty(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::getVertexDeclarationStatic(), 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.getTotalGameTimeProperty().getTotalSecondsProperty());
// 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, std::nullopt);
effect_->setWorldProperty(bill);
effect_->setViewProperty(view);
effect_->setProjectionProperty(proj);
gd.SetVertexBuffer(vb_.get());
gd.setIndicesProperty(ib_.get());
for (auto& pass : effect_->getCurrentTechniqueProperty()->getPassesProperty()) {
pass.Apply();
gd.DrawIndexedPrimitives(PrimitiveType::TriangleList, 0, 0, 4, 0, 2);
}
// (Game presents the frame after Draw() returns; no gd.Present() needed.)
}
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_;
};
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Vector, Matrix and MathHelper numerics: interpolation, clamping and degenerate inputs — What CNA's vector, matrix and MathHelper functions return for out-of-range amounts, inverted clamps, NaN, zero vectors, singular matrices and bad camera input, compared with XNA 4.0, plus the precision and test evidence.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-259: Matrix::Decompose follows FNA's algorithm, not XNA 4.0's: a mirrored matrix decomposes to positive scales and a non-unit quaternion, and a sheared matrix reports success — Matrix::Decompose never checks the determinant: a mirrored matrix returns positive scales, a non-unit quaternion and true where XNA 4.0 negates the largest scale, and a sheared matrix returns true where XNA returns false