Understanding CFrames

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.

1. Basics

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.

Positional matrix:
image

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

Directional Vectors

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.

image

When negating these vectors -, you get the opposite direction.

Example:
image
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”.

Unit 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:
image

Relative to it self:
image

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 0,0,0?

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.
image

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)

image
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,8,0, not 0, 15, 0.

Assume the rotation of the CFrame is 0,0,0, multiplying it with our part’s rotation results in the rotation 0,0,-39.

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)

image
Into

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.

More Information

You may have seen certain vectors exist in CFrames such as XVector, YVector and the ZVector.

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 math.deg(radians).

  • Rotation on CFrames should always be specified in radians!

Basic math operations that can be performed on CFrames:

3. CFrame methods

CFrame:ToWorldSpace(cf)

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 and B. A is positioned at the origin and B is positioned about a few studs off the origin.
image

I want to position A to B and then offset it 5 studs on the Y axis relative to B while A's rotation is also changed to match B's rotation.

workspace.A.CFrame = workspace.B.CFrame:ToWorldSpace(CFrame.new(0,5,0))


See how 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 CFrame.new(0,5,0) relative to B's CFrame.

CFrame:ToObjectSpace(cf)

This method converts cf(which is relative to the world origin) relative to CFrame.
Let’s say I have 2 parts, named part and 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 part.

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().

image

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 CFrame.

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.

Another example:

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.

CFrame:Inverse()
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 

CFrame.lookAt(eye, target)
This method takes eye and target as vectors and returns a CFrame where it is positioned at eye looking at target.

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 x, y and 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 0,0,0.

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
More Information

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 CFrame:ToOrientation().


image

CFrame:GetComponents()
Returns all the components of the whole CFrame. Same as CFrame:components().

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

CFrame:Lerp(goal, value)
Returns a CFrame interpolated between CFrame and the goal. The goal is determined by value. If 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 rightVector, upVector, 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)

Fin!

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!

How good was this tutorial?
  • 1
  • 2
  • 3
  • 4
  • 5

0 voters

Was this tutorial easier to comprehend?
  • Yes
  • No

0 voters

What other tutorials would you like to see?
  • Understanding OOP and making weapons using them
  • Understanding vectors
  • Implementing turret rotation out of CFrames

0 voters

38 Likes

But great tutorial anyway! Really helpful! :+1:

Oh ok then. Thanks for letting me know.

Tutorials is better I think, resources is more like links to stuff u made

I think you made a mistake here, you said back instead of up

2 Likes

Saw that, the typo is fixed now.

1 Like

Really awesome tutorial, explains a lot.

Good Job Bobo. You have made us proud

Amazing tutorial man! However on this

The rule actually is that 1 of their rows or columns must be the same. I am not sure of the 0,0,0,1 but it wouldn’t make much sense to treat the 1 as the scalar. Perhaps maybe it is the scale factor?

To add on this is a tiny bit petty but could you specify the inverse of a cframe is the additive inverse or in other words negative or positive inverse? Bringing each of its components to the power of -1 is what I thought at first when learning cframes

No, the rule is that the two numbers considered to be “inside” when the two orders of the matrices are adjoined must be the same for mulitplication to happen (i.e: going from left to right, the first matrix’s number of columns must be equal to the number of rows in the second matrix. Note that the order of a matrix is represented as “(# of rows) x (# of columns)”)

This also makes it easier to calculate the determinant of the matrix, which is in turned, used to calculate the inverse of the matrix (generally in math, but it’s easier with just the transpose of the rotation matrix since the vectors are orthogonal), since you’d most likely be looking for the row with the most 0’s, which is always the last row.

It is a scalar. The 1 is only there to preserve the identity matrix. An identity matrix is the matrix where all of the numbers in the leading diagonal are 1 and the rest 0. The identity matrix of a cframe (and generally for all 4x4 matrices) is:
image

image

It can be obtained from using a blank cframe, i.e: calling CFrame.new without any arguments: CFrame.new()

In some cases, negating the positional components and transposing the rotation matrix will give you the inverse, but in other cases, negating the positional components won’t give you the correct inverse. These cases that it would work in is when the cframe has default rotation. Mind you that the inverse of a matrix satisfies the equality of A * A^-1 = I, A being a matrix, A^-1 being matrix A’s inverse and I being the identity matrix


All in all, it’s a pretty good and comprehensive tutorial

1 Like

Nice job man! This is really good!
One thing I noticed however, in the picture of the Mouse.Hit lookvector, it’s rotation is actually determined by the camera’s lookvector instead of the character’s lookvector, but other than that it is phenominal!

Mouse.Hit.LookVector is relative to the camera and is based of mouse.UnitRay.

The CFrame of the mouse in 3D space is determined by mouse.UnitRay.

I’m not the best with drawings but in the picture, it is determined by the forward part of the mouse’s CFrame in 3D space so it shows the reader that LookVector is “forward”.

image