Basics, part 9

One 4x4 matrix moves the whole mesh

In 3D, three columns of a matrix turn and stretch a mesh but can't move it. A fourth column holds the position, and that 4 by 4 matrix tells a shader where your mesh sits.

  • Part 97 min dose
  • Beginner

In the matrices lesson, a 2 by 2 matrix turned, stretched and slanted the F. It can’t move it anywhere, though, because (0, 0) always stays put. Your mesh in Unity lives in 3D, and a 3 by 3 matrix has the same problem. Its three columns say where the x, y and z arrows land, so it can turn and stretch the mesh, but the middle never moves.

So Unity adds a fourth column that holds the position, and gives every point a fourth number, ww, which switches that position on (1) or off (0). That 4 by 4 matrix is how Unity tells a shader where your mesh sits. I’ll show the trick in 2D first, where the extra column fits on a flat picture, and then go to 3D.

Drag the F
Drag the F's corner, or press Tab to reach it and use the arrow keys

Turn on JavaScript to run this demo.

How far the F is turned, in degrees.30
The arrow's third number. 1 makes it a point that moves with the F, 0 a direction that only turns.
lib.hlsl and move.hlsl draw the demo above
Fig. 1

Drag the F by its corner, and the mint arrow rides along to its top corner. Then set w to Direction. Now the arrow stays put however far you drag, and it only follows Turn.

One more number

A matrix times (0, 0) is 0 of its first column plus 0 of its second, which is (0, 0) again. No numbers in the matrix can change that.

So let’s give every 2D point a third number, ww, and set it to 1. A 3 by 3 matrix has a third column, tt, and the point gets one whole tt added on:

M=[ixjxtxiyjyty001]M[xyw]=x i+y j+w t\begin{aligned} M &= \begin{bmatrix} \textcolor{#10916c}{i_x} & \textcolor{#d9468c}{j_x} & t_x \\ \textcolor{#10916c}{i_y} & \textcolor{#d9468c}{j_y} & t_y \\ \textcolor{#10916c}{0} & \textcolor{#d9468c}{0} & 1 \end{bmatrix} \\ M \begin{bmatrix} x \\ y \\ w \end{bmatrix} &= x\,\textcolor{#10916c}{i} + y\,\textcolor{#d9468c}{j} + w\,t \end{aligned}

ii and jj are the mint and pink columns from the matrices lesson, still where the two arrows land. tt is new, and it’s the move. The bottom row, (0, 0, 1), hands ww back as it was. In the demo above, tt is wherever you drag the F’s corner. This extra number is what the books below mean by homogeneous.

Points move, directions don’t

Now set ww to 0. Then w tw\,t is 0 too, so the move drops out and only the turn and stretch are left. That suits a direction, like the way a surface faces. It should turn with the mesh but never move. So points get w=1{w = 1} and directions w=0{w = 0}, like the w switch in the demo.

Mix them up and it bites. Put a point through the matrix as float4(p, 0.0), and the mesh’s position quietly drops out.

Unity’s own UnityObjectToWorldDir takes another road. It multiplies by (float3x3)unity_ObjectToWorld, the top left 3 by 3 corner of Unity’s 4 by 4 matrix, which leaves the position out. Then it normalizes the answer to length 1.

Normals, the arrows pointing straight out of a surface, are the exception. When a mesh is stretched more one way than another, they need the inverse transpose. UnityObjectToWorldNormal gets it with mul(norm, (float3x3)unity_WorldToObject), the row trick from the matrices lesson, and a later lesson on light explains why.

Three arrows and a position

In 3D, points get a fourth number, and Unity’s unity_ObjectToWorld is 4 by 4. The Unity Manual calls it the model matrix, and UNITY_MATRIX_M is another name for it. Its first three columns are where the mesh’s own x, y and z arrows land in the world, and the fourth is the position, the tt from before.

Say a mesh with no parent has Position (2, 0, 1), Rotation Y = 90 and Scale (1, 1, 1) in its Transform. The turn sends x to (0, 0, -1) and z to (1, 0, 0), and y stays. The position moves all three, and the point (1, 1, 0) with w=1{w = 1} lands on (2, 1, 0).

Rotation Y = 90 as a matrix

4: (1, 1, 0) with w = 1 lands on (2, 1, 0).

x → (0, 0, -1)y → (0, 1, 0)z → (1, 0, 0)
position (2, 0, 1)
_m02 = 1_m20 = -1
mul(M, (1, 1, 0, 1)) = (2, 1, 0, 1)

Here’s the whole matrix, and the point going through it:

M=[00120100−10010001]M[1101]=[2101]\begin{aligned} M &= \begin{bmatrix} \textcolor{#10916c}{0} & \textcolor{#d9468c}{0} & \textcolor{#9c7a1e}{1} & 2 \\ \textcolor{#10916c}{0} & \textcolor{#d9468c}{1} & \textcolor{#9c7a1e}{0} & 0 \\ \textcolor{#10916c}{-1} & \textcolor{#d9468c}{0} & \textcolor{#9c7a1e}{0} & 1 \\ \textcolor{#10916c}{0} & \textcolor{#d9468c}{0} & \textcolor{#9c7a1e}{0} & 1 \end{bmatrix} \\ M \begin{bmatrix} 1 \\ 1 \\ 0 \\ 1 \end{bmatrix} &= \begin{bmatrix} 2 \\ 1 \\ 0 \\ 1 \end{bmatrix} \end{aligned}

_m02, row 0 and column 2 counted from 0, is the 1 at the top of the butter column, and a shader reads it as unity_ObjectToWorld._m02. _m20 is the -1 in the mint one. The point takes 1 of mint, 1 of pink, none of butter and the position once. Pick direction in the figure, and the position drops out, leaving (0, 1, -1).

Read it backwards

Unity builds that matrix from the Transform’s three parts in a fixed order. A point is scaled first, then turned, then moved. As matrices that’s mul(T, mul(R, S)), where S scales, R turns and T moves. Like mul(A, B) in the matrices lesson, it reads from right to left.

The order matters, because a turn always swings around the origin. Take this one slowly. Here the F starts with its corner on the origin. Scaling first makes it twice as wide where it stands, the turn swings it 90 degrees around the origin, and the move takes its corner to (2, 1), the matrix’s last column.

Scale, turn and move

4: Then move by (2, 1), and the corner lands on (2, 1).

scale (2, 1) turn 90° move (2, 1)
mul(T, mul(R, S)) = float3x3(0, -1, 2, 2, 0, 1, 0, 0, 1)
last column: (2, 1)

Switch the figure to move, turn, scale, and the turn swings the moved F around the origin like a door. Its corner ends up on (-2, 2).

Same point, two sets of numbers

Every corner of a mesh has two sets of numbers. Its object numbers say where it sits on the mesh’s own grid, counted from its pivot. Its world numbers say where it sits in the scene. Both name the same point, and I think of them as two addresses for one house. unity_ObjectToWorld takes object numbers to world numbers, and unity_WorldToObject brings them back. In the demo, the F with its mint grid plays the mesh.

One dot, two sets of numbers
Drag the dot, or press Tab to reach it and use the arrow keys

Turn on JavaScript to run this demo.

How far the object is turned, in degrees.90
How big the object is.1.00
two-names.hlsl draws the demo above
Fig. 2

Drag the dot, and both pairs of numbers change. Then move Turn or Scale. The dot stays where it is in the world, so only its mint object numbers change.

In practice

Here’s the inside of a vertex shader that uses all of it. The first line is the one from the Unity shader file lesson, and the others carry the corner, the mesh’s own up and its normal into the world:

o.pos = UnityObjectToClipPos(v.vertex);
o.worldPos = mul(unity_ObjectToWorld, float4(v.vertex.xyz, 1.0)).xyz;
o.worldUp = UnityObjectToWorldDir(float3(0.0, 1.0, 0.0)); // the mesh's own up, in the world
o.worldNormal = UnityObjectToWorldNormal(v.normal);

There’s no magic in UnityObjectToClipPos. Unity’s UnityShaderUtilities.cginc writes it as mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, float4(pos, 1.0))), into the world first and then onto the screen (a 360 degree stereo mode adds a nudge in between). Here it is in two steps:

float4 world = mul(unity_ObjectToWorld, float4(v.vertex.xyz, 1.0));
float4 clipPos = mul(UNITY_MATRIX_VP, world); // what UnityObjectToClipPos does

Two things trip people up. The position is the fourth column, so you read it with _m03_m13_m23:

float3 origin = unity_ObjectToWorld._m03_m13_m23; // where the mesh's pivot is in the world

unity_ObjectToWorld[3] would give you the bottom row, (0, 0, 0, 1), and not the position.

And HLSL has no inverse(). Microsoft’s list of HLSL functions has transpose and determinant, but nothing that undoes a matrix. Unity provides unity_WorldToObject, the inverse of unity_ObjectToWorld. It takes a world point back to the mesh’s own numbers:

float3 objectPos = mul(unity_WorldToObject, float4(worldPos, 1.0)).xyz;

Takeaways

  • A point carries w=1{w = 1}, so a matrix’s last column moves it. A direction carries w=0{w = 0}, so it turns and stretches but never moves.
  • The first three columns of unity_ObjectToWorld are where the mesh’s x, y and z land in the world, and the fourth is its position.
  • Unity’s matrix scales, then turns, then moves, and unity_WorldToObject takes world numbers back to the mesh’s own.

Read more

Search the lessons

Type any word from the lessons, like normalize or smoothstep. The start of a word is enough.