Prototype R15 Animation IKControl Footplant

I have seen multiple posts 1, 2, and 3 on people wanting to know how recreate Roblox’s IK Control demo.

So I spent 1 hour to make a quick and dirty prototype. Enjoy.

Placefile:
IK Control requires basic setup from hip to foot. This place file should make it convenient to test out the code.

AnimationFootPlant.rbxl (134.0 KB)

Code
local model = script.Parent

--local animationID = "http://www.roblox.com/asset/?id=507777826"
local animationID = "http://www.roblox.com/asset/?id=507767714" --run
local animation = Instance.new("Animation")

animation.AnimationId = animationID

local animator = model.Humanoid.Animator

local walkAnim = animator:LoadAnimation(animation)
walkAnim:AdjustSpeed(0.5)
walkAnim:Play()
--walkAnim:AdjustSpeed(0.25)

local RunService = game:GetService("RunService")

local target = model.HumanoidRootPart.IKTarget1

local IKControl = model.Humanoid.IKControlRightLeg
local chainRoot : Motor6D = IKControl.ChainRoot
local endEffector = IKControl.EndEffector

local RightHip = model:FindFirstChild("RightHip", true)
local RightKnee = model:FindFirstChild("RightKnee", true)
local RightAnkle = model:FindFirstChild("RightAnkle", true)

--motor.part0*c0*transform = motor.Part1*C1

local rayParams = RaycastParams.new()
rayParams.FilterDescendantsInstances= {model}


print(IKControl.SmoothTime)

IKControl.SmoothTime = 0

----motor.part0*c0*transform = motor.Part1*C1
--Solve for Part1
--motor.part0*c0*transform*C1:Inverse() = motor.Part1
--Repeat all the way down to calculate end effector position where it should be without IKControl effecting it
RunService.Stepped:Connect(function(dt)
	
	local rightHipJointWorldCFrame = RightHip.Part0.CFrame*RightHip.C0
	local rightkneePart1CF = rightHipJointWorldCFrame*RightHip.Transform*RightHip.C1:Inverse()
	local rightFootPart1CF = rightkneePart1CF*RightKnee.C0*RightKnee.Transform*RightKnee.C1:Inverse()
	local test = rightFootPart1CF*RightAnkle.C0*RightAnkle.Transform*RightAnkle.C1:Inverse()
	
	local goalTargetWorldCF = test*endEffector.CFrame
	
	local direction = goalTargetWorldCF.Position - rightHipJointWorldCFrame.Position
	local raycastResult = workspace:Raycast(rightHipJointWorldCFrame.Position, direction, rayParams)
	
	if raycastResult then
		goalTargetWorldCF = goalTargetWorldCF.Rotation + raycastResult.Position
		target.WorldCFrame = goalTargetWorldCF
		IKControl.Enabled = true
	else
		
		IKControl.Enabled = false

	end
	
	--target.WorldCFrame = goalTargetWorldCF
end)


--Repeat for Left Leg
local target2 = model.HumanoidRootPart.IKTarget2

local IKControl2 = model.Humanoid.IKControlLeftLeg
local chainRoot : Motor6D = IKControl2.ChainRoot
local endEffector = IKControl2.EndEffector

local LeftHip = model:FindFirstChild("LeftHip", true)
local LeftKnee = model:FindFirstChild("LeftKnee", true)
local LeftAnkle = model:FindFirstChild("LeftAnkle", true)

--motor.part0*c0*transform = motor.Part1*C1

local rayParams = RaycastParams.new()
rayParams.FilterDescendantsInstances= {model}


print(IKControl2.SmoothTime)

IKControl2.SmoothTime = 0

target2.Visible = true
target.Visible = true

RunService.Stepped:Connect(function(dt)

	local leftHipJointWorldCFrame = LeftHip.Part0.CFrame*LeftHip.C0
	local leftkneePart1CF = leftHipJointWorldCFrame*LeftHip.Transform*LeftHip.C1:Inverse()
	local leftFootPart1CF = leftkneePart1CF*LeftKnee.C0*LeftKnee.Transform*LeftKnee.C1:Inverse()
	local test = leftFootPart1CF*LeftAnkle.C0*LeftAnkle.Transform*LeftAnkle.C1:Inverse()

	local goalTargetWorldCF = test*endEffector.CFrame

	local direction = goalTargetWorldCF.Position - leftHipJointWorldCFrame.Position
	local raycastResult = workspace:Raycast(leftHipJointWorldCFrame.Position, direction, rayParams)

	if raycastResult then
		local goalTargetWorldCF = CFrame.new() + raycastResult.Position
		target2.WorldCFrame = goalTargetWorldCF
		IKControl2.Enabled = true
	else
		IKControl2.Enabled = false
	end

	--target.WorldCFrame = goalTargetWorldCF
end)
47 Likes

Thank you so much this really helps me understand how it works more

1 Like

Currently getting “Part0 is not a valid member of MeshPart” and such. I’m pretty stumped on how to use this resource on a custom model/rig.

1 Like

robloxapp-20240318-2149246.wmv (2.1 MB)
Having some trouble… Do I need to relink the joints to the IK control?

Is there any way you can create a new file or edit the ```` code in your post so that there’s comments that explain each line? I’m really confused what all this means

Thanks

Sure,

The purpose of this code portion is to calculate the world position of the foot starting from the base hip of the character including the effects of animation CFrames which is the .Transform of a motor6D.

The reason for not just doing foot.Position is that this is the IK target if if you just use foot.Position the target position will stay the same due to the IK and will probably cause a weird cyclical effect bug and will not include the effects of movement ( .Transform and hip movement)

The formula for welds is here:

For a motor6D the only thing different is that in the formula .Transform is added based on which part is closest to the root part which is usually part0:

Part0.CFrame * C0 * Motor6D.Transform = part1.CFrame * C1 

You can visualize it as position vectors starting from world origin (0,0,0)

It starts from world origin which is the red part, goes to the hip position (RightHip.Part0.CFrame), moves down to the right hip joint where the leg roates from (C0 CFrame position vector), adds an extra animation CFrame (RightHip.Transform), moves down to the ankle (RightHip.C1:Inverse() it is inverse because the direction of this position vector starts from the ankle towards the hip so we need to reverse this direction to go from hip to ankle instead). This continues till it reaches the foot.

That should be the jist of it, I’ve also tried explaining it in my CFrame tutorial.

2 Likes

I was trying to replicate what you did in the roblox place that you shared in the topic but when trying to create this in a new file it does not work. It seems to not be getting the right ankle position correctly even tho the loop for this is inside a .Stepped loop. Do you know any other ways to get the position of a joint from animation or a way to fix this?

Hey, thanks for sharing this.

Yesterday I was kinda bored so I turned this into a little module so I can use it on players/npc’s (it’s by no means perfect, but it gets the job done), so here it is in case it helps anyone:

Preview:


File:
GlintStep.rbxl (97.0 KB)

Code Preview:

--!strict

--[[
@class GlintStep
Utility class that handles character foot planting.

@constructor new
Creates a new GlintStep instance for a given character.
@param Character Model - The character model.
@param Active boolean? - Whether foot planting should start active.
@return Glint - The new GlintStep instance.

@field Active boolean
Indicates whether foot planting is currently active.

@method Toggle
Activates or deactivates foot planting.
@param Active boolean - The new state of foot planting.
@return nil

@method Destroy
Cleans up the instance and removes all associated connections.
@return nil
--]]

----- Services -----

local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

----- Variables -----

local Code = ReplicatedStorage:WaitForChild("Code")
local Shared = Code.Shared
local Resources = Shared.Resources

local Types = require(script.Parent.Types)
local Trove = require(Resources.RbxUtil.Trove)

local MAX_SLOPE_ANGLE = math.rad(45) --Maximum angle the foot planting will apply to.
local TARGET_TRESHOLD_STANDING = 0.1 --Distance between the current point and the previous required before an update.
local TARGET_TRESHOLD_MOVING = 0.05
local SMOOTH_TIME_STANDING = 0.01 --Transition time to be applied to the footplanting as it aligns to the floor.
local SMOOTH_TIME_MOVING = 0.001
local WEIGHT_STANDING = 0.75 --Weight between 0 and 1 to be applied to the footplanting as it aligns to the floor.
local WEIGHT_MOVING = 0.45
local HEIGHT_STANDING_MULTIPLIER = 1 --Maximum distance the footplanting raycast will reach when aligning to the floor based on the character's height.
local HEIGHT_MOVING_MULTIPLIER = 0.4

----- Module -----

local GlintStep = {}
GlintStep.__index = GlintStep

--- Types ---

export type Glint = {
	Active: boolean,
	Toggle: (self: Glint, Active: boolean) -> (),
	Destroy: (self: Glint) -> ()
}
type InternalGlint = Glint & {
	Character: Types.Character,
	Trove: Trove.Trove,
	RayParams: RaycastParams,
	LeftIKControl: IKControl,
	RightIKControl: IKControl,
	LastTargetPositions: {
		Left: Vector3,
		Right: Vector3
	}
}

--- Methods ---

-- Constructor --

function GlintStep.new(Character: Types.Character, Active: boolean?): Glint
	assert(Character, "Character passed is nil.")
	local self: InternalGlint = {} :: any
	
	--Properties
	self.Character = Character
	self.Trove = Trove.new()
	self.Active = false
	self.LastTargetPositions = {
		Left = Vector3.zero,
		Right = Vector3.zero
	}
	
	--Ray Params
	self.RayParams = RaycastParams.new()
	self.RayParams.FilterDescendantsInstances = {self.Character :: any}
	self.RayParams.FilterType = Enum.RaycastFilterType.Exclude
	
	--IK Controls
	self.LeftIKControl = CreateIKControl(Character, "Left")
	self.RightIKControl = CreateIKControl(Character, "Right")
	
	--Handle cleanup
	self.Trove:Add(self.Character:GetPropertyChangedSignal("Parent"):Connect(function(): ()
		if not Character.Parent then
			GlintStep.Destroy(self)
		end
	end) :: RBXScriptConnection)
	
	--Toggle if requested
	if Active then
		GlintStep.Toggle(self, Active)
	end
	
	return setmetatable(self, GlintStep) :: any
end

-- Toggle --

function GlintStep.Toggle(self: InternalGlint, Active: boolean): ()
	if Active ~= self.Active then
		self.Active = Active
		
		--Toggle
		if Active then
			
			--Update IKControl
			local function UpdateIKControl(Side: "Left" | "Right"): ()
				local IKControl: IKControl = self[Side.."IKControl"]
				local Hip: Motor6D = self.Character[Side.."UpperLeg"][Side.."Hip"]
				local Knee: Motor6D = self.Character[Side.."LowerLeg"][Side.."Knee"]
				local Ankle: Motor6D = self.Character[Side.."Foot"][Side.."Ankle"]

				local HipJointWorldCFrame = assert(Hip.Part0).CFrame * Hip.C0
				local KneePart1CF = HipJointWorldCFrame * Hip.Transform * Hip.C1:Inverse()
				local FootPart1CF = KneePart1CF * Knee.C0 * Knee.Transform * Knee.C1:Inverse()
				local Reverse = FootPart1CF * Ankle.C0 * Ankle.Transform * Ankle.C1:Inverse()
				local GoalTargetWorldCF = Reverse * assert(IKControl.EndEffector :: Attachment).CFrame

				local Moving = self.Character.Humanoid.MoveDirection.Magnitude > 0
				local Height = (0.5 * self.Character.HumanoidRootPart.Size.Y) + self.Character.Humanoid.HipHeight

				local Treshold = Moving and TARGET_TRESHOLD_MOVING or TARGET_TRESHOLD_STANDING
				local RayDistance = Moving and (Height / HEIGHT_MOVING_MULTIPLIER) or (Height / HEIGHT_STANDING_MULTIPLIER)

				IKControl.Weight = Moving and WEIGHT_MOVING or WEIGHT_STANDING
				IKControl.SmoothTime = Moving and SMOOTH_TIME_MOVING or SMOOTH_TIME_STANDING

				--Raycast
				local Origin = HipJointWorldCFrame.Position + Vector3.new(0, self.Character[Side.."Foot"].Size, 0)
				local RayDist = Moving and self.Character.Humanoid.HipHeight or self.Character.Humanoid.HipHeight * 3
				local Direction = (GoalTargetWorldCF.Position - Origin).Unit * RayDist
				local Result = workspace:Raycast(Origin, Direction, self.RayParams)
				if Result then
					local SlopeAngle = math.acos(Result.Normal:Dot(Vector3.yAxis))
					local LastPosition: Vector3 = (self.LastTargetPositions :: any)[Side]
					if (SlopeAngle <= MAX_SLOPE_ANGLE) and ((Result.Position - LastPosition).Magnitude > Treshold) then
						(self.LastTargetPositions :: any)[Side] = Result.Position

						local RightVector = self.Character.HumanoidRootPart.CFrame.LookVector:Cross(Result.Normal).Unit
						local ForwardVector = Result.Normal:Cross(RightVector).Unit
						local GoalTargetWorldCF = CFrame.fromMatrix(Result.Position, RightVector, Result.Normal, ForwardVector)

						assert(IKControl.Target :: Attachment).WorldCFrame = GoalTargetWorldCF
					end
					IKControl.Enabled = true
				else
					IKControl.Enabled = false
				end
			end
			
			--Run
			self.Trove:Add(RunService.PreRender:Connect(function(dt: number)
				if self.Character.Humanoid.Health > 0 then
					UpdateIKControl("Left")
					UpdateIKControl("Right")
				end
			end))
			
		else --Cleanup
			self.Trove:Clean()
		end
	end
end

-- Destroy --

function GlintStep.Destroy(self: InternalGlint): ()
	self:Toggle(false)
	task.defer(function()
		self.Trove:Destroy()
		setmetatable(self, nil)
		table.clear(self :: {any})
		table.freeze(self :: {any})
	end)
end

--- Internal Methods ---

-- CreateIKControl --

function CreateIKControl(Character: Types.Character, Side: ("Left" | "Right")): IKControl
	local IKTarget = Instance.new("Attachment")
	IKTarget.Name = Side.."IKTarget"
	IKTarget.WorldCFrame = Character[Side.."Hand"].CFrame + Vector3.new(0, -0.75, 0)
	IKTarget.Parent = Character.HumanoidRootPart
	
	local IKControl = Instance.new("IKControl")
	IKControl.Name = Side.."IKControl"
	IKControl.Type = Enum.IKControlType.Transform
	IKControl.EndEffector = Character[Side.."Foot"][Side.."FootAttachment"]
	IKControl.Target = IKTarget
	IKControl.ChainRoot = Character[Side.."UpperLeg"][Side.."HipRigAttachment"]
	IKControl.Parent = Character.Humanoid
	
	return IKControl
end

return {
	new = GlintStep.new :: (Character: Model, Active: boolean?) -> Glint
}

You can probs get better results by tweaking the values and the raycast, but this was alright enough for me.


Edit: I’ve also added an npc example so you can test the module on npc’s (as you can see it looks kind of rough on some rigs atm, so def play around with the weight value during movement and such if you plan to use this):


Edit 2: I’ve played around with this a tiny bit more and I think I improved it slightly?

  • It now uses the Avatar Joint Upgrade (in fact, try it with IsKinematic set to false, it should still work).
  • Shapecasts are used for surface detection.
  • IKConstraint’s weight is now lerped so the thing is less stuttery.
  • I’ve added a “foot lock” so the foot doesn’t move around when you idle, dance and such (not perfect, it uses a torso/foot threshold, but it usually works).

It doesn’t seem to beheave the same between the player and the example rig I previously made, but the joint upgrade isn’t applied to general rigs yet, so it could just be that I applied the constraints to mine wrong, you’ll have to play around with your avatars and the values to figure what fits the best.

Preview:

Code:
GlintStep 1.1.rbxl (70.5 KB)

5 Likes