PcoWSkbVqDnWTu_dm2ix
We use cookies on this site to enhance your user experience

CFrame Math Operations

CFrame Math Operations

Nov 30 2018, 12:32 PM PST 15 min

Components of a CFrame

A CFrame is made up of 12 separate numbers, we call these components. We can simply find out what these numbers are by calling the CFrame:components() method which returns said numbers.

local x, y, z, m11, m12, m13, m21, m22, m23, m31, m32, m33 = cf:components()

We can also input these 12 numbers directly when defining a CFrame.

local cf = CFrame.new(x, y, z, m11, m12, m13, m21, m22, m23, m31, m32, m33)

The first three of the 12 numbers are the x, y, and z components of the CFrame, in other words the position. The rest of the numbers make up the rotation aspect of the CFrame. These numbers may look daunting, but if we organize them a bit differently we can see that the columns represents the rightVector, upVector, and negative lookVector respectively.

local cf = CFrame.new(0, 0, 0)
local x, y, z, m11, m12, m13, m21, m22, m23, m31, m32, m33 = cf:components()

-- m11, m12, m13,
-- m21, m22, m23,
-- m31, m32, m33

local right = Vector3.new(m11, m21, m31) -- This is the same as cf.rightVector
local up = Vector3.new(m12, m22, m32) -- This is the same as cf.upVector
local back = Vector3.new(m13, m23, m33) -- This is the same as -cf.lookVector

Having these vectors to visualize helps us see what the rotation numbers of our CFrame are actually doing. We can see that they represent three orthogonal vectors that all trace a 3D sphere of rotation.

CFrame Axes

CFrame * CFrame

CFrames are actually 4x4 matrices of the following form:

CFrameMath_cf4x4.png

This means we can easily multiply two CFrames together by simply multiplying two 4x4 matrices together!

CFrameMath_cf4x4Multiply2.png

Thus we can write a function to multiply two CFrames!

local function multiplyCFrame(a, b)
	local ax, ay, az, a11, a12, a13, a21, a22, a23, a31, a32, a33 = a:components()
	local bx, by, bz, b11, b12, b13, b21, b22, b23, b31, b32, b33 = b:components()
	local m11 = a11*b11+a12*b21+a13*b31
	local m12 = a11*b12+a12*b22+a13*b32
	local m13 = a11*b13+a12*b23+a13*b33
	local x = a11*bx+a12*by+a13*bz+ax
	local m21 = a21*b11+a22*b21+a23*b31
	local m22 = a21*b12+a22*b22+a23*b32
	local m23 = a21*b13+a22*b23+a23*b33
	local y = a21*bx+a22*by+a23*bz+ay
	local m31 = a31*b11+a32*b21+a33*b31
	local m32 = a31*b12+a32*b22+a33*b32
	local m33 = a31*b13+a32*b23+a33*b33
	local z = a31*bx+a32*by+a33*bz+az
	return CFrame.new(x, y, z, m11, m12, m13, m21, m22, m23, m31, m32, m33)
end

Alternatively a solution using loops:

local function multiply4x4(a, b)
	local out = {}
	for i = 1, 16 do
		out[i] = 0
		local r = math.floor((i-1)/4)+1
		local p = i%4 == 0 and 4 or i%4
		for j = 1, 4 do
			local ai = (r-1)*4+j
			local bi = p+(j-1)*4
			out[i] = out[i] + a[ai]*b[bi]
		end
	end
	return out
end
 
local function multiplyCFrame(a, b)
	local ax, ay, az, a11, a12, a13, a21, a22, a23, a31, a32, a33 = a:components()
	local bx, by, bz, b11, b12, b13, b21, b22, b23, b31, b32, b33 = b:components()
	a = {a11, a12, a13, ax, a21, a22, a23, ay, a31, a32, a33, az, 0, 0, 0, 1}
	b = {b11, b12, b13, bx, b21, b22, b23, by, b31, b32, b33, bz, 0, 0, 0, 1}
	local m11, m12, m13, x, m21, m22, m23, y, m31, m32, m33, z = unpack(multiply4x4(a, b))
	return CFrame.new(x, y, z, m11, m12, m13, m21, m22, m23, m31, m32, m33)
end

Finally, a test to verify.

local cf = CFrame.new(1, 2, 3) * CFrame.Angles(math.rad(14), math.rad(72), math.rad(-32));

print(cf*cf)
print(multiplyCFrame(cf, cf))
4.44273901, 3.34623194, 2.4210279, -0.849777162, -0.0723331869, 0.522155881, -0.316965073, 0.861586094, -0.396487743, -0.421203077, -0.502431393, -0.755083263
4.44273901, 3.34623194, 2.4210279, -0.849777162, -0.0723331869, 0.522155941, -0.316965073, 0.861586154, -0.396487743, -0.421203077, -0.502431393, -0.755083263

Something very important to note from all this. CFrame multiplication is not commutative. This means that a * b is not necessarily equal to b * a.

local cf1 = CFrame.new(1, 2, 3) * CFrame.Angles(math.rad(14), math.rad(72), math.rad(-32))
local cf2 = CFrame.new(0.1, -10, 6) * CFrame.Angles(math.rad(90), math.rad(-28), math.rad(-86))

print(cf1*cf2)
print(cf2*cf1)
5.09500504, -7.92827415, 7.54646206, -0.937961817, 0.220474482, -0.26761657, 0.0239842981, -0.728708208, -0.684404194, -0.345908046, -0.64836365, 0.678212643
0.514770269, -13.618248, 5.1419487, 0.162701935, 0.975506902, -0.148035079, 0.94501543, -0.197204709, -0.260875672, -0.283679247, -0.0974504724, -0.953954697

There are a few exceptions to this rule one of them is inverses, which we will talk about later, and the other is the identity CFrame which we will talk about now.

The Identity CFrame is as follows:

local identityCFrame = CFrame.new(0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1)
-- note: CFrame.new(0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1) == CFrame.new()

If we pre or post multiply a CFrame by the identity CFrame we simply get the original CFrame as if the multiplication never happened.

local cf = CFrame.new(1, 2, 3) * CFrame.Angles(math.rad(14), math.rad(72), math.rad(-32))

print(cf*CFrame.new())
print(CFrame.new()*cf)
1, 2, 3, 0.262061268, 0.163754046, 0.95105654, -0.319058299, 0.944782019, -0.0747579709, -0.910783052, -0.283851326, 0.299837857
1, 2, 3, 0.262061268, 0.163754046, 0.95105654, -0.319058299, 0.944782019, -0.0747579709, -0.910783052, -0.283851326, 0.299837857

CFrame * Vector3

Since we now know that CFrames are actually 4x4 matrices we can now get a look at how they multiply against vectors. The operation of multiplying a CFrame against a Vector3 looks like this in matrix form.

CFrameMath_cfv3Multiply.png

Thus we can write a function as such

local function multiplycfv3(a, b)
	local x, y, z, m11, m12, m13, m21, m22, m23, m31, m32, m33 = a:components()
	local vx, vy, vz = b.x, b.y, b.z
	local nx = m11*vx+m12*vy+m13*vz+x
	local ny = m21*vx+m22*vy+m23*vz+y
	local nz = m31*vx+m32*vy+m33*vz+z
	return Vector3.new(nx, ny, nz)
end

Once again we can test.

local cf = CFrame.new(1, 2, 3) * CFrame.Angles(math.rad(14), math.rad(72), math.rad(-32))
local v3 = Vector3.new(5, 6, -12)

print(cf*v3)
print(multiplycfv3(cf, v3))
-8.11984825, 6.97049618, -6.85507774
-8.11984825, 6.97049618, -6.85507774

Now unlike the CFrame * CFrame multiplication the CFrame * Vector3 multiplication can be broken down into something that is a bit more intuitive. Let’s slightly adjust the notation.

CFrameMath_cfv3Multiply2.png

Notice anything about the vectors we are multiplying against vx, vy, and vz? They’re the right, up, and back vectors we learned about earlier! We can rewrite our function to represent this.

local function multiplycfv3(a, b)
	return a.p + b.x*a.rightVector + b.y*a.upVector - b.z*a.lookVector
end

This also helps us visualize what the operation is actually doing.

CFrameMath_cfv3MultiplyVisual.png

CFrame + or - Vector3

Adding or subtracting Vector3s to CFrames is very straight forward. We simply add/subtract the vector x, y, and z to the CFrame x, y, and z and the rotation aspect remain unchanged.

local function addcfv3(a, b)
	local x, y, z, m11, m12, m13, m21, m22, m23, m31, m32, m33 = a:components()
	return CFrame.new(x + b.x, y + b.y, z + b.z, m11, m12, m13, m21, m22, m23, m31, m32, m33);
end;

And of course a test.

local cf = CFrame.new(1, 2, 3) * CFrame.Angles(math.rad(14), math.rad(72), math.rad(-32))
local v3 = Vector3.new(5, 6, -12)

print(cf + v3)
print(addcfv3(cf, v3))
6, 8, -9, 0.262061268, 0.163754046, 0.95105654, -0.319058299, 0.944782019, -0.0747579709, -0.910783052, -0.283851326, 0.299837857
6, 8, -9, 0.262061268, 0.163754046, 0.95105654, -0.319058299, 0.944782019, -0.0747579709, -0.910783052, -0.283851326, 0.299837857

The Inverse of a CFrame

This is one of the more challenging aspects of the CFrames for most people. In this article we will not be covering how to actually calculate the inverse but rather how to use it.

Near the end of the section on CFrame against CFrame multiplication it was mentioned that multiplication is not always commutative. This is NOT true for the inverse of a CFrame multiplied against the CFrame is was derived from. No matter if you pre or post multiply a CFrame by its inverse it will ALWAYS return the identity CFrame!

local cf = CFrame.new(1, 2, 3) * CFrame.Angles(math.pi/2, 0, 0)

print(cf*cf:inverse())
print(cf:inverse()*cf)
0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1
0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1

The trick to using the inverse of a CFrame is to write out an equation and then to use what we know about the identity CFrame and the non-commutative property of CFrame multiplication. Let’s do some examples.

Reverting to Original Values

Let’s say we have two CFrames and we multiply them together to get a new CFrame.

local cf1 = CFrame.new(1, 2, 3) * CFrame.Angles(math.pi/3, math.pi/6, 0)
local cf2 = CFrame.new(-4, 5, 7.2) * CFrame.Angles(0, math.pi/7, -math.pi/3)

local cf = cf1 * cf2

Say we are given only cf and cf1, but we want to find cf2. How can we do that? To start let’s look at the equation for cf.

cf = cf1 * cf2

We can then apply what we know about inverses to solve for cf2.

cf = cf1 * cf2
cf1:inverse() * cf = cf1:inverse() * cf1 * cf2 -- pre-multiply both sides by cf1:inverse()
cf1:inverse() * cf = CFrame.new() * cf2 -- recall cf:inverse() * cf = identityCFrame
cf1:inverse() * cf = cf2 -- recall identityCFrame * cf = cf

Sure enough when we test we can verify this.

local cf1 = CFrame.new(1, 2, 3) * CFrame.Angles(math.pi/3, math.pi/6, 0)
local cf2 = CFrame.new(-4, 5, 7.2) * CFrame.Angles(0, math.pi/7, -math.pi/3)

local cf = cf1*cf2

print(cf2)
print(cf1:inverse() * cf)
-4, 5, 7.19999981, 0.450484395, 0.780261934, 0.433883756, -0.866025448, 0.49999997, 0, -0.216941863, -0.375754386, 0.90096885
-4, 5.00000143, 7.19999933, 0.450484395, 0.780261934, 0.433883697, -0.866025507, 0.5, -2.98023224e-08, -0.216941863, -0.375754386, 0.90096879

note the slight variation in output is due to floating point math imprecision

Say we had cf2 and cf, but not cf1. To solve for that we follow a similar procedure.

cf = cf1 * cf2
cf * cf2:inverse() = cf1 * cf2 * cf2:inverse() -- post-multiply both sides by cf2:inverse()
cf * cf2:inverse() = cf1 * CFrame.new() -- recall cf * cf:inverse() = identityCFrame
cf * cf2:inverse() = cf1 -- recall cf * identityCFrame = cf

Once again testing to verify.

local cf1 = CFrame.new(1, 2, 3) * CFrame.Angles(math.pi/3, math.pi/6, 0)
local cf2 = CFrame.new(-4, 5, 7.2) * CFrame.Angles(0, math.pi/7, -math.pi/3)

local cf = cf1*cf2

print(cf1)
print(cf * cf2:inverse())
1, 2, 3, 0.866025388, 0, 0.5, 0.433012724, 0.49999997, -0.75, -0.249999985, 0.866025448, 0.433012664
1.00000048, 2.00000048, 3.00000095, 0.866025329, -2.98023224e-08, 0.49999997, 0.433012664, 0.5, -0.75, -0.25000003, 0.866025507, 0.433012664

note the slight variation in output is due to floating point math imprecision

You might be asking why does the pre/post multiplication matter? To see why let’s purposefully go through the steps where we pre-multiply cf by cf2:inverse() and see where that leads us.

cf = cf1 * cf2
cf2:inverse() * cf = cf2:inverse() * cf1 * cf2
-- we don't know what cf2:inverse() * cf1 = ???, thus
cf2:inverse() * cf = ???

The lesson here is that order matters and that what we do to one side we must do to the other and that includes whether or not we pre or post multiply!

Rotating a Door

Let’s say we want to CFrame a door opening. This might be difficult to someone learning CFrames because when we use the CFrame.Angles function on a part’s CFrame and update, it spins from the center.

CFrameMath_doorRotate1.gif

local door = game.Workspace.Door

game:GetService("RunService").Heartbeat:connect(function(dt)
	door.CFrame = door.CFrame * CFrame.Angles(0, math.rad(1)*dt*60, 0)
end)

Ideally we want to have our door spin around a hinge of some sort. This means we need to find a way to get our hinge to act as the center of rotation. We we know we can rotate the hinge in a similar way to how we rotated the door earlier.

CFrameMath_doorRotate2.gif

local door = game.Workspace.Door
local hinge = game.Workspace.Hinge

game:GetService("RunService").Heartbeat:connect(function(dt)
	hinge.CFrame = hinge.CFrame * CFrame.Angles(0, math.rad(1)*dt*60, 0)
end)

If we could somehow calculate the offset of the door from the un-rotated hinge we could apply that offset to the rotated hinge and get the rotated door CFrame. In other words we need to solve offset in the following:

hinge.CFrame * offset = door.CFrame

The key to finding the offset value is to use inverses! Remember, if we do something to one side of an equation we have to do it to the other.

hinge.CFrame * offset = door.CFrame -- want to get rid of hinge.CFrame on left side
hinge.CFrame:inverse() * hinge.CFrame * offset = hinge.CFrame:inverse() * door.CFrame -- we pre-multiply b/c non-commutative property
CFrame.new() * offset = hinge.CFrame:inverse() * door.CFrame -- cf:inverse() * cf = CFrame.new()
offset = hinge.CFrame:inverse() * door.CFrame -- CFrame.new() * cf = cf

Now that we have the offset it’s just a matter of applying it to the rotated hinge!

CFrameMath_doorRotate3-compressor.gif

local door = game.Workspace.Door
local hinge = game.Workspace.Hinge

local offset = hinge.CFrame:inverse() * door.CFrame; -- offset before rotation
game:GetService("RunService").Heartbeat:connect(function(dt)
	hinge.CFrame = hinge.CFrame * CFrame.Angles(0, math.rad(1)*dt*60, 0) -- rotate the hinge
	door.CFrame = hinge.CFrame * offset -- apply offset to rotated hinge
end)

Try Yourself: Welds

Welds are subject to the following constraint.

weld.Part0.CFrame * weld.C0 = weld.Part1.CFrame * weld.C1

Using what you know about inverses try to solve for Weld.C0 and Weld.C1. Try not to look at the answer til you’ve tried yourself.

--Solving for Weld.C0:
weld.Part0.CFrame * weld.C0 = weld.Part1.CFrame * weld.C1 
weld.Part0.CFrame:inverse() * weld.Part0.CFrame * weld.C0 = weld.Part0.CFrame:inverse() * weld.Part1.CFrame * weld.C1 
CFrame.new() * weld.C0 = weld.Part0.CFrame:inverse() * weld.Part1.CFrame * weld.C1 
weld.C0 = weld.Part0.CFrame:inverse() * weld.Part1.CFrame * weld.C1 

--Solving for Weld.C1:
weld.Part0.CFrame * weld.C0 = weld.Part1.CFrame * weld.C1 
weld.Part1.CFrame:inverse() * weld.Part0.CFrame * weld.C0 = weld.Part1.CFrame:inverse() * weld.Part1.CFrame * weld.C1 
weld.Part1.CFrame:inverse() * weld.Part0.CFrame * weld.C0 = CFrame.new() * weld.C1 
weld.Part1.CFrame:inverse() * weld.Part0.CFrame * weld.C0 = weld.C1

CFrame Methods

In this last section we will go over each of the transformation methods and some of the intuition you can apply to them.

CFrame:ToObjectSpace()

Equivalent to CFrame:inverse() * cf

We actually already know what this method does from when we got the offset when we were trying to rotate the door. This method calculates the offset CFrame needed to get from CFrame to get to cf

This can be easily verified in the following:

CFrame * CFrame:toObjectSpace(cf)
CFrame * CFrame:inverse() * cf
identityCFrame * cf
cf

CFrame:ToWorldSpace()

Equivalent to CFrame * cf

Since this method simply does CFrame multiplication it’s not super exciting. However, it’s name might help give us a bit more intuition in regards to what the multiplication operation is actually doing. We saw from the CFrame:toObjectSpace(cf) method that it returns the offset between two CFrames. We also noted that when we multiplied CFrame by the offset we ended with cf. So what is actually happening when we multiply two CFrames is that we are treating the second CFrame as an offset.

local cf = CFrame.new(1, 2, 3) * CFrame.Angles(math.rad(14), math.rad(72), math.rad(-32))
local offset = CFrame.new(0, 0, -10) -- 10 studs forward offset

print(cf:toWorldSpace(offset)) -- 10 studs forward, recall that forward for cf is cf.lookVector
-8.51056576, 2.74757957, 0.00162148476, 0.262061268, 0.163754046, 0.95105654, -0.319058299, 0.944782019, -0.0747579709, -0.910783052, -0.283851326, 0.299837857

CFrame:PointToObjectSpace()

Equivalent to CFrame:inverse() * v3

CFrameMath_pointToObjectSpace.gif

This method takes a point in 3D space, makes it relative to CFrame.p, and then converts it to an offset.

An alternative equivalent of this method would be (CFrame - CFrame.p):inverse() * (v3 - CFrame.p)

local cf = CFrame.new(1, 2, 3) * CFrame.Angles(math.rad(14), math.rad(72), math.rad(-32))
local v3 = Vector3.new(10, 10, 15)

print(cf:pointToObjectSpace(v3))
print((cf - cf.p):inverse() * (v3 - cf.p))
-11.123312, 5.62582684, 11.5594997
-11.123312, 5.62582684, 11.5594997

CFrame:PointToWorldSpace()

Equivalent to CFrame * v3

Since we already covered the intuition behind CFrame * v3 there’s not much more to say about this method other than what you already know. However, we will once again note that this method is the equivalent to applying an offset without the rotation aspect.

local cf = CFrame.new(1, 2, 3) * CFrame.Angles(math.rad(14), math.rad(72), math.rad(-32))
local v3 = Vector3.new(10, 10, 15)

print(cf * cf:pointToObjectSpace(v3))
10.000001, 10, 15.0000019

note the slight variation in output is due to floating point math imprecision

CFrame:VectorToObjectSpace()

Equivalent to (CFrame-CFrame.p):inverse() * v3

This method is very similar to the alternate form of the CFrame:pointObjectSpace(v3) method. The main difference is that the v3 no longer subtracts CFrame.p. This means the steps are very similar with one difference it does not make v3 relative to the CFrame.p, it assumes our input v3 is already relative.

local cf = CFrame.new(1, 2, 3) * CFrame.Angles(math.rad(14), math.rad(72), math.rad(-32))

print(cf:vectorToObjectSpace(cf.rightVector))
1.00000012, 2.98023224e-08, 0

note the slight variation in output is due to floating point math imprecision

We can see this is equal almost equal to Vector3.new(1, 0, 0), the identityCFrame’s rightVector. In a perfect world these two would be exactly equal, but as mentioned above the slight differences are do to floating point math imprecision.

CFrame:VectorToWorldSpace()

Equivalent to (CFrame-CFrame.p) * v3

An alternative equivalent of this method would be CFrame * v3 - CFrame.p which basically tells us this pretty much is the same as the the CFrame:pointWorldSpace(v3) method except that it does not add the CFrame.p (as we learned in the CFrame * v3 section).

local cf = CFrame.new(1, 2, 3) * CFrame.Angles(math.rad(14), math.rad(72), math.rad(-32))

print(cf:vectorToWorldSpace(Vector3.new(1, 0, 0)))
print(cf * Vector3.new(1, 0, 0) - cf.p)
print(cf.rightVector)
0.262061268, -0.319058299, -0.910783052
0.262061238, -0.319058299, -0.910783052
0.262061268, -0.319058299, -0.910783052
Tags:
  • cframe
  • coordinate frame
  • math