Inverse kinematics for arm/hand placement with FABRIK?

for ease and for both my sanity and your sanity, im going to strictly follow the format:

  1. What do you want to achieve? Keep it simple and clear!
    i want to extend an R15 arm to a specified point using FABRIK

  2. What is the issue? Include screenshots / videos if possible!
    (sorry for straight gyazo links, it spits it out when i try to upload an image from a link)

including the target in my FABRIK chain causes the entire thing to bug out:

local points = {shoulderPoint,elbowPoint,wristPoint,target.Position}

https://i.gyazo.com/62d72574e732399ae640602e7a1c0f98.mp4

and omitting the target point almost works, but it skips the last entry and therefore skips the hand:

local points = {shoulderPoint,elbowPoint,wristPoint}

https://i.gyazo.com/b9fb6804d77e4bd9fdee71ebb543fe93.mp4

  1. What solutions have you tried so far? Did you look for solutions on the Developer Hub?
    ive tried using the 2-joint-2-limb system, but i have hands i have to work with as well, and it fell apart quickly. (plus, a post on there recommended FABRIK for th
    ive had more luck with FABRIK, and im trying to get it to work, which is the point of this post

Code:

FABRIK module, written by egomoose
-- FABRIK
-- Ego

local chain = {};
local plane = {};

-- table sum function
function sum(t)
	local s = 0;
	for _, value in ipairs(t) do
		s = s + value;
	end;
	return s;
end;

local part = Instance.new("Part");
part.Material = Enum.Material.Plastic;
part.Anchored = true;
part.CanCollide = false;
part.BrickColor = BrickColor.Blue();

local parts = {};
local model = Instance.new("Model", game.Workspace);
function drawline(key, a, v)
	if not parts[key] then parts[key] = part:Clone(); parts[key].Parent = model; end;
	parts[key].Size = Vector3.new(.2, .2, v.magnitude);
	parts[key].CFrame = CFrame.new(a + v/2, a + v);	
	return parts[key];
end;

-- fabrik chain class
function chain.new(joints, target)
	local self = setmetatable({}, {__index = chain});

	local lengths = {};
	for i = 1, #joints - 1 do
		lengths[i] = (joints[i] - joints[i+1]).magnitude;
	end;	
	
	self.n = #joints;
	self.tolerance = 0.1;
	self.target = target;
	self.joints = joints;
	self.lengths = lengths;
	self.origin = CFrame.new(joints[1]);
	self.totallength = sum(lengths);
	
	-- rotation constraints
	self.constrained = false;
	self.left = math.rad(89);
	self.right = math.rad(89);
	self.up = math.rad(89);
	self.down = math.rad(89);
	
	return self;
end;

-- this is the hardest part of the code so I super commented it!
function chain:constrain(calc, line, cf)
	local scalar = calc:Dot(line) / line.magnitude;
	local proj = scalar * line.unit;
	
	-- get axis that are closest
	local ups = {cf:vectorToWorldSpace(Vector3.FromNormalId(Enum.NormalId.Top)), cf:vectorToWorldSpace(Vector3.FromNormalId(Enum.NormalId.Bottom))};
	local rights = {cf:vectorToWorldSpace(Vector3.FromNormalId(Enum.NormalId.Right)),  cf:vectorToWorldSpace(Vector3.FromNormalId(Enum.NormalId.Left))};
	table.sort(ups, function(a, b) return (a - calc).magnitude < (b - calc).magnitude end);
	table.sort(rights, function(a, b) return (a - calc).magnitude < (b - calc).magnitude end);
	
	local upvec = ups[1];
	local rightvec = rights[1];

	-- get the vector from the projection to the calculated vector
	local adjust = calc - proj;
	if scalar < 0 then
		-- if we're below the cone flip the projection vector
		proj = -proj;
	end;
	
	-- get the 2D components
	local xaspect = adjust:Dot(rightvec);
	local yaspect = adjust:Dot(upvec);
	
	-- get the cross section of the cone
	local left = -(proj.magnitude * math.tan(self.left));
	local right = proj.magnitude * math.tan(self.right);
	local up = proj.magnitude * math.tan(self.up);
	local down = -(proj.magnitude * math.tan(self.down));
	
	-- find the quadrant
	local xbound = xaspect >= 0 and right or left;
	local ybound = yaspect >= 0 and up or down;
	
	local f = calc;
	-- check if in 2D point lies in the ellipse 
	local ellipse = xaspect^2/xbound^2 + yaspect^2/ybound^2;
	local inbounds = ellipse <= 1 and scalar >= 0;
	
	if not inbounds then
		-- get the angle of our out of ellipse point
		local a = math.atan2(yaspect, xaspect);
		-- find nearest point
		local x = xbound * math.cos(a);
		local y = ybound * math.sin(a);
		-- convert back to 3D
		f = (proj + rightvec * x + upvec * y).unit * calc.magnitude;
	end;
	
	-- return our final vector
	return f;
end;

function chain:backward()
	-- backward reaching; set end effector as target
	self.joints[self.n] = self.target;
	for i = self.n - 1, 1, -1 do
		local r = (self.joints[i+1] - self.joints[i]);
		local l = self.lengths[i] / r.magnitude;
		-- find new joint position
		local pos = (1 - l) * self.joints[i+1] + l * self.joints[i];
		self.joints[i] = pos;
	end;
end;

function chain:forward()
	-- forward reaching; set root at initial position
	self.joints[1] = self.origin.p;
	local coneVec = (self.joints[2] - self.joints[1]).unit;
	for i = 1, self.n - 1 do
		local r = (self.joints[i+1] - self.joints[i]);
		local l = self.lengths[i] / r.magnitude;
		-- setup matrix
		local cf = CFrame.new(self.joints[i], self.joints[i] + coneVec);
		-- find new joint position
		local pos = (1 - l) * self.joints[i] + l * self.joints[i+1];
		local t = self:constrain(pos - self.joints[i], coneVec, cf);
		self.joints[i+1] = self.constrained and self.joints[i] + t or pos;
		coneVec = self.joints[i+1] - self.joints[i];
	end;
end;

function chain:solve()
	local distance = (self.joints[1] - self.target).magnitude;
	if distance > self.totallength then
		-- target is out of reach
		for i = 1, self.n - 1 do
			local r = (self.target - self.joints[i]).magnitude;
			local l = self.lengths[i] / r;
			-- find new joint position
			self.joints[i+1] = (1 - l) * self.joints[i] + l * self.target;
		end;
	else
		-- target is in reach
		local bcount = 0;
		local dif = (self.joints[self.n] - self.target).magnitude;
		while dif > self.tolerance do
			self:backward();
			self:forward();
			dif = (self.joints[self.n] - self.target).magnitude;
			-- break if it's taking too long so the game doesn't freeze
			bcount = bcount + 1;
			if bcount > 10 then break; end;
		end;
	end;
end;

return chain;
my arm script, utilizing his FABRIK module
--frqst

local runService = game:GetService("RunService")

local ik = require(workspace.FABRIK)

local model = workspace.FrqstbiteDummy
local upperArm = model.RightUpperArm
local lowerArm = model.RightLowerArm
local hand = model.RightHand
local target = workspace.ArmFinish

function getMedian(vec1,vec2) --midpoint between vectors, used for easily getting joint between two parts
	return (vec1 + vec2)/2
end

function getExtreme(part,mult) --gets top or bottom of part; 1 for top, -1 for bottom
	local size = part.Size
	local cf = part.CFrame
	return (cf * CFrame.new(0,(size.Y/2) * mult,0)).Position
end

local shoulderPoint = getExtreme(upperArm,1)
local elbowPoint = getMedian(getExtreme(upperArm,-1),getExtreme(lowerArm,1))
local wristPoint = getMedian(getExtreme(lowerArm,-1),getExtreme(hand,1))

local parts = {upperArm,lowerArm,hand}
local points = {shoulderPoint,elbowPoint,wristPoint}
local chain = ik.new(points, target.Position)

function update()
	chain.target = target.Position
	chain:solve()
	for i,_ in ipairs(chain.joints) do
		local newVal = i < #chain.joints and i + 1
		if newVal then
			local diff = chain.joints[newVal] - chain.joints[i]
			parts[i].CFrame = CFrame.new(chain.joints[i] + diff/2, chain.joints[newVal]) * CFrame.Angles(math.rad(90),0,0)
		end;
	end;
end

update()
target:GetPropertyChangedSignal("CFrame"):Connect(update)
1 Like