Understanding CFrame with Matrices!

Hello developers!!!

Everyone that programs in Roblox, use CFrame for the position and orientation of the objects in our 3D space, but if we learn about this, we can optimize this and increment the posibilities.
In this post, I’ll teach you about this and increment your scripting skills!

Required knowledge
  • Vectors
  • Radians
  • Trigonometry (sine, cosine)

What’s really a CFrame?

A CFrame is just a matrix 4x4. This matrix has 4 hidden vectors that no one talks:

Right Up Forward Position
Rx Ux Fx Px
Ry Uy Fy Py
Rz Uz Fz Pz
0 0 0 1

The R vector represents a direction, where is looking the right face of the model, the U vector represents where is looking the top face of the model, the Fvector represents where is the forward of the model and the P vector represents the position of the model. That’s fine!
But… what’s about the bottom linea? This line hasn’t a function, just make the matrix multiplicable by others matrixes equals to this as we’ll see later.

We can use CFrame.new(x, y, z, r00, r01, r02, r10, r11, r12, r20, r21, r22) to set the individual numbers to the matrix, but is difficult think the numbers with this way. We can use the function in more lines to solve this:

CFrame.new(
   x, y, z,

   r00, r01, r02,
   r10, r11, r12,
   r20, r21, r22
)

Now we can see better our matrix!
The numbers x, y and z are the axes of the vector position, while r00, r10 and r20 are the axes of the vector with the right direction, r10, r11 and r12 are tha exis of the vector with the up direction and r20, r21 and r22 are the axes of the vector with the forward direction.
That’s all, in this consists this matrix. To make a test, we can try to input different orientation. For example, this is a matrix without orientation:

CFrame.new(
   0, 0, 0,

   1, 0, 0,
   0, 1, 0,
   0, 0, 1
) --> That's equals to CFrame.identity

As you can see, in the vector that define the right direction is looking to the right, the vector that define the up direction of the model is looking up and the vector that define the forward direction is looking to the forward.

We can also get the different components of the CFrame using the function CFrame:GetComponents() like this:

local x, y, z, r00, r01, r02, r10, r11, r12, r20, r21, r22 = cframe:GetComponents()

Making rotations working directly with the matrix (1 axis)

The first step to rotate with a matrix is think in what axis we want rotate and, depends of the axis, put in the matrix some values or others values. Now, we need to visualize an matrix 3x3 with that headers to improve the comprehension:

_ x y z
x
y
z

Now, in the row and colums that correspond with the axis, we need to set 0 and where it intersets, we set a 1. Here we have the results:

In X axis:

_ x y z
x 1 0 0
y 0
z 0

In Y axis:

_ x y z
x 0
y 0 1 0
z 0

In Z axis:

_ x y z
x 0
y 0
z 0 0 1

Now, in the void spaces, we need to set the sine and cosine of the angle that we want to rotate in this order:
cos θ -sin θ
sin θ cos θ

In X axis:

_ x y z
x 1 0 0
y 0 cosθ -sinθ
z 0 sinθ cosθ

In Y axis:

_ x y z
x cosθ 0 sinθ
y 0 1 0
z -sinθ 0 cosθ

In Z axis:

_ x y z
x cosθ -sinθ 0
y sinθ cosθ 0
z 0 0 1

We can use this in our code like this:

local position = script.Parent.Position

local theta = math.pi * .5
local sin, cos = math.sin(theta), math.cos(theta)

script.Parent.CFrame = CFrame.new(
   position.X, position.Y, position.Z,

   1,  0,    0,
   0, cos, -sin,
   0, sin,  cos
)

This rotate the script.Parent 90 degrees in the X axis.
That’s fine but… how we can rotate in more axes? The answer is simple: matrix multiplication.

Making rotations working directly with the matrix (2+ axes)

As I said before, if we want to rotate an object in more than one axis, we need to multiply the matrix of the axes that we want rotate.
The matrix multiplication is confused, but we’ll try to explain that the best way that I can:
The first step is checking that we can multiply this matrix. To know that, we need to check that the columns amount of the first matrix is equals to the rows amount of the second matrix. We are working with matrix 3x3, so, we can multiply that.

To start, we will stay in position in the matrix (1, 1), it means that we need to put in the row number 1 of the first matrix and in the column number 1 of the second matrix, getting 3 numbers per matrix.
Now, we’ll multiply the first number of the row 1 of the first matrix by the first number of the column 1 of the second matrix, the second number of the row by the second number of the column and the third number of the row by the third number of the column and add the results:

To continue, we’ll move to the position (1, 2), repeating the same calculus with de row 1 of the first matrix and the second column of the second matrix:

To end with this tedious work, repeat this proccess with all the positions:

Finally we got the result!
In code looks like this:

local function multiply(matrix1, matrix2)
	local result = {
		{matrix1[1][1] * matrix2[1][2] + matrix1[1][2] * matrix2[2][1] + matrix1[1][3] * matrix2[3][1], matrix1[1][1] * matrix2[1][2] + matrix1[1][2] * matrix2[2][2] + matrix1[1][3] * matrix2[3][2], matrix1[1][1] * matrix2[1][3] + matrix1[1][2] * matrix2[2][3] + matrix1[1][3] * matrix2[3][3]},
		{matrix1[2][1] * matrix2[1][2] + matrix1[2][2] * matrix2[2][1] + matrix1[2][3] * matrix2[3][1], matrix1[2][1] * matrix2[1][2] + matrix1[2][2] * matrix2[2][2] + matrix1[2][3] * matrix2[3][2], matrix1[2][1] * matrix2[1][3] + matrix1[2][2] * matrix2[2][3] + matrix1[2][3] * matrix2[3][3]},
 		{matrix1[3][1] * matrix2[1][2] + matrix1[3][2] * matrix2[2][1] + matrix1[3][3] * matrix2[3][1], matrix1[3][1] * matrix2[1][2] + matrix1[3][2] * matrix2[2][2] + matrix1[3][3] * matrix2[3][2], matrix1[3][1] * matrix2[1][3] + matrix1[3][2] * matrix2[2][3] + matrix1[3][3] * matrix2[3][3]},
	}

	return result
end

The code is really long and a bit stressful to program (almost for me).
We can program this better using Type Checking, OOP and metatables, but for this tutorial it’s great.

CFrame.Angles() function

If you have any experience with the CFrame, it’s probbably that you usually use this function, but if you don’t know what is this, let me explain it:

CFrame.Angles creates a cframe with a rotation. It requires 3 numbers, this numbers are the angle (in radians) that you want to rotate a 3D object in the differents axes.

Okay, so… Why we need to know how works the CFrame to rotate objects if there is a function that it make this for us? The answer is very simple: optimization.
Just think how can be this function inside. I just explained that to rotate an object in more than one axis, we have to multiply the matrices and know how is the matrices. So, we can see the maths with this operation:

As you can see, this function isn’t the most optimized function in roblox. That’s why I taught you that, for make less calculus and make our rotations faster.

CFrame.Angles() script

I recreated the CFrame.Angles function for your comprenssion of the maths inside of this function. That’s the function that we need to make when we need a rntation in the three axes, with innecesary calculus if we need a rotation in one or two axes.

local function multiply(matrix1, matrix2)
	return {
		{matrix1[1][1] * matrix2[1][2] + matrix1[1][2] * matrix2[2][1] + matrix1[1][3] * matrix2[3][1], matrix1[1][1] * matrix2[1][2] + matrix1[1][2] * matrix2[2][2] + matrix1[1][3] * matrix2[3][2], matrix1[1][1] * matrix2[1][3] + matrix1[1][2] * matrix2[2][3] + matrix1[1][3] * matrix2[3][3]},
		{matrix1[2][1] * matrix2[1][2] + matrix1[2][2] * matrix2[2][1] + matrix1[2][3] * matrix2[3][1], matrix1[2][1] * matrix2[1][2] + matrix1[2][2] * matrix2[2][2] + matrix1[2][3] * matrix2[3][2], matrix1[2][1] * matrix2[1][3] + matrix1[2][2] * matrix2[2][3] + matrix1[2][3] * matrix2[3][3]},
		{matrix1[3][1] * matrix2[1][2] + matrix1[3][2] * matrix2[2][1] + matrix1[3][3] * matrix2[3][1], matrix1[3][1] * matrix2[1][2] + matrix1[3][2] * matrix2[2][2] + matrix1[3][3] * matrix2[3][2], matrix1[3][1] * matrix2[1][3] + matrix1[3][2] * matrix2[2][3] + matrix1[3][3] * matrix2[3][3]},
	}
end

function CFrame.Angles(rx: number, ry: number, rz: number): CFrame
	local sinX, cosX = math.sin(rx), math.cos(rx)
	local sinY, cosY = math.sin(ry), math.cos(ry)
	local sinZ, cosZ = math.sin(rz), math.cos(rz)

	local matrixX = {
		{1, 0, 0},
		{0, cosX, -sinX},
		{0, sinX, cosX}
	}
	local matrixY = {
		{cosY, 0, sinY},
		{0, 1, 0},
		{-sinY, 0, cosY}
	}
	local matrixZ = {
		{cosZ, -sinZ, 0},
		{sinZ, cosZ, 0},
		{0, 0, 1}
	}

	local result = multiply(matrixX, matrixY)
	result = multiply(result, matrixZ)

	return CFrame.new(0, 0, 0
		result[1][1], result[1][2], result[1][3],
		result[2][1], result[2][2], result[2][3],
		result[3][1], result[3][2], result[3][3]
	)
end

The function CFrame.fromMatrix()

With this knowledge, this function is really easy to understand. This function need three vectors and an optional vector:

CFrame.fromMatrix(pos: Vector3, vX: Vector3, vY: Vector3, vZ: Vector3?)

The vector “pos” is the position of the CFrame, the vector “vX” is the vector in the first column in the matrix, “vY” is the vector in the second column and “vZ” is the vector in the third column.
If you remenber the first thing that we learned in the post, you would know that vX, vY and vZ are the directions of the right, top and forward faces of a model.

CFrame.fromMatrix() script

The script inside this function is, more or less, this:

function CFrame.fromMatrix(pos: Vector3, vX: Vector3, vY: Vector3, vZ: Vector3?): CFrame
	local vZ = vZ or vX:Cross(vY).Unit

	return CFrame.new(
		pos.X, pos.Y, pos.Z,
		vX.X, vY.X, vZ.X,
		vX.Y, vY.Y, vZ.Y,
		vX.Z, vZ.Z, vZ.Z
	)
end

Lerping

When we interpolate a CFrame, we are interpolating all the numbers in it, getting a function like that:

local function lerp(a, b, t)
	return a + (b - a) * t
end

function CFrame:Lerp(goal: CFrame, t: number): CFrame
	local xO, yO, zO, r00O, r01O, r02O, r10O, r11O, r12O, r20O, r21O, r22O = self:GetComponents()
	local xT, yT, zT, r00T, r01T, r02T, r10T, r11T, r12T, r20T, r21T, r22T = goal:GetComponents()
	
	return CFrame.new(
		lerp(xO, xT, t), lerp(yO, yT, t), lerp(zO, zT, t),
		
		lerp(r00O, r00T, t), lerp(r01O, r01T, t), lerp(r02O, r02T, t),
		lerp(r10O, r10T, t), lerp(r11O, r11T, t), lerp(r12O, r12T, t),
		lerp(r20O, r20T, t), lerp(r21O, r21T, t), lerp(r22O, r22T, t)
	)
end

We can optimize that working with the CFrame directly with the matrix.
Imagine that we just need a interpolation in the X angle, we can remove so many inneccesary calculus. And the same with 2 axes.


Using CFrames we can also see that there’s this function:
CFrame.new(x, y, z, qX, qY, qZ, qW)
This is a quaternion. I made a post explaining what are the quaternions and how to use it in roblox.

And this was all! So, what are you waiting for using matrix instead of CFrame?

49 Likes

So interesting topic. Hope seeing more soon !

2 Likes

Sorry to bump an old topic but while using this great source to brush up on my understanding of CFrame I noticed a mistake with the Y axis (yaw) rotation matrix. The negative should be on the bottom left sin, not the top right.

Also perhaps it might be worth noting that despite the fact that CFrame orientation is in the form (pitch, yaw, roll) that if you are multiplying the rotational matrices to get the rotational component of a CFrame it has to be done in the order yaw x pitch x roll. I doubt that anyone would really bother doing that manually when there’s such a handy function for it but it’s nice to know the inner workings.

3 Likes