rgb.sh

3D Matrix Transformations

May 4 2020

An interactive journey through the matrix transformations of the rendering pipeline.

At their most basic, transformation matrices just convert coordinates from one space to another. However, while conceptually simple, there are many small details that can make dealing with transformations confusing and frustrating. Anytime I have ever done graphics work, I have ended up incorrectly applying my transformations to create varying amounts of visual garbage. I wrote this post and built the below tool to act as a reference when working with graphics transformations.

Before getting into the details, it’s important to mention a few major points of confusion around the subject:

  1. Coordinate orientation & handedness

    The orientation of a coordinate space dictates how a coordinate (x,y,z) maps to an relative location in 3D space. There are no restrictions on how you decide to orient the axes. That said, it’s typical to have +x point to the right and +y to point up, while forward varies. Generally speaking, Direct3D applications use a left-handed system with +z pointing forward whereas OpenGL uses a right-handed system with z forward. 3D modeling software may use an entirely different convention, such as +z up & +y forward in Blender.

  2. Normalized device coordinates (NDC)

    Normalized device coordinates, or NDC, are essentially the real coordinates from the perspective of the display. Each graphics API defines the orientation and bounds of NDC space differently by default. For example, Vulkan has +y as down and z ranges from 0 at the near side to 1 on the far end, while OpenGL has +y as up and z ranges from -1 to 1. This is not impacted by how you oriented your application’s coordinate system; that instead just decides how you will convert from your coordinates to clip space, which the GPU then converts to NDC. This is somewhat configurable through your choice of graphics API, though.

  3. Row-major vs column-major storage order

    Matrix storage order determines how a linear memory sequence converts to a 2D matrix (from 1D index to 2D index). Matrices can be in either row- or column-major ordering; row-major stores row-by-row from top to bottom, while column major stores each column from left to right.

    Given the linear sequence xmemory, we can see the difference: xmemory=[1234000012340000]

    Xrowmajor=[1234000012340000] Xcolumnmajor=[1010202030304040]

    If you switch the interpretation, it acts as a transpose. Be aware that row-major storage is common on the CPU, while you are likely using column-major by default in your shaders.

  4. Matrix layout & multiplication order

    This decides how elements are arranged in the matrix (from 2D index to element [e.g. x translation]). This is directly tied to which side you will be multiplying your transforms on. OpenGL generally multiplies from the left, while Direct3D multiplies from the right. There is no functional difference, but it is important to know which side the matrices are intended to be multiplied on (which is generally decided by your matrix helper functions and choice of API). You don’t have to worry about column versus row vectors; shaders will interpret a vector in the way needed for a matrix-vector multiply.

  5. GLSL matrix indices

    GLSL matrices use mat[col][row] rather than mat[row][col]. Just be aware of this if working in GLSL.

Interactive Transforms

This tool lets you see walk through all of the coordinate spaces of the rendering pipeline and visualize how you transform between them. I’ve limited the controls at each stage to be representative, not all-inclusive, of the kinds of transforms you’d perform.

    • Model
    • Trans (X)
    • Rot (Y)
    • Scale
    • View
    • Trans (Z)
    • Rot (Y)
    • Projection
    • FoV
Close Controls

You can move the camera to get a better look.

Model / Object Space

This is the space in which the model’s vertices exist, as in a 3D modeling package.

At this point you would also apply animations, if applicable.