[Roblox] Single joint Inverse Kinematics using Law of Cosines


  • This tutorial will cover single joint Inverse Kinematics using Law of Cosines.

    To start off we have some questions we may need to answer:

    1. What is Forward Kinematics?
    Forward Kinematics is where you solve for the end effector (the target position, or end of the two or more joints) given the joint angles and lengths.

    2. What is Inverse Kinematics?
    Inverse Kinematics is the inverse of Forward Kinematics, where you are given the end effector (target position/point) and solve for the joint angles, given the lengths of the joints and distance from target..

    3. Is it a lot of math?
    Not really, its only a few lines and its not too hard either. But keep in mind this is only one method of completing inverse kinematics with a single joint, there are plenty of other methods you can choose from.

    4. What about when I have more than one joint?
    As I said, there are plenty of other methods, some are more accurate than others at the cost of more computing power.
    Some of the methods that cover multiple joints include:

    • Jacobian Inverse Kinematics
    • Cyclic Coordinate Descent
    • Forward And Backward Reaching Inverse Kinematics (FABRIK)

    I should note that EgoMoose (someone who has admirable math skills) has a comprehensive tutorial on FABRIK on the Roblox Wiki. You can see it here.

    I guess we can go straight into the tutorial now!

    • Insert 4 parts into workspace, name them: "origin", "target", "a", "b".
    The "origin" part will be the start of the joint (where the parts are connected to) and the "target" part is where we will want to reach (the end effector). The parts "a" and "b"
    Note: you don't have to create an origin or target part, you just need something where there is a consistent CFrame you can access.
    • Make the parts "origin" and "target" semi transparent (0.5 transparency) and cube shaped (Size '1,1,1').
    This step isn't actually necessary (except the size, partially) it just makes it more pretty to look at.
    • Make the parts "a" and "b" Size '0.5, 0.5, p' and '0.5, 0.5, q'.
    Lengths "p" and "q" can be any number, you can pick one between 1 and 10 for this tutorial to keep it simple. You can also colour them so you know which part is which. These will be the joint lengths.
    Make sure all the parts are anchored and have can collide off.
    So our parts don't fall off into the abyss and there aren't any collisions, duh.
    • Insert an empty script into workspace.
    This will be where we can write the code to solve the Inverse kinematics
    • Define all the parts we just created:

    local origin, target, pa, pb = workspace.origin, workspace.target, workspace.a, workspace.b
    

    This is so we can index these at any time without having to write out the whole path, saving us time and effort!
    • Define the lengths "a" and "b". We can do this using the part size, however you can also use numbers.

    local la, lb = pa.Size.Z, pb.Size.Z
    --or
    local la, lb = 4, 4
    

    If you are using numbers, make sure they match the parts length. This is important to the Cosine Law where you need all the lengths of all the sides in order to solve for the angles.

    • Create a function called "solveIK" with the parameters "originpos", "targetpos", "a", "b" and "angle".

    local origin, target, pa, pb = workspace.origin, workspace.target, workspace.a, workspace.b
    local la, lb = pa.Size.Z, pb.Size.Z
    
    local function solveIK(originpos, targetpos, a, b, angle)
    	
    end
    

    This is so we can call it multiple times around the script, it also makes it alot neater.
    As you can guess, the "originpos" and "targetpos" arguments are the origin position (vector) and target position (vector, end effector).
    The "a" and "b" arguments are the lengths of our two joints.
    The "angle" argument is the plane rotation. I've done a diagram to explain.

    • Solve the Plane CFrame, this is what the joint will be calculated on. There are many methods to solve this but we will use a simple method.

    local origin, target, pa, pb = workspace.origin, workspace.target, workspace.a, workspace.b
    local la, lb = pa.Size.Z, pb.Size.Z
    
    local function solveIK(originpos, targetpos, a, b, angle)
        local planeCF = CFrame.new(originpos, targetpos)
    end
    

    What this does is set the vector construct of the CFrame to the origin position, and angles it towards the target position. This will be what we solve the inverse kinematics on.
    We will need to use the "angle" parameter to rotate the Plane CFrame.

    local origin, target, pa, pb = workspace.origin, workspace.target, workspace.a, workspace.b
    local la, lb = pa.Size.Z, pb.Size.Z
    
    local function solveIK(originpos, targetpos, a, b, angle)
        local planeCF = CFrame.new(originpos, targetpos)  * CFrame.Angles(0, 0, math.rad(angle or 0))
    end
    

    This rotates the plane in any direction, for example, this may be good for creating elbows.
    If the angle parameter is not supplied, it will default to 0, hence the "or 0".
    • Solve for length "c"

    local origin, target, pa, pb = workspace.origin, workspace.target, workspace.a, workspace.b
    local la, lb = pa.Size.Z, pb.Size.Z
    
    local function solveIK(originpos, targetpos, a, b, angle)
        local planeCF = CFrame.new(originpos, targetpos)  * CFrame.Angles(0, 0, math.rad(angle or 0))
        local c = (originpos - targetpos).magnitude
    end
    

    This is basic vector math, where one vector is taken away from another, generating a new vector we can get the magnitude (length of).
    • Now that we have the length "a", "b" and "c" we can use Law of Cosines to solve the missing angles "B" and "C" (the angle name is that of the opposite side)
    Note: The Law of Cosines is normally written as c^2 = a^2 + b^2 - 2ab cos(C), so we need to rearrange this to get cos(A or B or C):
    cos(A) = (b^2 + c^2 - a^2)/(2bc)
    cos(B) = (a^2 + c^2 - b^2)/(2ac)
    cos(C) = (a^2 + b^2 - c^2)/(2ab)
    You can read more about the Law Of Cosines here.
    In roblox you will need to use the math.acos function to get the angle in radians:

    angle = math.acos((b^2 + c^2 - a^2)/(2*b*c))
    

    The acos function is the inverse of the cos function.
    • Now we need to put this inside of our function, solving for angles "B" and "C":
    This is the triangle we are solving.

    local origin, target, pa, pb = workspace.origin, workspace.target, workspace.a, workspace.b
    local la, lb = pa.Size.Z, pb.Size.Z
    
    local function solveIK(originpos, targetpos, a, b, angle)
        local planeCF = CFrame.new(originpos, targetpos)  * CFrame.Angles(0, 0, math.rad(angle or 0))
        local c = (originpos - targetpos).magnitude
    
        local B = -math.acos((a*a + c*c - b*b)/(2*a*c))
        local C = math.pi -math.acos((a*a + b*b - c*c)/(2*a*b))
        return planeCF, B, C --return it so we can use the values
    end
    

    Note: We need to take angle C away from pi (180 degrees) so it is the correct orientation.
    Generally, if the angle isn't correct, try adding these to the end of the statement:
    +math.pi, +math.pi/2, -math.pi, -math.pi/2
    e.g

    local B = -math.acos((a*a + c*c - b*b)/(2*a*c)) + math.pi/2
    

    (sorry about why I can't provide a legitimate reason to this, I just know that this sometimes works)
    Testing it:
    • Our function is almost complete but you can test it now and it should work.
    • We need to use the "Stepped" event of RunService so that it will run in our script:

    game:GetService("RunService").Stepped:Connect(function()
    
    end)
    

    This runs every step of the run service, which is roughly 1/60th of a second (much better than while loops), however if you are using local scripts, make sure you use RenderStepped as this will bind to the client refresh rate.
    • Now all we need to is call our function and set the cframes of the joints and we are ready to go!

    local origin, target, pa, pb = workspace.origin, workspace.target, workspace.a, workspace.b
    local la, lb = pa.Size.Z, pb.Size.Z
    
    local function solveIK(originpos, targetpos, a, b, angle)
        local planeCF = CFrame.new(originpos, targetpos)  * CFrame.Angles(0, 0, math.rad(angle or 0))
        local c = (originpos - targetpos).magnitude
    
        local B = -math.acos((a*a + c*c - b*b)/(2*a*c))
        local C = math.pi -math.acos((a*a + b*b - c*c)/(2*a*b))
        return planeCF, B, C
    end
    
    game:GetService("RunService").Stepped:Connect(function()
        local planeCF, B, C = solveIK(origin.Position, target.Position, la, lb)
        pa.CFrame =   planeCF * CFrame.Angles(B, 0, 0) * CFrame.new(0, 0, -la/2)
        pb.CFrame = pa.CFrame * CFrame.new( 0, 0, -la/2) * CFrame.Angles(C, 0, 0) * CFrame.new(0, 0, -lb/2)
    end)
    

    When we are setting the CFrame of our part "pa" we are setting it to the plane CFrame (where the vector construction of it is the same as the origin, and pointing at the target position) then we are setting the rotation of the part so it in the right direction of the triange, then offsetting it so that only the end of the part is touching the origin. Note how in roblox you have to very specific order for offsets and rotationsfor specific translations. One will make you do a rotation around the origin point (offset * angle) while the other will rotate it then offset it (transform it), (angle * offset), which in most cases is what you want.
    The offset for pb is setting it to the the CFrame of pa, then offsetting it by half of length a (to put it at the end of part a), then applying the correct triangle rotation to it, then offsetting it by half of length b (so the end of b touches the end of a), thus creating a joint.
    You should notice that if you run this, the parts do make a joint, however if you go outside the max length they will disappear or if one is shorter than the other, it will also disappear.

    Making some improvements:

    • All we need to now is make it so that when the target is out of reach, the parts point to the target but stay attached to the origin.
    So, we add some if statements:

    local origin, target, pa, pb = workspace.origin, workspace.target, workspace.a, workspace.b
    local la, lb = pa.Size.Z, pb.Size.Z
    
    local function solveIK(originpos, targetpos, a, b, angle)
        local planeCF = CFrame.new(originpos, targetpos)  * CFrame.Angles(0, 0, math.rad(angle or 0))
        local c = (originpos - targetpos).magnitude
    
        if c < math.max(a, b) - math.min(a, b) then
    	--when one part is smaller than the other and the target is out of reach
        elseif c > a + b then
    	--if the target is too far away from the origin
        else
            local B = -math.acos((a*a + c*c - b*b)/(2*a*c))
            local C = math.pi -math.acos((a*a + b*b - c*c)/(2*a*b))
            return planeCF, B, C
        end
    end
    
    game:GetService("RunService").Stepped:Connect(function()
        local planeCF, B, C = solveIK(origin.Position, target.Position, la, lb)
        pa.CFrame =   planeCF * CFrame.Angles(B, 0, 0) * CFrame.new(0, 0, -la/2)
        pb.CFrame = pa.CFrame * CFrame.new( 0, 0, -la/2) * CFrame.Angles(C, 0, 0) * CFrame.new(0, 0, -lb/2)
    end)
    

    The first if statement checks if the target is too close to the origin (if the parts are not the same length, this is the only time this if statement will run), where the long part offsets the short part too far off the origin so it can't reach it, or vice versa.
    The second if statement checks if the target is out of reach. this sets the parts as straight angles and makes them point at the target part, while being attached to the origin part.
    • Now all we need to do it calculate what should happen if one of the first two occur.

    local origin, target, pa, pb = workspace.origin, workspace.target, workspace.a, workspace.b
    local la, lb = pa.Size.Z, pb.Size.Z
    
    local function solveIK(originpos, targetpos, a, b, angle)
        local planeCF = CFrame.new(originpos, targetpos)  * CFrame.Angles(0, 0, math.rad(angle or 0))
        local c = (originpos - targetpos).magnitude
    
        if c < math.max(a, b) - math.min(a, b) then
    	local x = 0
    	if a < b then x = 1 end
    	return planeCF, math.pi*x, math.pi
        elseif c > a + b then
    	return planeCF, 0, 0
        else
            local B = -math.acos((a*a + c*c - b*b)/(2*a*c))
            local C = math.pi -math.acos((a*a + b*b - c*c)/(2*a*b))
            return planeCF, B, C
        end
    end
    
    game:GetService("RunService").Stepped:Connect(function()
        local planeCF, B, C = solveIK(origin.Position, target.Position, la, lb)
        pa.CFrame =   planeCF * CFrame.Angles(B, 0, 0) * CFrame.new(0, 0, -la/2)
        pb.CFrame = pa.CFrame * CFrame.new( 0, 0, -la/2) * CFrame.Angles(C, 0, 0) * CFrame.new(0, 0, -lb/2)
    end)
    

    I should state for the first one there is no "best method" to solve this, however the one I have given should do the job.
    It returns the plane cf, and part "a" remains attached to the the origin part, in the direction it normally would, then it makes the part "b" attempt to point to the target position. of course this can never be truly solved because of the length problems.
    The "x" variable" accounts for when part "a" is either longer or shorter than part "b".
    Of course, the second one just returns the planeCF and then the angles are 0 since it needs to point straight to the origin.
    • There we go! Thats all the code you will need to do single joint Inverse kinematics!

    [Updated on 29/11/2018 @ 19:52PM]

    Thank you for reading this tutorial. If you notice any mistakes or improvements that could be made, you can tell me.
    Tutorial inspired by WhoBloxedWho.


  • This made my brain hurt real bad.


  • This post is deleted!

  • 1 + 1 = 3 :wink:


  • @EXpodo1234ALT I guess


  • @DinozCreates in a good or bad way ;)

  • Banned

    This post is deleted!
  • Global Moderator

    This is a bad tutorial. It's just
    "do this, do that, do this...
    - why?
    - because i said so!"

    That's not what I call a tutorial.

    @abnotaddable said in [Roblox] Inverse Kinematics using Law of Cosines:

    1. What is Forward Kinematics?
      Forward Kinematics is where you solve for the end effector given the joint angles and > lengths.
    1. What is Inverse Kinematics?
      Inverse Kinematics is the inverse of Forward Kinematics, where you are given the end > effector and solve for the joint angles.

    You should start by explaining what is meant by "end effector"

    @abnotaddable said in [Roblox] Inverse Kinematics using Law of Cosines:

    1. Is it alot of math?
      Not really, its only a few lines and its not too hard either.

    This is implying there's only a single method to approaching inverse kinematics. The reality is that there are several ways to go about the problem, and this is just one of them. Some algorithms can be more accurate than others, but also more difficult to implement.

    For a good inverse kinematics tutorial, you should see EgoMoose's inverse kinematics tutorial on the Wiki instead.

    @abnotaddable said in [Roblox] Inverse Kinematics using Law of Cosines:

    1. Is it alot of math?

    Also, it's spelt a lot.


  • This post is deleted!

  • @Link150 Just updated it, hopefully a lot better! If you notice any more mistakes or improvements I could make, I am open to suggestions. All I want to make is a reliable source for people, so they too can do inverse kinematics. I didn't have many resources that clearly explained a single joint IK equation in a simplistic way. (for roblox)

Log in to reply
 

Looks like your connection to Scripting Helpers was lost, please wait while we try to reconnect.