“The space between not knowing something and knowing something is really painful. When you’re in the space [in-between], it’s really easy to want to exit something. People think that resilience and success come from getting to knowing as soon as possible. They’re wrong. Resilience and success come from the length of time you can just tolerate being in-between.”
-Dr. Becky Kennedy
Coordinate frames are one of the most notoriously difficult topics (if not the most) in ROBLOX programming. It is easy to know a little but difficult to become proficient. It is easy to get lost in the in-between and especially easy to want to quit.
Do not feel down on yourself if you don’t understand everything after your first read. Ask for help. Find other resources online. That being said, grab a coffee (or whatever powers your imagination™ ) and get comfortable. This is a marathon, not a sprint.
By the end of this article, you will know all of the fundamental mathematical operations that can be performed involving CFrame
objects. I commit to providing an intuitive explanation for each step along the way. We will develop ways of breaking down problems graphically that will allow us to code sophisticated systems that would otherwise be a headache.
1 | Frames of Reference
Throughout this article I will make frequent mention of the “reference frame” of a vector. This term is not regularly used in programming and comes from my background in physics. This way of thinking may be new and uncomfortable to many. Rest assured, this approach will make CFrame
operations much easier down the road.
At its core, a reference frame is a view in 3D space that we use to assign positions to things. Look around you now and find something in the room you are currently in. Where is the object located? Is it to your right? Left? How far away is it? Now stand up and move to the other side of the room (Or just imagine. After all, Einstein did not revolutionize our understanding of the world by riding a beam of light. This laziness is so dogmatic that the Germans invented a name for it: the gedankenexperiment.) Where is the object now? It is on the other side of the room compared to where you were, despite having just been right next to you.
In other words, the position you assign to an object depends on your perspective. I know I’m making a big deal out of this, but you must understand this before moving on. In the next section, we will discuss what this looks like for vector objects.
2 | Vectors
A vector can be defined in terms of components. In math, we can write these a vector as a collection of three numbers, called components:
We are missing something important, however. Can you spot it? We have no reference frame! This is important because, as we saw in the previous section, you will ascribe different position vectors to the same object depending on your reference frame. To avoid this confusion, we can assign labels to our reference frames. I will adopt the convention of naming these frames single letters, like A or B. For example, if I wanted to indicate that a particular vector object is being written from the perspective of frame A, I would write:
This is exclusively to make our lives easier. Every time we write down a vector, we must ask ourselves: What reference frame am I using? “What about properties like position?” you may ask. These vectors are expressed in a special reference frame called world space. The world space frame is defined by the ROBLOX engine to provide a consistent convention for just these properties. Other than this, world space is identical to every other reference frame.
3 | Transformations between Reference Frames
A majority of present guides fall short of explaining the intuition of coordinate frames. At its core, coordinate frames are a way of transforming between reference frames. I know that sounds abstract. Let me show you what I mean.
If you supply a CFrame
with a vector defined in the input frame, it will give you a transformed vector expressed in the output frame. Note that we have labeled each vector with its reference frame and that a CFrame
object-- which we are defining as a transformation between reference frames-- will take in a vector from one frame and give you a vector in another.
Familiarize yourself with this way of representing these transformations. It may seem tedious now, but becomes incredibly useful for mapping out your thoughts in complex problems.
In the next section we will see how to perform these transformations mathematically and in code.
3.1 | Vector Transformations
At the beginning of the previous section, I asserted that the CFrame
object allows us to transform vector representations between reference frames. Let’s make this more concrete. This transformation can be written mathematically as:
Don’t worry if this doesn’t make sense to you just yet. Let’s go over it carefully. The first term, , refers to the
CFrame
object that defines the transformation from frame A to world space, O. The term refers to a
Vector3
object in frame A. This could be the (relative) position of a part, for example. The output of this operation gives the same underlying vector that we fed in, but expressed in world space as opposed to frame A (it is identical to CFrame:ToWorldSpace()
). Though it may seem pedantic to include both the input and output reference frames in the subscript of the CFrame
, it will become important to our discussion on more complex operations.
The ordering of the terms in this equation is important. The underlying mathematical objects representing CFrame
objects make it such that they typically do not commute when they appear in an equation. This will become obvious during our optional discussion on linear algebra. The general order of terms, however, will remain consistent. The first term will be your transformation, the second will be the object being transformed (typically a Vector3
) where the object is expressed in the appropriate “input” reference frame.
It is worth noting that when you read the CFrame
property of a Part
, what you are getting is the transformation from local space to world space, as written above. Not the other way around. What if we wanted to go in the opposite direction…?
3.2 | Importance of CFrame:Inverse()
Let’s say that instead of wanting to transform from frame A to world space, we wanted to take a vector representation in world space and transform it to A. How would we do this using only the CFrame
associated with A → O? For this we introduce the inverse method, and it does exactly as it says. Explicitly, it takes a transformation and flips its input and output reference frames:
where A and B are arbitrary reference frames. This means to transform a vector from world space into local space, we can write:
This transformation is of the same form as we saw in the previous subsection, only we do not necessarily have an explicit form for that can be read as a property. The transformation
CFrame
is explicitly calculated using the :Inverse()
method. This equation is equivalent to using the CFrame:ToObjectSpace()
method.
3.3 | Object Reference Frames
Before we talk about more complicated operations with reference frame transformations, I want to make sure we are all on equal footing when it comes to the CFrame
property of an object in ROBLOX. So far, we have talked about how CFrame
objects are just a way of transforming vector objects between reference frames. If this is the case, then what does it mean to talk about the CFrame
of a part?
As I alluded to in the final paragraph of Section 3.1, when we read the CFrame
property of an object, what we are getting is the transformation from that object’s reference frame to world space. Let’s see what this looks like.
These are two different perspectives of the same cube. The arrows represent the coordinates axes, with the red, green, and blue axes pointing in the positive direction of the x-, y-, and z-components, respectively. I have enabled the orientation indicator feature to show that the LookVector
actually points in the negative z-direction.
One way we can talk about this is by visualizing the input reference frame of the CFrame
, which is created based on the part’s position and orientation. This is what is commonly referred to as the part’s local space. That little blue orb indicates the direction the cube is facing.
Before we move on, I want elaborate on how we can describe a part’s CFrame
as a coordinate transformation. Essentially, the CFrame
property of an object tells the game how we get from our object’s local space back to world space. This means if we feed the zero vector (Vector3.zero
) into the CFrame
property of the part, we would get the part’s position in world space. The part is located at (0, 0, 0) in its own local space and CFrame
tells us how to get back to the world space coordinate system.
3.4 | Composition of Transformations
Now that we know how to transform between two reference frames using CFrame
objects, we can ask “How do we chain these operations together?” This is called composition, and it falls into place naturally if you consider each step of the expression as a transformation between input and output reference frames. Let’s take a look.
Let’s say you want to create a moving platform without using ROBLOX physics (this example is for demonstration purposes only… please don’t do this.) You would begin by creating a platform and a function to rotate the platform:
local RunService = game:GetService("RunService")
local platform = game.Workspace.Platform
RunService:BindToRenderStep(
"RotatePlatform",
Enum.RenderPriority.Camera.Value - 50,
function(deltaTime: number)
platform.CFrame *= CFrame.fromEulerAnglesXYZ(0, deltaTime, 0)
end
)
As seen above, the platform rotates under the character, but the character does not follow the platform’s motion. We can fix this by updating the position of the character every frame. How should we go about doing this? We know that we want to maintain the relative position between the character and the platform. In terms of reference frames, we want the position of the character to be constant in the platform’s reference frame. Let’s begin by calculating the character’s position relative to the platform:
local character = Players.LocalPlayer.Character
local characterOffset: Vector3 = platform.CFrame:Inverse() * character.PrimaryPart.Position
Carefully study this definition of characterOffset
. If the expression is confusing, I encourage you to revisit Section 3.1 and 3.2 for a refresher on how these operations work. This offset variable is the offset from the platform to the character’s position. With this information, we can update the character’s position after we rotate the platform:
local character = players.LocalPlayer.Character
local characterOffset: Vector3 = platform.CFrame:Inverse() * character:GetPivot().Position
-- Rotate the platform
platform.CFrame *= CFrame.fromEulerAnglesXYZ(0, deltaTime, 0)
-- Update our position to match the previously calculated offset
local newCharacterCFrame = CFrame.new(platform.CFrame * characterOffset)
character:PivotTo(newCharacterCFrame)
By updating their position, the character will remain at a constant spot on the platform. Let’s see what this looks like.
This is better than what we started with, but we can do better. Since we are only working with positions, information about the character’s relative orientation is lost every time we update their position. How can we solve this? You’ve probably guessed the answer already: CFrame composition!
We started by calculating the character’s position relative to the platform, but this does not contain any information about the character’s orientation. If we want to keep this information, we can work with the character’s CFrame
, which we saw stores information on both position and orientation. This begs the question: How can we transform this information between reference frames? Let’s start with something we already know how to do. Instead of computing the character’s offset relative to the platform-- a vector-- let’s compute their CFrame
relative to that of the platform. I know this is a bit of a leap, so let me explain. In Section 3.3 we saw that the CFrame
property of a part contains information on that part’s position and orientation. Just as we can transform a vector between reference frames using a CFrame
, we can do the same thing to transform CFrame
objects themselves between reference frames!
Let’s map this out. We will start by calculating the relative CFrame
of the character in the reference frame of the platform:
A comparison of how we calculate the position of the character relative to the platform for
Vector3
and CFrame
objects. In the first case, we get the Vector3
position of the character relative to the platform. In the second case, we get the CFrame
transformation from the character’s reference frame to the plate’s reference frame. CHR is the character’s local space, PLT is the plate’s local space, and O is world space.
We can confirm that these two objects represent the same position by checking what the result would be if we fed Vector3.zero
into the bottom right CFrame
object. What we find is consistent with the position of the character relative to the plate: the origin of the character frame transformed into the plate frame is just the position of the character relative to the plate. Revisit the final paragraph of Section 3.3 for the intuition if you find this confusing.
This is the first time we are seeing that we can create new transformations by taking the product of two CFrame
objects. I encourage you to closely study the target input and output frames in the bottom operation to make sure you agree with the output. If you find this confusing, try feeding a vector in the character’s frame into the transformation. Follow the left-most transformation and then continue to the middle transformation from there: CHR → O → PLT to give us the net transformation CHR → PLT as expected.
Now that we determined the character’s relative positioning, we can rotate the platform and then update the player’s position to maintain their relative location on the platform. To do this, we calculate the character’s position (and orientation) in world space using the new orientation of the platform. This is as easy as applying the platform’s CFrame
as the transformation:
Reversing the calculation we did above to determine the character’s new
CFrame
. Here I have used prime ( ’ ) to indicate that the platform’s CFrame
has changed after the rotation. Similarly, CHR’ is used to indicate that the character’s CFrame
has also been updated.
Now that we have the character’s new CFrame
, we can update their location and it will appear that they stay at the same location on the platform. Putting all of our previous results together, we have:
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local platform = game.Workspace:WaitForChild("Platform")
RunService:BindToRenderStep(
"RotatePlatform",
Enum.RenderPriority.Camera.Value - 50,
function(deltaTime: number)
local character = Players.LocalPlayer.Character
if not character or not character.PrimaryPart then return end
local characterOffset = platform.CFrame:Inverse() * character:GetPivot()
-- Rotate the platform
platform.CFrame *= CFrame.fromEulerAnglesXYZ(0, deltaTime, 0)
-- Update our position to match the previously calculated offset
local characterNewCFrame = platform.CFrame * characterOffset
character:PivotTo(characterNewCFrame)
end
)
In-game, we have:
you spin me right round
Now our character maintains the same relative orientation throughout the platform’s motion!
Generally, problems involving CFrame
composition will be difficult to wrap your head around. I suggest drawing things out graphically as I did here, carefully mapping out the input and output reference frames and converting those equations into code. If it’s easier for you, start by working with only relative position (Vector3
) objects, since these operations are more intuitive. Then, switch vector representations for CFrame
representations to include orientation.
4 | Linear Algebra (Optional)
4.1 | Vectors
As we discussed before, we can write vectors as a collection of three components. The only difference here is a matter of representation. We write these components as what’s called a column vector:
where a, b, and c are the x-, y-, and z-components of the vector. As before, when we start talking about transforming vectors between reference frames we include labels for bookkeeping purposes:
Where O is the label of our reference frame. As you know, we can multiply vectors by numbers to obtain a scaled vector:
This operation will be especially important during our discussion on matrix multiplication.
4.2 | Reference Frame Transformations as Matrix Multiplication
In the beginning of Section 3.1, we saw how we can transform a vector object to a new reference frame using a CFrame
:
How could we mathematically represent such a transformation numerically? For this we introduce something entirely new: the matrix. A matrix is just a way to organize numbers into rows and columns. In this context, these numbers represent something about the underlying coordinate system. Let’s begin with an example. Let’s say I wanted to rotate an object 90 degrees clockwise about the z-axis.
Let’s try transforming an arbitrary vector, call it v, to this new reference frame using matrix multiplication:
Here I have written out the multiplication as a sum of the columns of the matrix scaled by the associated vector component. For example, the first column of the matrix is scaled by the first component (the x-component) of the vector we are transforming. This operation is repeated for all of the column vectors and these products are summed to obtain the transformed vector:
Inspect these components closely. This is exactly what we would expect for a rotation around the z-axis! In the next section we will examine how we can express CFrame
transformations-- that is, a transformation consisting of a translation and rotation in 3D space-- using the mathematical formalism of matrices.
4.3 | Coordinate Transformations as Matrices
A matrix (plural matrices) is a mathematical object that describes a coordinate transformation. We write these objects as a collection of numbers in rows and columns. As you probably suspected, since CFrame
objects are coordinate transformations, we can represent them as a matrix! These transformations are represented as 4x4 matrices:
I know this looks scary, but we can break it down into smaller parts. Remember, at their core, a CFrame
stores a position and an orientation. The column on the far-right looks exactly like the column vector in the previous section! This column represents the position component of the CFrame
. That leaves the orientation, which, as you probably guessed, is represented by the remainder of the matrix.
We can organize these components as their own matrix. By removing the position component, we obtain what is called the rotation matrix. This is because the remaining information encodes the orientation of the CFrame
:
This matrix itself is just a collection of three column vectors! Look back at the representation of a part’s CFrame
I gave in Section 3.3, these three vectors are just the orientation of the coordinates axes in world space! Let’s rewrite this matrix as an array of these three vectors:
In other words, the rotation matrix is a collection of three vectors, each representing one axis of the new coordinate system. This should remind you of the multiplication involving the columns of a matrix we saw in the previous section.
Let’s try to apply this transformation using the matrix multiplication we discussed:
I encourage you to examine these components carefully. If you find this confusing, go back and review the multiplication between a matrix and vector that we discussed in the previous section. I’ll expand it for you just so you can get a handle for how this would look for a CFrame
:
4.4 | Further Reading
Linear algebra is an expansive topic and the subject of entire college courses. The topics we have discussed here are only the first layer of understanding. If you are interested in linear algebra (or are having trouble with how I have presented things here), I would encourage you to check out the series “The Essence of Linear Algebra” by the YouTuber 3Blue1Brown. Chapters 3 through 5 are extremely informative and supplement what we have talked about here perfectly. Having seen these topics in the context of game development, you gain a whole new appreciation for the power of this seemingly abstract math.
Others have covered the methods you can use with CFrame
objects more extensively. Things like constructors or interpolation. Topics like spherical interpolation invoke even more abstract math (quaternions, Rodrigues’ rotation, etc), however, being armed with the knowledge of how these transformations can be represented as matrices is a good starting point. For topics like these I would highly recommend EgoMoose’s tutorials. He’s a good writer and does a great job at explaining things for people who don’t necessarily have an advanced background in math:
I hope this way of understanding CFrame
objects has been helpful for you. If there are more advanced topics you want me to cover, I am open to suggestions! I had a lot of fun writing this and I hope to create more like this in the future.