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?