In modern open-world, survival, and tactical action games on Roblox—such as Deepwoken, Frontlines, and Dead Ahead—visual immersion is instantly broken when characters walk across uneven terrain. Default R15 and R6 animations assume flat, horizontal ground surfaces; on stairs, rocky inclines, or steep hillsides, one foot hovers awkwardly in mid-air while the other clips through solid rock.
To achieve lifelike locomotion, top developers implement procedural Inverse Kinematics (IK) foot placement. In this comprehensive technical guide, we engineer a production-ready two-bone analytical IK solver in Luau. We derive the law of cosines joint angles, cast precision downward raycasts from hip attachments, calculate dynamic pelvis drops to prevent leg overextension, and align ankle rotations to terrain surface normals.
1. Why Default Humanoids Hover: The Case for Procedural Foot IK
Default Roblox animation blending cannot account for runtime geometric ground variations:
- Rigid Keyframe Animation: Keyframe sequences bake fixed limb trajectories relative to the pelvis. When the ground rises or falls beneath the character, the animation plays unchanged.
- Visual Disconnect on Slopes: Characters appear to stand on the edge of cliffs or slopes with one leg suspended over open air, completely destroying physical credibility.
- Stair Treading Failures: Walking up stairs causes characters to skate over stair risers rather than planting each sole cleanly on sequential steps.
- Procedural IK Solution: By intercepting Motor6D joint transforms at runtime, procedural IK dynamically adjusts hip, knee, and ankle rotations to adapt to whatever terrain geometry is beneath the avatar.
2. The Mathematical Foundation: Analytical Two-Bone Law of Cosines
Two-bone leg inverse kinematics (UpperLeg -> LowerLeg -> Foot) is solved analytically using the Law of Cosines without expensive iterative FABRIK solvers:
- Triangle Definitions: Thigh length L1, calf length L2, and target distance D = (FootTarget - HipOrigin).Magnitude.
- Hypotenuse Clamping: To prevent mathematical domain errors (NaN from math.acos), distance D is clamped: D = math.clamp(D, 0.01, L1 + L2 - 0.001).
- Hip Joint Angle (alpha): math.acos((L1^2 + D^2 - L2^2) / (2 * L1 * D)), giving the angular pitch offset of the thigh toward the foot target.
- Knee Joint Angle (beta): math.acos((L1^2 + L2^2 - D^2) / (2 * L1 * L2)), giving the interior bend angle of the knee.
- Coordinate Frame Rotation: The hip coordinate frame is aligned to the plane formed by Hip, Knee, and Foot target points, applying alpha and beta rotations.
3. Complete Procedural Foot Placement Luau Implementation
Below is a complete, modular Luau controller running on RunService.RenderStepped for buttery-smooth 60+ FPS foot adaptation:
- Dual Raycast Sampling: Casts downward rays from Left and Right Hip attachments to find exact ground contact positions and surface normals.
- Pelvic Drop Calculation: Drops the RootJoint Motor6D downward by the lowest foot deficit to ensure the higher leg bends while the lower leg can fully reach the ground.
- Ankle Normal Alignment: Rotates the foot joint CFrame to match the surface normal of the terrain, preventing toes from digging into uphill slopes.
--!strict
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")
local FootIK = {}
FootIK.__index = FootIK
export type LegRig = {
HipMotor: Motor6D,
KneeMotor: Motor6D,
AnkleMotor: Motor6D,
HipAttachment: Attachment,
ThighLength: number,
CalfLength: number,
CurrentFootOffset: number,
}
export type CharacterIK = {
Character: Model,
RootJoint: Motor6D,
LeftLeg: LegRig,
RightLeg: LegRig,
RayParams: RaycastParams,
HipDropOffset: number,
}
local function SolveTwoBone(origin: CFrame, targetPos: Vector3, l1: number, l2: number): (CFrame, CFrame)
local localized = origin:PointToObjectSpace(targetPos)
local dist = localized.Magnitude
dist = math.clamp(dist, 0.05, (l1 + l2) - 0.001)
local cosAlpha = (l1 * l1 + dist * dist - l2 * l2) / (2 * l1 * dist)
local alpha = math.acos(math.clamp(cosAlpha, -1, 1))
local cosBeta = (l1 * l1 + l2 * l2 - dist * dist) / (2 * l1 * l2)
local beta = math.acos(math.clamp(cosBeta, -1, 1))
local planeRot = CFrame.lookAt(Vector3.zero, localized)
local hipRot = planeRot * CFrame.Angles(alpha, 0, 0)
local kneeRot = CFrame.Angles(-(math.pi - beta), 0, 0)
return hipRot, kneeRot
end
function FootIK.Step(ik: CharacterIK, dt: number)
local char = ik.Character
local rootPart = char.PrimaryPart
if not rootPart then return end
local rayDown = Vector3.new(0, -5, 0)
local leftOrigin = ik.LeftLeg.HipAttachment.WorldPosition
local rightOrigin = ik.RightLeg.HipAttachment.WorldPosition
local leftHit = Workspace:Raycast(leftOrigin, rayDown, ik.RayParams)
local rightHit = Workspace:Raycast(rightOrigin, rayDown, ik.RayParams)
local leftTarget = leftHit and leftHit.Position or (leftOrigin + Vector3.new(0, -3.2, 0))
local rightTarget = rightHit and rightHit.Position or (rightOrigin + Vector3.new(0, -3.2, 0))
-- Calculate pelvis drop
local leftDeficit = leftOrigin.Y - leftTarget.Y
local rightDeficit = rightOrigin.Y - rightTarget.Y
local maxDeficit = math.max(leftDeficit, rightDeficit)
local targetHipDrop = math.clamp(maxDeficit - 3.2, 0, 1.8)
ik.HipDropOffset = ik.HipDropOffset + (targetHipDrop - ik.HipDropOffset) * math.clamp(dt * 15, 0, 1)
ik.RootJoint.Transform = CFrame.new(0, -ik.HipDropOffset, 0)
-- Solve Left Leg
local leftHipCF = ik.LeftLeg.HipMotor.Part0.CFrame * ik.LeftLeg.HipMotor.C0
local lHipRot, lKneeRot = SolveTwoBone(leftHipCF, leftTarget, ik.LeftLeg.ThighLength, ik.LeftLeg.CalfLength)
ik.LeftLeg.HipMotor.Transform = lHipRot
ik.LeftLeg.KneeMotor.Transform = lKneeRot
-- Solve Right Leg
local rightHipCF = ik.RightLeg.HipMotor.Part0.CFrame * ik.RightLeg.HipMotor.C0
local rHipRot, rKneeRot = SolveTwoBone(rightHipCF, rightTarget, ik.RightLeg.ThighLength, ik.RightLeg.CalfLength)
ik.RightLeg.HipMotor.Transform = rHipRot
ik.RightLeg.KneeMotor.Transform = rKneeRot
end
return FootIK
4. Pelvic Hip Drop & Ankle Surface Normal Alignment
When a character stands on a slope, solving leg IK alone causes the downhill leg to stretch to its limit while the uphill leg bends unnaturally:
- Pelvic Drop Compensation: Lowering the character's root pelvis (RootJoint Motor6D offset) brings the hip socket closer to the lower foot, allowing both knees to maintain natural flex.
- Lerp Damping: Pelvic height adjustments are smoothed using exponential interpolation (target + (dest - target) * dt * speed) to avoid camera bobbing and visual popping on sharp step edges.
- Ankle Surface Conformance: Calculating the cross product of character LookVector and ground hit normal yields a rotational matrix that tilts the foot part flat against steep terrain.
- Toe Raycasting for Complex Ramps: Placing secondary toe raycasts to split foot rotation between heel and toe on extreme rocky inclines.
5. Performance Optimization, Frustum Culling & Multiplayer LOD
Running trigonometric solvers on hundreds of player avatars simultaneously can bottleneck the CPU without smart optimization:
- Camera Frustum Culling: Using Camera:WorldToViewportPoint() to skip IK calculations for avatars positioned behind the camera.
- Distance-Based LOD Tiering: Updating IK at 60Hz for avatars within 35 studs, 30Hz between 35-70 studs, and disabling IK entirely beyond 70 studs.
- State-Based Early Exits: Disabling foot raycasts immediately when Humanoid:GetState() is Freefall, Swimming, Seated, or Ragdoll.
- Pure Client-Side Simulation: Keeping all IK calculations local to client RenderStepped to ensure 0 bytes of network replication overhead.
Frequently Asked Questions
Why use analytical two-bone trigonometry instead of Roblox's built-in IKControl?
While Roblox IKControl is convenient, custom analytical two-bone IK gives developers direct programmatic control over pelvis hip drop damping, exact ankle surface normal alignment, custom raycast filters, and distance-based frustum LOD optimizations.
What causes leg shaking or jitter on stairs?
Jitter occurs when raycasts hit alternating step edges and risers between consecutive frames. Implementing a moving average filter or exponential lerp on foot target positions completely eliminates stair stepping jitter.
How does pelvic drop affect character collision and jumping?
Because the pelvic drop is applied to the visual RootJoint Motor6D transform rather than the physical HumanoidRootPart position, physical collision capsules and jump velocities remain completely unaffected.