Procedural R15 Arm Solving from Torso and Hand Positions

For Nexus VR Character Model V2, I need to map character arms and legs to VR inputs. The torso is defined already by the head, and the hands are defined by the controllers, so all that is needed is to solve the upper and lower arms and legs. I have done this before with the first version and Self-Driving Simulator, but they both have issues when trying to use Roblox’s attachments. I am attempting to redo the code for it, and I haven’t had any luck getting a reliable result for the past few hours. Sometimes, the result is acceptable but breaks the wrist, and sometimes it is unusable.

The implementation I attempted was with LMH_Hutch’s joint solving with a code block I have used for a few years. However, when attempting to blindly use it to get the angles and facing angle to the wrist and use Roblox’s attachments, Roblox’s attachments on some rigs results in the arm going off because they don’t follow a straight line through the arm/leg.
image

I then attempted to apply a correction to the rotation based on where the end is and where it should be, which results in the first screenshot. The following is the code used for the demo as well as a demo place file:

--[[
TheNexusAvenger

Solves the R15 arm for a given shoulder CFrame
and CFrame to hold.
--]]

--[[
Solves a joint using the "naive" approach. Recycled
from several projects.
--]]
local function SolveJoint(OriginCFrame,TargetPosition,Length1,Length2)
	local LocalizedPosition = OriginCFrame:pointToObjectSpace(TargetPosition)
	local LocalizedUnit = LocalizedPosition.unit
	local Hypotenuse = LocalizedPosition.magnitude

	--Get the axis and correct it if it is 0.
	local Axis = Vector3.new(0,0,-1):Cross(LocalizedUnit)
	if Axis == Vector3.new(0,0,0) then 
		if LocalizedPosition.Z < 0 then
			Axis = Vector3.new(0,0,0.001)
		else
			Axis = Vector3.new(0,0,-0.001)
		end
	end

	--Calculate and return the angles.
	local PlaneRotation = math.acos(-LocalizedUnit.Z)
	local PlaneCFrame = OriginCFrame * CFrame.fromAxisAngle(Axis,PlaneRotation)
	if Hypotenuse < math.max(Length2,Length1) - math.min(Length2,Length1) then
		local ShoulderAngle,ElbowAngle = -math.pi/2,math.pi
		return PlaneCFrame * CFrame.new(0,0,math.max(Length2,Length1) - math.min(Length2,Length1) - Hypotenuse),ShoulderAngle,ElbowAngle
	elseif Hypotenuse > Length1 + Length2 then
		
		local ShoulderAngle,ElbowAngle = math.pi/2, 0
		return PlaneCFrame * CFrame.new(0,0,Length1 + Length2 - Hypotenuse),ShoulderAngle,ElbowAngle
	else
		local Angle1 = -math.acos((-(Length2 * Length2) + (Length1 * Length1) + (Hypotenuse * Hypotenuse)) / (2 * Length1 * Hypotenuse))
		local Angle2 = math.acos(((Length2  * Length2) - (Length1 * Length1) + (Hypotenuse * Hypotenuse)) / (2 * Length2 * Hypotenuse))

		return PlaneCFrame,Angle1 + math.pi/2,Angle2 - Angle1
	end
end



--[[
Returns the CFrame of the upper arm, lower arm,
and hand of the right arm.
--]]
return function (StartCFrame,HoldCFrame,RightUpperArm,RightLowerArm,RightHand)
	--Determine the appendage end CFrame.
	local AppendageEndCFrame = HoldCFrame * RightHand:WaitForChild("RightGripAttachment").CFrame:Inverse()
	local AppendageEndJointCFrame = AppendageEndCFrame * RightHand:WaitForChild("RightWristRigAttachment").CFrame

	--Get the attachment CFrames.
	local UpperLimbStartCFrame = RightUpperArm:WaitForChild("RightShoulderRigAttachment").CFrame
	local UpperLimbJointCFrame = RightUpperArm:WaitForChild("RightElbowRigAttachment").CFrame
	local LowerLimbJointCFrame = RightLowerArm:WaitForChild("RightElbowRigAttachment").CFrame
	local LowerLimbEndCFrame = RightLowerArm:WaitForChild("RightWristRigAttachment").CFrame

	--Solve the joint.
	local UpperLimbLength = (UpperLimbStartCFrame.Position - UpperLimbJointCFrame.Position).Y
	local LowerLimbLength = (LowerLimbJointCFrame.Position - LowerLimbEndCFrame.Position).Y
	local PlaneCFrame,ShoulderAngle,ElbowAngle = SolveJoint(StartCFrame,AppendageEndJointCFrame.Position,UpperLimbLength,LowerLimbLength)
	
	--Correct the rotation of the start joint so that it targets the end correctly.
	local TargetGoal = CFrame.new(PlaneCFrame.Position,AppendageEndJointCFrame.Position)
	local TargetActual = CFrame.new(PlaneCFrame.Position,(PlaneCFrame * CFrame.Angles(ShoulderAngle,0,0) * UpperLimbStartCFrame:Inverse() * UpperLimbJointCFrame * CFrame.Angles(ElbowAngle,0,0) * LowerLimbJointCFrame:Inverse() * LowerLimbEndCFrame).Position)
	local StartJointCorrection = TargetActual:Inverse() * TargetGoal
	--StartJointCorrection = CFrame.new() --Quick way to disable the correction attempt.

	--Calculate the part CFrames.
	local PlaneCFrameRotation = CFrame.new(-PlaneCFrame.Position) * PlaneCFrame
	local StartJointCFrame = CFrame.new(PlaneCFrame.Position) * StartJointCorrection * PlaneCFrameRotation * CFrame.Angles(ShoulderAngle,0,0)
	local UpperLimbCFrame = StartJointCFrame * UpperLimbStartCFrame:Inverse()
	local JointCFrame = UpperLimbCFrame * UpperLimbJointCFrame * CFrame.Angles(ElbowAngle,0,0)
	local LowerLimbCFrame = JointCFrame * LowerLimbJointCFrame:Inverse()

	--Return the part CFrames.
	return UpperLimbCFrame,LowerLimbCFrame,AppendageEndCFrame
end

R15 IK Arm.rbxl (29.3 KB)

I am not sure how to go from here to make it work reliably. I don’t need it to be perfect in every case as it would rely on the other components to be perfect, which the torso won’t be when the user is taking off a headset.

2 Likes

why not try removing the joints all together?
or just detect if somethin is off then have the game manually realign the attachments / “joints”?

seems to be working fine for me

1 Like

The point of Nexus VR Character Model is to use the player’s R15 character model. Having VR players be blocky and non-VR players being normal would be strange.

That is what I was attempting, but didn’t get anywhere.

I do have another idea to try at some point, and I will post if it is successful.

I got it!

To summarize, the thought I had was to solve the joint normally assuming 2 straight lights from the attachments, calculate the base elbow CFrames, and then apply a rotational offset to align the disconnected attachments. This rotational offset is the inverse of the angle with respect to the Y axis from the ending attachment to the starting attachment. The following code can be used with place file in the original post, although some comments are missing.

--[[
TheNexusAvenger

Solves the R15 arm for a given shoulder CFrame
and CFrame to hold.
--]]

--[[
Solves a joint using the "naive" approach. Recycled
from several projects.
--]]
local function SolveJoint(OriginCFrame,TargetPosition,Length1,Length2)
	local LocalizedPosition = OriginCFrame:pointToObjectSpace(TargetPosition)
	local LocalizedUnit = LocalizedPosition.unit
	local Hypotenuse = LocalizedPosition.magnitude

	--Get the axis and correct it if it is 0.
	local Axis = Vector3.new(0,0,-1):Cross(LocalizedUnit)
	if Axis == Vector3.new(0,0,0) then 
		if LocalizedPosition.Z < 0 then
			Axis = Vector3.new(0,0,0.001)
		else
			Axis = Vector3.new(0,0,-0.001)
		end
	end

	--Calculate and return the angles.
	local PlaneRotation = math.acos(-LocalizedUnit.Z)
	local PlaneCFrame = OriginCFrame * CFrame.fromAxisAngle(Axis,PlaneRotation)
	if Hypotenuse < math.max(Length2,Length1) - math.min(Length2,Length1) then
		local ShoulderAngle,ElbowAngle = -math.pi/2,math.pi
		return PlaneCFrame * CFrame.new(0,0,math.max(Length2,Length1) - math.min(Length2,Length1) - Hypotenuse),ShoulderAngle,ElbowAngle
	elseif Hypotenuse > Length1 + Length2 then
		
		local ShoulderAngle,ElbowAngle = math.pi/2, 0
		return PlaneCFrame * CFrame.new(0,0,Length1 + Length2 - Hypotenuse),ShoulderAngle,ElbowAngle
	else
		local Angle1 = -math.acos((-(Length2 * Length2) + (Length1 * Length1) + (Hypotenuse * Hypotenuse)) / (2 * Length1 * Hypotenuse))
		local Angle2 = math.acos(((Length2  * Length2) - (Length1 * Length1) + (Hypotenuse * Hypotenuse)) / (2 * Length2 * Hypotenuse))

		return PlaneCFrame,Angle1 + math.pi/2,Angle2 - Angle1
	end
end


local function RotationTo(StartCFrame,EndCFrame)
	local Offset = (StartCFrame:Inverse() * EndCFrame).Position
	return CFrame.Angles(math.atan2(Offset.Z,Offset.Y),0,-math.atan2(Offset.X,Offset.Y))
end

--[[
Returns the CFrame of the upper arm, lower arm,
and hand of the right arm.
--]]
return function (StartCFrame,HoldCFrame,RightUpperArm,RightLowerArm,RightHand)
	--Determine the end CFrame of the appendage end's joint.
	local AppendageEndJointCFrame = HoldCFrame * RightHand:WaitForChild("RightGripAttachment").CFrame:Inverse() * RightHand:WaitForChild("RightWristRigAttachment").CFrame

	--Get the attachment CFrames.
	local UpperLimbStartCFrame = RightUpperArm:WaitForChild("RightShoulderRigAttachment").CFrame
	local UpperLimbJointCFrame = RightUpperArm:WaitForChild("RightElbowRigAttachment").CFrame
	local LowerLimbJointCFrame = RightLowerArm:WaitForChild("RightElbowRigAttachment").CFrame
	local LowerLimbEndCFrame = RightLowerArm:WaitForChild("RightWristRigAttachment").CFrame

	--Solve the joint.
	local UpperLimbLength = (UpperLimbStartCFrame.Position - UpperLimbJointCFrame.Position).magnitude
	local LowerLimbLength = (LowerLimbJointCFrame.Position - LowerLimbEndCFrame.Position).magnitude
	local PlaneCFrame,ShoulderAngle,ElbowAngle = SolveJoint(StartCFrame,AppendageEndJointCFrame.Position,UpperLimbLength,LowerLimbLength)
	
	local JointPreCFrame = PlaneCFrame * CFrame.Angles(ShoulderAngle,0,0) * CFrame.new(0,-UpperLimbLength,0)
	local JointPostCFrame = JointPreCFrame * CFrame.Angles(ElbowAngle,0,0)
	local UpperLimbCFrame = JointPreCFrame * RotationTo(UpperLimbJointCFrame,UpperLimbStartCFrame):Inverse() * UpperLimbJointCFrame:Inverse()
	local LowerLimbCFrame = JointPostCFrame * RotationTo(LowerLimbEndCFrame,LowerLimbJointCFrame):Inverse() * LowerLimbJointCFrame:Inverse()
	local AppendageEndCFrame = CFrame.new((LowerLimbCFrame * LowerLimbEndCFrame).Position) * (CFrame.new(-AppendageEndJointCFrame.Position) * AppendageEndJointCFrame) * RightHand:WaitForChild("RightWristRigAttachment").CFrame:Inverse()
	
	--Return the part CFrames.
	return UpperLimbCFrame,LowerLimbCFrame,AppendageEndCFrame
end
6 Likes