Introduction
5/20/2024 - this is pretty old now, these examples/methods still work, but there are better ways of doing IK. This tutorial is still pretty decent practice for thinking through and solving problems/systems.
Some time ago I replied to a thread explaining a method of IK I used and the theory behind it. It’s been linked all over and I figure it could use a more thought out tutorial. This thread is an expanded stand-alone version of that reply. Here you will find a breakdown of a super basic form of IK solving as well an example .rbxl file.
This tutorial assumes you have an understanding of:
- Basic algebra and trigonometry
- CFrames pertaining to welds and operations
- Basic understanding of vectors
Theory
The TL;DR of Inverse Kinematics is that it’s a process of solving a system of joints and limbs to reach from one point to another. There are a number of ways to do this, all varying in complexity and purpose. We’ll be solving a 2 joint 2 limb system by adapting the Law of Cosines. For those unfamiliar the law allows us to take the lengths of a triangle and solve for its angles.
We can put the picture above into the context of an arm by relating the following values:
- Angle A is the angle of the shoulder
- Angle C is the angle of the elbow
- Vertex of angle A is the shoulder position
- Vertex of angle B is the goal position
- Length c is distance from the shoulder to the goal
- Length b is the length of the upper arm
- Length a is the length of the lower arm
Lengths b and a we’ll have to provide, but since they are arm lengths that information should already available to us. Length c is going to be the difference between the shoulder position and the goal position which is something we can easily calculate. We’ll just need to solve for the angles A and C, we can do that by adapting two of the following equations:
The values to solve our system may end up looking something like:
b = 1.255
a = 0.735
c = (shoulderPosition - goalPosition).magnitude
A = math.acos((-a^2 + b^2 - c^2) / (2 * b * c))
C = math.acos((a^2 + b^2 - c^2) / (2 * a * b))
Now all that is left to do is apply these values and account for cases in which these values cannot be solved for. This is the stage in which you have some creative decisions to make as to how you want to use your solved values, later on I’ll be showing and explaining the cases used in Apocalypse Rising 2.
Application
When it comes to applying properly solved angles in 3D space we can think of the line making up distance c as a “flat” point of reference between our shoulder and goal position. We can use CFrame.new(shoulderPosition, goalPosition)
to get a CFrame we can use as that reference point and orientation.
Applying the angles is now just as simple as building out the CFrames. Our shoulder represented as a CFrame is now just CFrame.Angles(A + math.pi/2, 0, 0)
, and our elbow as a CFrame is CFrame.Angles(C - A, 0, 0)
.
Depending on your use case you can plug these values in as “offsets” to joint C0 properties, or you may need to use them as references for building out the arm from shoulder to elbow. These may look something like:
-- constants used for solving
local shoulderC0Save = shoulder.C0
local elbowC0Save = elbow.C0
local upperArmLength = 1.4
local lowerArmLength = 1.2
-- base values to be calulated at run time
local shoulderCFrame = upperTorso.CFrame * shoulderC0Save
local basePlane = CFrame.new(shoulderCFrame.p, goalPosition)
local shoulderAngle = CFrame.Angles(A + math.pi/2, 0, 0)
local elbowAngle = CFrame.Angles(C - A, 0, 0)
-- weld/motor example
shoulder.C0 = upperTorso.CFrame:ToObjectSpace(basePlane) * shoulderAngle
elbow.C0 = elbowC0Save * elbowAngle
-- building it out example
upperArm.CFrame = basePlane * shoulderAngle * CFrame.new(0, -upperArmLength/2, 0)
lowerArm.CFrame = upperArm.CFrame * CFrame.new(0, -upperArmLength/2, 0) * elbowAngle * CFrame.new(0, -lowerArmLength/2, 0)
Unsolvable Cases
Unsolvable cases occur when the length between the shoulder position and goal position is shorter than the smallest of the arm lengths or is longer than the sum of both arm lengths. The c length in either of these cases will result in the cosine law spitting out numbers that cannot be used.
There are a few ways of solving these situations depending on your use case. In Apocalypse Rising 2 I do the following:
case: point distance is closer than our smallest arm length
solution: push back the shoulder position so the “hand” still reaches, fold the arm into itself
case: point distance is further than our arms can reach
solution: push forward the shoulder position so the “hand” still reaches, arm fully extended
Natural Arm Rotation
This method of IK is useful in cases where you’re trying to model the movement of arms or legs (2 joint 2 limb) in humans. The anatomy of a shoulder doesn’t allow it to rotate in the same ways CFrame.new(pointA, pointB)
will so we’ll need to come up with a better solution that better mimics the rotation restrictions of a human shoulder.
A simple solution to this problem is to roll our base plane. If you hold your arm out in front of you your elbow is pointed down and your shoulder can be described as having 0 roll. Attempting to mirror that hand position but behind you requires you to roll your shoulder and shift your elbow from pointing downwards to pointing upwards.
Translating that back into code means we’ll have to adjust the information we use to build our plane since we’ll need to factor in shoulder direction to derive the rolling motion. We can do this by changing the ShoulderPosition
value from a Vector3 to a CFrame since it will better model our base of orientation.
To start we’ll need to find out our goal position localized to our shoulder cframe. This is going to give us a direction vector we can use to base our rolling motion off of.
local localized = shoulderCFrame:PointToObjectSpace(goalPosition)
local localizedUnit = localized.unit
We can use cross product and our direction from above to create a new vector that represents the axis we’ll be rolling on. The angle at which we’ll rotate along that axis can be taken from our direction vector as well.
local axis = Vector3.new(0, 0, -1):Cross(localizedUnit)
local angle = math.acos(-localizedUnit.Z)
Using our new angle and rotation axis we can replace:
local basePlane = CFrame.new(shoulderPosition, goalPosition)
With:
local basePlane = originCF * CFrame.fromAxisAngle(axis, angle)
Giving us the same directional behavior as CFrame.new(pointA, pointB)
but with the more natural rolling motion described above.
Putting Everything Together
So now that we’ve learned how to solve the law of cosines, safeguard against unsolvable cases, and create a better arm rotation motion, we can finally put it all into a single solution. The following code snippet combines everything covered in this thread into a single function:
-- originCF: the "Shoulder" CFrame
-- targetPos: the goal position we're trying to reach
-- l1: the length of a segment of arm
-- l2: the length of a segment of arm
local function solveIK(originCF, targetPos, l1, l2)
-- build intial values for solving
local localized = originCF:pointToObjectSpace(targetPos)
local localizedUnit = localized.unit
local l3 = localized.magnitude
-- build a "rolled" planeCF for a more natural arm look
local axis = Vector3.new(0, 0, -1):Cross(localizedUnit)
local angle = math.acos(-localizedUnit.Z)
local planeCF = originCF * CFrame.fromAxisAngle(axis, angle)
-- case: point is to close, unreachable
-- return: push back planeCF so the "hand" still reaches, angles fully compressed
if l3 < math.max(l2, l1) - math.min(l2, l1) then
return planeCF * CFrame.new(0, 0, math.max(l2, l1) - math.min(l2, l1) - l3), -math.pi/2, math.pi
-- case: point is to far, unreachable
-- return: for forward planeCF so the "hand" still reaches, angles fully extended
elseif l3 > l1 + l2 then
return planeCF * CFrame.new(0, 0, l1 + l2 - l3), math.pi/2, 0
-- case: point is reachable
-- return: planeCF is fine, solve the angles of the triangle
else
local a1 = -math.acos((-(l2 * l2) + (l1 * l1) + (l3 * l3)) / (2 * l1 * l3))
local a2 = math.acos(((l2 * l2) - (l1 * l1) + (l3 * l3)) / (2 * l2 * l3))
return planeCF, a1 + math.pi/2, a2 - a1
end
end
Found below is a more hands on example showcasing usage on systems with no welds/joints and a system that uses character limbs. I’d encourage you all to explore more ways of applying the basic concepts covered here, and I’d love to see what you end up creating.
Accurate play solo may inhibit your ability to interact with the demo file as described. Start server or turning off accurate play solo is recommended to get desired results when testing the jointless solving demo.