Earlier you learned how to position a part using the Vector3. But there is a limitation to using Vector3s for positioning—they lack the ability to set rotation.
It wouldn’t make for a very interesting game if every part always faced the same direction (or maybe it would?).
The ‘CFrame‘ datatype, short for coordinate frame, is a Roblox built-in type that encodes positional and also orientation data. This makes it suitable for operations where orientation is important.
Like most other Roblox datatypes, one way to create a CFrame is via the ‘new‘ constructor.
If no argument is given, an unrotated CFrame at position (0, 0, 0) will be returned. The constructor can also accept an argument in (X, Y, Z) format or a Vector3.
local cframe = CFrame.new() --(0, 0, 0)
local cframe = CFrame.new(1, 2, 3) --(1, 2, 3)
local cframe = CFrame.new(Vector3.new(1, 2, 3)) --(1, 2, 3)
With the third method above, you can also specify a Vector3 position and the returned CFrame will be constructed to point toward that position.
local part = Instance.new("Part")
local lookAtPart = Instance.new("Part")
local position = Vector3.new(25, 20, 33)
local lookAt = Vector3.new(10, 10, 30)
local cframe = CFrame.new(position, lookAt)
lookAtPart.Position = lookAt
part.CFrame = cframe
part.Parent = game.Workspace
lookAtPart.Parent = game.Workspace
This creates a part at position (25, 20, 33) and faces it toward (10, 10, 30).
Right click on the part and select “Show Orientation Indicator” to view the forward direction of the part.
While this constructor returns a rotated CFrame, it still does not give full control over the orientation.
You can see that part may be rotated around its forward axis any amount and still have a valid ‘LookAt‘ orientation. The forward axis is not “constrained.”
For something like an airplane or a camera, this unconstrained axis can have a major impact on the final result.
The API does provide quite a number of constructors and methods for creating or rotating a CFrame.
However most of the constructors that have the ability to set the position and rotation of a CFrame require some understanding of linear algebra (or Quaternions) and thus necessitate their own chapter.
We will return to those when we cross that bridge, but for beginners an easy way to create a rotated CFrame is via the ‘Angles‘ constructor.
This constructor cannot set the position of the CFrame but if composed with a positional CFrame, we can then arrive at a CFrame that has the desired position and rotation.
local radians = math.rad(45)
local rotatedCf = CFrame.Angles(radians, radians, radians)
local part = Instance.new("Part")
part.CFrame = rotatedCf
part.Parent = game.Workspace
‘math.rad‘ is a function from the Lua library. Because the CFrame constructor requires angles to be in radians, this function converts degrees to radians.
The constructed CFrame is positioned at (0, 0, 0) and rotated 45 degrees (counter-clockwise) around each local axis.
Rotation is applied in the order X->Y->Z.
This means that after the first rotation about the X axis, the local Y and Z are oriented in a new direction. From that new orientation, the local Y rotation is applied which orients local X and Z in a different direction. Finally from that new local orientation, the local Z rotation is applied.
To illustrate the example above, you can arrive at the original orientation (in this case, no an unrotated CFrame) using Studio tools by applying rotations in reverse.
Looking in the negative direction for each local axis, rotate the Z axis negative 45 degrees (i.e. clockwise), and then Y negative 45, and then X negative 45 in that order.
Operators
Like with Vector3, CFrames have their own set of math operators.
CFrames are based around its local axis which does not necessarily align with the world axis. Most operations are performed relative to this local axis.
CFrame * CFrame
Multiplying two CFrames returns a single CFrame composed of the two CFrames. The second CFrame is applied relative to the first, so order matters here.
local radAngle = math.rad(45)
local rotationCf = CFrame.Angles(0, radAngle, 0)
local positionCf = CFrame.new(30, 5, 0)
local positionFirstPart = Instance.new("Part")
local rotationFirstPart = Instance.new("Part")
positionFirstPart.BrickColor = BrickColor.new("Really blue")
rotationFirstPart.BrickColor = BrickColor.new("Really red")
positionFirstPart.CFrame = positionCf * rotationCf
rotationFirstPart.CFrame = rotationCf * positionCf
positionFirstPart.Parent = game.Workspace
rotationFirstPart.Parent = game.Workspace
In line 12, the CFrame is moved to (30, 5, 0) relative to its local axis (which is aligned with the world axis) and then rotated 45 degrees. This is the CFrame for the blue brick.
On the other hand, line 13 rotates the CFrame 45 degrees first and then moves to (30, 5, 0) relative to this local axis, creating the red brick’s CFrame. Both parts end up at different positions.
CFrame * Vector3
Multiplying a CFrame and Vector3 applies the position relative to the CFrame’s local axis and returns just the Vector3 of that new position.
local radians = math.rad(45)
local rotationCf = CFrame.Angles(0, radians, 0)
local positionCf = CFrame.new(30, 5, -10)
local v3Offset = Vector3.new(10, 0, 0)
local cf = positionCf * rotationCf
local position = cf * v3Offset
local worldPart = Instance.new("Part")
local localPart = Instance.new("Part")
worldPart.BrickColor = BrickColor.new("Really blue")
localPart.BrickColor = BrickColor.new("Really red")
worldPart.CFrame = cf
localPart.Position = position
worldPart.Name = "World Part"
worldPart.Parent = game.Workspace
localPart.Parent = game.Workspace
print("World position:", position)
The red part is at position (10, 0, 0) relative to the blue part’s local coordinates and has a world position of (37.071, 5, -17.071).
CFrame ± Vector3
And adding or subtracting a Vector3 from a CFrame does a simple translation in world space.
local cframe = CFrame.new(15, 5, -5) * CFrame.Angles(0, math.pi/4, 0)
local v3 = Vector3.new(0, 0, 15)
local cfPlusV3 = cframe + v3
local cfMinusV3 = cframe - v3
local partOriginal = Instance.new("Part")
local part1 = Instance.new("Part")
local part2 = Instance.new("Part")
partOriginal.BrickColor = BrickColor.new("Lime green")
partOriginal.Shape = Enum.PartType.Ball
part1.BrickColor = BrickColor.new("Really blue")
part2.BrickColor = BrickColor.new("Really red")
partOriginal.CFrame = cframe
part1.CFrame = cfPlusV3
part2.CFrame = cfMinusV3
partOriginal.Parent = game.Workspace
part1.Parent = game.Workspace
part2.Parent = game.Workspace
The red and blue parts are translated ±(0, 0, 15) in world space relative to the original CFrame position.