CFrames, one of the most intimidating and important concept one has to grasp when writing code related to position and rotation. Today, you shall learn a lot about CFrames; how they work and also with visuals and and hopefully have grasped the concept by the end of this long post.
Note: You should be having a understanding of vectors and must be familiar with Luau.
A CFrame is a data type which consists of positional and rotational matrix. It is proven more useful than
Vector3's since not only it has more methods, it also contains the rotational matrix which can be manipulated unlike
Vector3, which only consists of positional data.
The positional matrix is technically a 4x4, not 3x4. The
0,0,0,1 in the last only exists so you can multiply CFrames together, you cannot multiply a 3x4 matrix by a 3x4 because the rule for multiplying 2 matrixes together is that the number of rows in the first matrix must be the same as the number of columns in the left CFrame. The rotational matrix is a 3x3.
Multiplying CFrame A with B
Multiplying 2 CFrames together, you can see that the number of columns in
A is the same as the number of rows in
B. If they weren’t the same (if
0,0,0,-1 didn’t exist), you couldn’t multiply them both.
Whenever you print a CFrame, it’s whole matrix is printed out or in other words, the positional and the rotational matrix.
print(CFrame.new()) --> 12 numbers [3 positional, 9 rotational] -- output: 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1
The rotational matrix consists of 3 vectors or directional vectors to be precise, look, right and up. Each vector is made up of 3 numbers of course, x, y and z.
The first vector in the matrix is the right vector,
CFrame.new(1,0,0), the second being the up vector,
CFrame.new(0, 1, 0) and the last being the look vector,
CFrame.new(0, 0, -1).
This also means that directional vectors are positioned in a way relative to a part, but are actually relative to the world origin.
LookVector represents the forward part of a CFrame,
RightVector represents the right direction of a CFrame and
UpVector represents the up direction of a CFrame.
When negating these vectors
-, you get the opposite direction.
Same rule applies for other directions, i.e right and up vectors.
Note: To reference look, right and top vectors, I’ll reference them by “directional vectors”.
They are basically unitized vectors or vectors having a length (magnitude) of 1. You may also have heard people usually say that the directional vectors are unitized vectors. This is true because each directional vector has a length of 1. Unit vectors are also known as vectors with directions, hence “directional vectors”.
A representation of a vector and a unitized vector.
They are really useful because unitized vectors allow us to control a vector’s length An example would be ray casting from the player’s humanoid root part to the mouse, but what if we wanted to limit how long the ray can extend to? Well, take a look at this code.
local MAX_LIMIT = 35 local Players = game:GetService("Players") local mouse = Players.LocalPlayer:GetMouse() local character = Players.LocalPlayer.Character or Players.LocalPlayer.CharacterAded:Wait() local direction = (mouse.Hit.LookVector * MAX_LIMIT) local ray = workspace:RayCast(character:WaitForChild("Head").Position, direction) if (ray) then print(ray.Instance) end
I multiply the look vector of the mouse by 35 since it has a length of 1 (remember the directional vectors are unitized vectors). The resultant vector will be the same but extended by 35 studs.
2. CFrame math
At this point, you should know the basics of CFrame and what they are, let’s get into the math when working with them.
What If I were to told you that you can actually create a CFrame out of 12 numbers instead of 3.
local cf = CFrame.new(5, 5, 5, 1, 0, 0, 0, 1, 0, 0, 0, -1)
Cool right? This means we can create a CFrame with different rotational values!
Creating a CFrame only by position results in the rotational matrix to have the vectors as if they were relative to the origin or in other words, the rotation of the CFrame would be
0,0,0. This also means that LookVector isn’t necessarily
0,0,-1, it is dependent on the rotation of the CFrame of course, same rule applies for other directional vectors as well.
Multiplying 2 CFrames is basically matrix multiplication:
The components in the rotational and positional matrix both added and multiplied.
Think of it like this explicitly:
local cf1 = CFrame.new(0,5,0) local cf2 = CFrame.new(0, 3, 0) local result = cf1 * cf2 print(result) --> 0, 8, 0
And here is the difference of positioning of the part relative to the world origin and positioning the part relative to it self.
Relative to world origin:
Relative to it self:
Notice how the first one doesn’t care about the rotation of the part, but the second one cares. Relative positioning.
Have you ever created a new CFrame out of 3 numbers and set a part’s CFrame to that CFrame and saw the rotation of the part was set to
The reason for that is because of the rotational matrix. As I previously stated, when creating a CFrame only by position, the rotational matrix have the default values as if they were relative to the world origin or in other words, the rotation of that CFrame will be
0,0,0. If you don’t want the rotation to be
0,0,0, you would manually have to set the values of the rotational matrix your self or multiply the CFrame.
local part = workspace.Part part.CFrame *= CFrame.new(0, 0, 5)
I’m explaining this theoretically, let’s write some code shall we?
I have a part which is rotated about -39 degrees on the Z axis.
Now, I want to position the part about 5 studs relative to itself. I can simply just multiply it’s CFrame by
CFrame.new(0, 5, 0).
local part = workspace.Part part.CFrame *= CFrame.new(0, 5, 0)
See how it moved relative to it’s position? Well, remember CFrames also contain the rotational matrix? Since I’m performing matrix multiplication on CFrames specifically, it also accounts for the rotation of the CFrame we’re performing the multiplication on, this proves that CFrames are relative to them selves!
CFrame.new(0, 5, 0) will have a rotation of
0,0,0. Also, matrix multiplication is different.
CFrame.new(0, 5,0) * CFrame.new(0, 3, 0) results in
0, 15, 0.
Assume the rotation of the CFrame is
0,0,0, multiplying it with our part’s rotation results in the rotation
All of us know that any number multiplied by 0 is 0, so why didn’t the part’s rotation change back to
0,0,0? Well, as I said, matrix multiplication is internally different.
Think of the arithmetic on rotation of the part being performed like this:
(0,0,-39) * (0,0,0) = 0,0,-39 -- or think of them being added explicitly.
Alright, but what if I added a Vector to a CFrame? The resultant CFrame will be positioned in a way relative to the world origin.
local part = workspace.Part part.CFrame += Vector.new(0, 5, 0)
See how it didn’t care about the rotation of the part but the rotation of the world origin which is defined to be
0,0,0? That is because vectors are relative to
0,0,0. We are just moving the CFrame by 5 studs relative to
0,0,0 without accounting for it’s rotation since vectors don’t contain rotation.
You may have seen certain vectors exist in CFrames such as
YVector and the
They are actually the same as the directional vectors!
CFrame.XVector is the CFrame’s vector of the first column of the rotational matrix, which means that
CFrame.RightVector is the same as
CFrame.XVector since both of them are the first vectors in the rotational matrix.
CFrame.ZVector is the CFrame’s vector of the last column of the rotational matrix, which means that
CFrame.LookVector is the same as
CFrame.ZVector since both of them are the last vectors in the rotational matrix.
CFrame.YVector is the CFrame’s vector of the middle column of the rotational matrix, which means that
CFrame.UpVector is the same as
CFrame.YVector since both of them are the middle vectors in the rotational matrix.
Also, there will be times you may want to calculate directions. The formula for it is simply
(target - eye).Unit * DISTANCE.
Example use case:
local MAX_RAY_LENGTH = 60 local character = --player's character local mouse = -- player's mouse local direction = (mouse.Hit.LookVector - character:WaitForChild("Head").Position).Unit local ray = workspace:RayCast(character.Head.Position, direction * MAX_RAY_LENGTH ) if (ray) then print(ray.Instance) end
Subtracting the mouse’s position from the head’s position is the same as putting the tail of the head’s position on the head of the mouse’s position, but having its direction reversed (negated) and then we unitize our direction and multiply it by 60.
Also, if you want to only get the positional components of a CFrame, you can simply index
.Position which is a property of a CFrame.
local cf = CFrame.new(0, 5, 0) print(cf.Position) --> 0, 5,0 (only 3 numbers, the positional components)
Here are the math operations you SHOULD know when working with CFrames!
For converting from degrees to radians when working with CFrames, use
math.rad(degrees). For the opposite, use
Rotation on CFrames should always be specified in radians!
Basic math operations that can be performed on CFrames:
3. CFrame methods
This method basically returns
CFrame multiplied by
cf. If I wanted to position a part to another part but with a offset, I can simply use this method.
Say I have 2 parts, named
A is positioned at the origin and
B is positioned about a few studs off the origin.
I want to position
B and then offset it 5 studs on the Y axis relative to
A's rotation is also changed to match
workspace.A.CFrame = workspace.B.CFrame:ToWorldSpace(CFrame.new(0,5,0))
A's rotation is exactly
B's rotation? This is because
A's CFrame has changed to
B's CFrame and then it is offsetted by
This method converts
cf(which is relative to the world origin) relative to
Let’s say I have 2 parts, named
AnotherPart respectively. I want to get
part to look relative to
AnotherPart via using body gyro.
Take a look at this code:
local part = workspace.Part local another = workspace.AnotherPart while game:GetService("RunService").Stepped:Wait() do local relativeCF = another.CFrame:ToObjectSpace(part.CFrame) part.BodyGyro.CFrame = part.CFrame:ToObjectSpace(relativeCF) end
Alright so I do get
part's CFrame relative to
AnotherPart and then I convert that relative CFrame relative to
part. I’m first converting
part's CFrame relative to
AnotherPart and capturing it in a variable, then I convert that relative CFrame relative to
To understand why I convert
relativeCF relative to
part, an explanation from the developer hub should make sense:
"To understand object space, imagine you’re a football player. Your team is facing the opponents head on. If you hear your opponent yell “Go left!” That player is certainly referring to a direction relative to their team’s object space. To understand the direction that player is referring to, you would have to translate that direction relative to your player by ToObjectSpace().
CFrame:ToWorldSpace() as a method to translate a direction from object space to world space.
CFrame:ToObjectSpace() as a method to translate a direction relative to the world origin relative to
AnotherPart looking at left relative to it self will make
part look at the right relative to it self because
AnotherPart's left is
part's right, relative rotation.
So I want to check if
B is forward relative to
A, not the world origin. I can simply write a function which can be reuseable as well.
local function partIsForward(part, target) return target.CFrame:ToObjectSpace(part.CFrame).Z >= 1.3 end print(partIsForward(workspace.B, workspace.A)) --> true
It returns a CFrame where
part's origin is relative to
target and checks if the Z axis of the relative position is greater or equal to 1.3 because remember, the Z axis is forward/back. Hope this makes sense.
This method just returns the inverse of the whole CFrame matrix or in other words, it “negates” the whole CFrame matrix
local cf = CFrame.new(0, 5, 0) print(cf:Inverse()) --> 0, -5, -0, 1, 0, 0, 0, 1, 0, 0, 0, 1
This method takes eye and target as vectors and returns a CFrame where it is positioned at
eye looking at
local eye = Vector3.new() local target = Vector3.new(0, 50, 0) local part = Instance.new("Part") part.Anchored = true part.CFrame = CFrame.lookAt(eye, target) part.Parent = workspace
CFrame.new(x, y, z)
This method returns a newly constructed CFrame out of
z. Normally a CFrame is created out of 12 numbers, 3 positional and 6 rotational, but providing only the positional components results the rotational matrix of the CFrame to be the default or in other words, it’s rotation to be
local cf = CFrame.new(0,5,0) -- positional components of cf is 0, 5,0 but the rotational components is the default -- rotational components: 1,0,0, 0,1,0 0,0,-1 local newCF = CFrame.new(0, 5, 0, 0, 0, 1, 0, 1, 0, -1, 0, 0) -- positional components of cf is 0, 5,0 but the rotational components is not the default! -- rotational components: 0, 0, 1, 0, 1, 0, -1, 0, 0
You may notice that you can also use
CFrame.new(eye, target) instead of
CFrame.lookAt(eye, target). However, the first one shouldn’t be used for creating a CFrame looking at
target and the latter replaces
CFrame.new(position, lookAt) constructor which accomplished the same task. However, the latter also allows you to specify the up vector.
CFrame.EulerAnglesXYZ(x, y, z)
This method takes x, y, and z as values in degrees converted to radians and returns a CFrame whose rotation in degrees is x, y and z but applied in z, y and x order. Same as
CFrame.Angles(x, y, z).
local part = Instance.new("Part") part.Anchored = true part.CFrame = CFrame.Angles(0, math.rad(60), math.rad(30)) part.Parent = workspace
CFrame:ToEulerAnglesYXZ(x, y, z)
Works the same as
CFrame.EulerAnglesXYZ(x, y, z) but applies rotation in z, x and y order. Same as
Returns all the components of the whole CFrame. Same as
local x, y, z, r00, r01, r02, r10, r11, r12, r20, r21, r22 = CFrame.new(0, 0, 1):GetComponents() local upVector = Vector3.new(r01, r11, r21) print(upVector) --> 0, 1, 0
Returns a CFrame interpolated between
CFrame and the goal. The goal is determined by
value is 1, then it will return the full value of
goal, else if it is .5 (half), it will return half the value of
goal and etc.
workspace.Part.CFrame = workspace.Part.CFrame:Lerp(CFrame.new(5, 5, 0), .5)
CFrame.fromMatrix(eye, rightVector, upVector, zVector)
This CFrame method takes all it’s arguments as vectors and returns a CFrame positioned at
eye whose rotational matrix consists of
zVector. Pretty straightforward right?
local angle = CFrame.Angles(math.rad(30), math.rad(50), 0) workspace.Part.CFrame = CFrame.fromMatrix(workspace.Part.Position, angle.RightVector, angle.UpVector)
You’ve reached the end of this long post, I’m sure by now you should be familiar with the fundamental concept of CFrames and their main methods. Feel free to ask any questions!
- Understanding OOP and making weapons using them
- Understanding vectors
- Implementing turret rotation out of CFrames