Hello I’m dthecoolest, I’m mainly known for my CFrame work on the devforum specifically inverse kinematics with the CCDIK controller. Surprisingly there isn’t a lot of motor6d or weld tutorial on the devforum so I’ll be here to try and fix that.
Contents
CFrame Multiplication what is it?. 2
Understanding welds and Motor6Ds. 3
World CFrame relative to Motor6D/Weld space. 4
Common CFrame Tips and Advice. 5
CFrame Rotation in WorldSpace. 6
CFrame.Angles ~= CFrame.fromOrientation, and how I learned to love CFrame.fromOrientation. 7
Prerequisite knowledge
I recommend watching Alvinblox tutorial as it will teach you the basics of what a CFrame is (position and orientation) and the main points.
- you can combine CFrames using multiplication and that CFrame.Angles uses radians.
Part.CFrame = CFrame.new(5,5,5) * CFrame.Angles(0, math.rad(180), 0)
- And that you can “add” onto a CFrame via multiplication
Part.CFrame = Part.CFrame * CFrame.Angles(0, math.rad(180), 0)
- The LookVector, RightVector, and UpVector which describe where the part is looking as a unit vector with the length/magnitude equal to 1.
I will try my best to expand upon that with one weird quirk about CFrame multiplication and its implications.
CFrame Multiplication what is it?
One of the main CFrame article is this one on CFrame math’s and multiplication and you see all the complicated math and you are wondering what does it mean?
Honestly, we can basically ignore the maths and instead focus on the results by what we observe when performing CFrame operations which leads to the most important rule there is.
When multiplying CFrames, the Axis in which the translational or rotational motion occurs is relative to the previous CFrame being multiplied with.
Coincidentally this is the same as CFrame:ToWorldSpace() which is also just CFrame multiplication and the description provide in Understanding CFrames article.
The
CFrame:ToWorldSpace()
function transforms an object’s CFrame — respecting its own local orientation — to a new world orientation
The best way to visualize this property is through attachments as an attachments WorldCFrame uses CFrame multiplication as seen below.
worldCFrame = attachment.Parent.CFrame * attachment.CFrame
--or
Attachment.WorldCFrame = PartWithAttachment.CFrame * attachment.CFrame
This WorldCFrame describes where the attachment is located relative to origin like a parts CFrame.
Additionally, additional knowledge for those not familiar with Attachments is the Axis and Secondary Axis. Basically, the attachments secondary axis represents the Y axis (UpVector) and the Axis represents the X axis (RightVector).
To observe how CFrame multiplication is relative based upon the previous CFrame we can simply insert an attachment in a part and rotate it and observe where the attachment will end up keeping in mind the attachment WorldCFrame formula.
Here is an example with an attachment with position (5, 5, -5) and notice how the attachment is located diagonally from the top left corner of the face.
Then by turning the head +45 degrees on the Y axis:
Observations:
- As noticed the positioning of the (5, 5, -5) attachment remains the same relative to the part remaining at the top left corner of the face diagonally outwards.
- Additionally, the X and Y axis of the attachment still maintain the same relative to the part’s RightVector and UpVector showing the rotation is the same as the part.
Consequently, this shows that the axis in which the (5, 5, -5) translation is relative to the parts CFrame axis which leads the attachment being positioned the same diagonal position from the part’s corner. This also creates a point and orientation known as a “CFrame offset” which will be an important term to take note.
Another method to visualize the importance of order of operations is a basic rotation script when the part is tilted.
local part = script.Parent
while true do
local dt = task.wait()
local radiansPerSecond = math.pi/4
local increment = dt*radiansPerSecond
part.CFrame = part.CFrame*CFrame.Angles(0,increment,0)
end
First as noticed in the video the part is rotating along it’s local Y axis, due to the fact that Y axis is based on part.CFrame which is being multiplied by.
Now compare this with the rotation happening before adding on the part’s CFrame
local part = script.Parent
while true do
local dt = task.wait()
local radiansPerSecond = math.pi/2
local increment = dt*radiansPerSecond
local rotation = CFrame.Angles(0,increment,0)*part.CFrame
part.CFrame = rotation
end
As noted in the video the rotation is happening along the global Y axis instead of the parts local Y axis. Why is this happening?
Well as we noted down the previous CFrame that is being multiplied by is the axis of the rotation, so what is the axis here? The answer is that the axis is CFrame.new() which is the origin CFrame as can be seen below:
local rotation = CFrame.Angles(0,increment,0)*part.CFrame
--CFrame.new() * anyCFrame = anyCFrame, identity CFrame multiplication property
--Also CFrame.new() is located at the origin (0, 0, 0) with orientation (0, 0, 0)
local rotation =CFrame.new()* CFrame.Angles(0,increment,0)*part.CFrame
However, this method also messes up with the translational axis which can be undesirable creating a sort of orbital motion effect.
Consequently, it’s recommended to do the operations separately, firstly perform the rotation relative to the world space axis, then add the parts original position:
local part = script.Parent
while true do
local dt = task.wait()
local radiansPerSecond = math.pi/2
local increment = dt*radiansPerSecond
local rotation = CFrame.Angles(0,increment,0)*part.CFrame
part.CFrame = rotation.Rotation + part.Position
end
Understanding welds and Motor6Ds
Opening the Devforum API Reference on welds will give you this info.
While the weld is Active, it maintains the part positions such that:
part1.CFrame * C1 == Part0.CFrame * C0
What does this mean?
Using what we know of CFrame multiplication and attachment world CFrame one can notice the equation is very similar to an attachment WorldCFrame equation.
Attachment.WorldCFrame = Part.CFrame * Part.Attachment.CFrame
Where Part is the Part0 and C1 is a constant “CFrame offset” CFrame just like an attachment’s CFrame.
Consequently, by substituting the attachment WorldCFrame equation into a weld equation we can see what weld equation is implying.
part1.CFrame * C1 == Part0.CFrame * C0
--Replace C1 and C0, with a constant Attachment CFrame
Part1.CFrame * Part1.Attachment.CFrame == Part0.CFrame * Part0.Attachment.CFrame
--Replace PartWithAttachment.CFrame * attachment.CFrame with Attachment.WorldCFrame
Part1.Attachment.WorldCFrame == Part0.Attachment.WorldCFrame
From this equation we can finally deduce what a weld does which maintains the equation such that these two points created by the CFrame offsets are equal.
To visualize it we can use the new Rigid Constraint CFrame offsets points can be visualized as attachments WorldCFrame as previously discussed:
Yep, that’s all a weld is.
And you can even do it by script using the original weld Instance:
local weld = Instance.new("Weld")
weld.C0 = attachment0.CFrame
weld.Part0 = attachment0.Parent
weld.C1 = attachment1.CFrame
weld.Part1 = attachment1.Parent
weld.Parent = attachment1.Parent --any parent under workspace to make the weld active
World CFrame relative to Motor6D/Weld space
Ok, now that we have understood what a weld can do, so now the question is how do we manipulate it via script without breaking the weld? The answer is to modify the offsets C0 and C1. Unfortunately this goes into CFrame:Inverse() territory which is a bit more complex and not accounted for in this tutorial however I will just point out the math properties during the derivation
--Starting from the weld equation
part1.CFrame * C1 == Part0.CFrame * C0
--Apply Part0.CFrame:Inverse() to the left hand side of both equations
Part0.CFrame:Inverse()*part1.CFrame * C1 = Part0.CFrame:Inverse()*Part0.CFrame * C0
--Apply CFrame:Inverse() * CFrame = CFrame.new(), CFrame Inverse property
Part0.CFrame:Inverse()*part1.CFrame * C1 = CFrame.new() * C0
--CFrame.new() *anyCFrame = anyCFrame, Identity CFrame property
Part0.CFrame:Inverse()*part1.CFrame * C1 = C0
--Rearrange equation, swap sides
C0 = Part0.CFrame:Inverse()*part1.CFrame * C1
--We want part1 CFrame to equal some CFrame like CFrame.lookAt()
C0 = Part0.CFrame:Inverse()*CFrame.lookAt(part1, part2) * C1
Using this knowledge, we have derived one very useful CFrame formula.
Here is what it looks like in function form and an example when applies to a common Motor6D rig like R6:
local motor = script.Parent
local target = workspace.Target
local function worldCFrameRotationToC0ObjectSpace(motor6DJoint,worldCFrame)
local part1CF = motor6DJoint.Part1.CFrame
local part0 = motor6DJoint.Part0
local c1Store = motor6DJoint.C1
local c0Store = motor6DJoint.C0
local relativeToPart0 = part0.CFrame:inverse() * worldCFrame * c1Store
local goalC0CFrame = relativeToPart0
return goalC0CFrame
end
while true do
task.wait()
motor.C0 = worldCFrameRotationToC0ObjectSpace(motor, target.CFrame)
end
Wow it functions as if it’s just a regular part and you are simply setting the parts CFrame!
However usually you don’t want limbs detached so we maintain the original position component and input in our new calculated CFrame rotation. So these are the changes to the function:
local function worldCFrameRotationToC0ObjectSpace(motor6DJoint,worldCFrame)
local part1CF = motor6DJoint.Part1.CFrame
local part0 = motor6DJoint.Part0
local c1Store = motor6DJoint.C1
local c0Store = motor6DJoint.C0
local relativeToPart0 = part0.CFrame:inverse() * worldCFrame * c1Store
local goalC0CFrame = relativeToPart0.Rotation+c0Store.Position--New orientation but keep old C0 joint position
return goalC0CFrame
end
As seen this implies we can use WorldSpace CFrames for it! Stuff like CFrame.lookAt will work now which is especially useful for inverse kinematics, some arm pointing, and head look at script.
As a bonus thanks to some advice is the derivation for C1 which is more interesting as the goal CFrame is the one being inversed instead of the Part0 for the C0 solution:
Part0.CFrame * C0 = part1.CFrame * C1
weld.Part1.CFrame:inverse() * weld.Part0.CFrame * weld.C0 = weld.C1
--Subsitute part1 with some goalCFrame, to make part1 go to goal target
goalCFrame:Inverse() * motor6DJoint.Part0.CFrame * motor6DJoint.C0
local function worldCFrameRotationToC1(motor6DJoint, worldCFrame)
local part0 = motor6DJoint.Part0
local c0Store = motor6DJoint.C0
return worldCFrame:Inverse() * part0.CFrame * c0Store
end
Common CFrame Tips and Advice
CFrame Rotation in WorldSpace
To show case the importance of order of operations within CFrames is this method to rotate a CFrame in worldspace instead of the parts local axis. Here is
CFrame.Angles ~= CFrame.fromOrientation, and how I learned to love CFrame.fromOrientation
Simply put, CFrame.Angles is not equal to orientation which will lead to some unintended bugs and that Orientation is really easy to work with since it’s the default angle measurement of Roblox Studio.
Additionally there is a really neat property with CFrame.lookAt and CFrame:ToOrientation() and that’s a CFrame generated by CFrame.lookAt() doesn’t have any Z axis component when converted to orientation which I used in my turret controller. However :ToObjectSpace() was needed for certain scenarios especially when the turret is rotated.
CFrame aligning vectors
Usually, we want to align a CFrame such that it’s UpVector matches another arbitrary vector for example if we want to align a part to a surface normal vector.
One common method that is seen is the CFrame.lookAt method
CFrame.lookAt(pos1, pos1 + newDirection)
However, the above CFrame method forcefully also generates the rest of the vectors such as the right vector and up vector and doesn’t allow for much control unless you manually mess around with the third parameter the “UpVector”
The second option you could use, is @EgoMooses advanced CFrame technique like in this which does the job of alignment post.
Here are some more other resources which I found usefull when studying this type of problem. One with a problem statement, and one with a explanation of surface normal plus the CFrame look at technique.
Conclusion
Thanks for reading!
Please let me know if I have missed anything in this tutorial or if some points could be clarified further.
I believe there’s a lot more such as using CFrame:Inverse() or CFrame:ToObjectSpace() however that should be pretty lengthy and deserves it’s own tutorial.