So you've got a procedural locomotion solver. It moves the feet, adjusts the hips, looks decent on flat ground. But then the character steps on a rock, a slope, a stair — and the foot just clips through, or slides unnaturally. The problem? Your solver probably ignores contact normals. That's the surface angle under the foot. If you don't account for it, the solver thinks the ground is always flat. And that breaks immersion fast.
This article is for people who've built or are building their own procedural foot placement — maybe in Unity, Unreal, or a custom engine. You already know IK basics. You've seen foot sliding. But fixing it means touching the solver's core logic. We'll walk through what goes wrong, what you need, the actual steps to fix it, tool-specific gotchas, variations for different terrains, and debugging when it still fails. No fluff. No 'major shift' promises. Just a tired engineer's notes on getting contact normals right.
Who Needs This and What Goes Wrong Without It
The 'Ice Skating' Problem on Slopes
You know the look. Character shuffles sideways on a 30-degree hill, feet sliding like hockey pucks on wet glass. That's what happens when your locomotion solver treats every surface as a flat plane. The solver plants the foot, assumes the contact normal points straight up, and then wonders why the character drifts downhill. I have seen studios spend three weeks polishing foot IK weights only to realize the entire slope response was hallucinated. The fix is not more spring stiffness—it's feeding the solver the actual surface direction.
Foot Penetration Into Angled Surfaces
Worse than sliding is clipping. Your character stands on a 45-degree rock face, looks perfectly planted from the front, but from the side? Half the foot mesh is inside the geometry. The root cause is simple: contact normals dictate where the foot should stop, but if your solver only uses vertical raycasts, it treats the surface as a flat floor tilted by magic. The foot lowers until the ray hits something, then stops—but the ankle joint twists to match a phantom orientation. The odd part is—this bug survives playtests for months because nobody orbits the camera during climbing sections. Until someone does. Then the QA ticket reads "foot phase-shifts into wall" and the team loses a day reproducing it.
Hip Sway Artifacts From Ignoring Normals
The sneakiest failure mode is the hip sway. No sliding, no clipping—just a weird wobble in the pelvis every third step on uneven terrain. Your character looks drunk on cobblestones. The mechanism: the solver adjusts foot positions using ground height alone, so the hip IK solver guesses the rest of the chain based on a false floor orientation. Each step micro-corrects, the pelvis oscillates, and the result is a nausea-inducing jitter that no amount of low-pass filtering fixes. Why? Because you're filtering a signal that was wrong at source.
'We had a character that looked perfectly fine on flat ground, but the moment we put them on a rocky outcrop, the hips started doing the hula. Took us a week to realize we'd never touched the contact normals.'
— Lead technical animator, AAA open-world title, 2022
That's the real cost. Not the time to implement normals—the week you burn pretending the problem is somewhere else. Most teams skip this because their ground planes are flat in the first demo, then the level designer adds a 40-degree ramp and suddenly the animation team is chasing ghosts. The catch is: you can't fake it with raycast offsets or slerp smoothing. Those mask the symptom, not the cause. The solver needs the actual surface normal per foot, per frame, applied at the constraint stage. Skip that and your characters will slide, clip, or sway—pick two.
Who needs this? Anyone whose characters walk on surfaces that are not a perfectly horizontal plane. That's your first-person parkour game, your quadruped robot simulation, your survival game with mountainous terrain. If your test level includes a single sloped rock, you need contact normals. If it doesn't yet, it will—and by then the fix is twice as expensive.
Odd bit about animation: the dull step fails first.
Odd bit about animation: the dull step fails first.
Prerequisites: What You Should Have Before Touching Normals
Basic IK Setup — Two-Bone or FABRIK
You need a solver that actually works before you touch normals. Not a half-finished limb flailing at random targets — a reliable IK chain that settles cleanly on flat ground. Two-bone works fine for humanoid legs; FABRIK handles snakes, tentacles, or anything with more joints. The catch is most tutorials stop at getting the chain to reach a point. That point sits on a flat plane, often the world origin, and everything looks great until your character stands on a 45° slope. I have seen teams spend a week debugging foot-slide because their solver converged but their contact normal was never part of the math. Wrong order. Fix the solver first — and make sure it supports a target rotation, not just a target position. Without that, you can't tilt the foot to match the ground, and your character will clip through the surface on every slanted step. That hurts.
Raycasting for Ground Detection
Single ray straight down works on flat floors. On stairs, rubble, or uneven terrain it returns garbage — the foot either floats or stabs through because one point can't capture the local slope. You want a short ray fan: three to five rays spread across the foot's contact area, each maybe twenty centimeters long. The hit that matters is the one with the highest Y value, not the deepest one. Most teams skip this: they cast one ray, get a normal, and call it done. The result is a foot that tilts wildly on every bump, or worse, snaps upright when the ray misses a narrow edge. What usually breaks first is the ray origin — place it too high, and you miss ramps; place it too low, and the foot clips into ascents. The odd part is — you don't need perfect geometry detection. A cheap capsule cast with a normal output often beats a single precise ray because it filters noise. Choose based on your environment: structured corridors want sharp rays, open landscapes want sweeps.
'A ray is a promise. Three rays are a negotiation. Five rays is a committee that actually votes on what the ground thinks.'
— technical lead on a survival game that shipped with zero normal-related foot bugs
Coordinate Space Conventions — World vs. Local
Here is the trap: your raycast returns a normal in world space. Your IK solver expects local rotations. If you apply that world normal directly to the foot transform, the alignment breaks the moment the character rotates or the root bone twists. The fix is trivial after you see it happen once: convert the normal into the foot's parent space before feeding it to the solver. That means multiplying the inverse of the parent's world rotation matrix by the hit normal. Simple math. Yet I have debugged three separate projects where engineers skipped this step because "it looked fine on the test floor." The test floor was axis-aligned. The production level was a collapsing spiral staircase. Pure world-space normals on that staircase produced a foot that pointed east while the character faced north — a twist that no IK chain could resolve. The result was a violent pop on every step. So before you integrate normals, verify your transform tree: which bone owns the foot, what space does your solver expect, and can your debug draw show both the world normal (as a line) and the local-aligned version (as a cone)? If those two arrows point different directions, you have work to do. Don't skip the debug visualization — that single overlay saves hours of guesswork.
One last thing: test on a moving platform. A rotating disc with 10° tilt will reveal every hidden assumption in your transform pipeline. If the foot slides or stutters on that disc, your world-to-local conversion is wrong. Fix that before you build the procedural solver on top. Not yet. That step comes next. But without these prerequisites, the core workflow in the next section will collapse on the very first slanted surface you throw at it.
Core Workflow: Integrating Contact Normals Step by Step
Raycast from Foot Position, Cache hit.normal
Start at the moment your solver asks “where is the ground?”
Shoot a ray straight down from the foot’s expected landing position — not from the hip, not from the character’s root. Why? Because a ray from the hip hits a slope’s upper edge and returns a normal that tilts the foot before it lands. Pre-landing wobble. I have seen that cost teams two days of taming. The raycast length should be your step height plus a small buffer — 0.2 units works for most human-scale characters. Cache hit.normal immediately after the cast. Don't recast it later; frame-delay introduces a one-frame pop that feels like a micro-stutter in locomotion. Store the normal alongside the foot’s target position. A struct works. A pair of floats works. What doesn't work is forgetting to reset the cached value when the foot lifts — stale normals on a stair edge create a diagonal foot that phases through the next step.
Rotate Foot Target to Align with Normal
Now you have a vector. Use it.
Rotate the foot’s local up axis to match hit.normal. In Unity, that's Quaternion.FromToRotation(Vector3.up, hit.normal). In Unreal, a similar MakeRotFromZ call. The catch: applying this rotation instantly produces a foot that snaps to the slope — looks like the character stepped on a banana peel then recovered. You need a Slerp with a rate between 8–12 radians per second. Lower than 6 and the foot drags across the slope before planting; higher than 15 and the snap returns. I once watched an animator tweak that value for an hour and a half. The correct number depends on your character’s step duration, not your framerate — delta-time multiplication keeps it framerate-independent. That said, don't Slerp the foot if the normal changes by more than 45 degrees inside one step. That scenario usually means a thin object or a capsule collision edge, and blending into it produces a contorted ankle. Instead, hold the previous normal and wait for the next foot plant.
Honestly — most animation posts skip this.
Honestly — most animation posts skip this.
Adjust Hip Height Based on Normal Angle
Most teams skip this: the hip stays at a fixed height regardless of the floor tilt. Wrong order.
When the character stands on a 30-degree slope, the foot that lands uphill sits higher relative to the hip than the foot downhill. If you only rotate the feet, the uphill leg appears shorter — a visual crouch. The fix is simple: compute the slope’s effect on the hip center. Take the average of both feet’s heights projected along the character’s local up vector. Subtract the flat-ground height offset. The difference becomes a vertical hip offset that raises or lowers the entire pelvis. Apply it before the foot rotation constraints. The tricky bit is blending — if the character transitions from flat ground to a slope over two steps, the hip should not jump. Smooth that offset with a spring dampener (critical damping ratio around 1.2). Too much damping and the hip lags behind the slope change, making the character look drunk. Too little and the hip bounces at every step change. What usually breaks first is the hip plunging when the character steps onto a downward slope — the average height drops, and the solver overcorrects. Clamp the hip offset to ±0.15 meters. That covers most traversal surfaces without creating a cartoon pogo effect.
Blend Between Flat and Slope Corrections
Blending is not binary. You don't snap between “flat mode” and “slope mode” — the foot will shear through geometry at the transition line.
— comment from a production solver pass that added 300 lines of interpolation logic.
Use the angle between the character’s forward direction and the steepest gradient of the floor as a blend weight. If the angle is near zero (walking directly uphill), weight the slope correction fully. If the angle is near 90 degrees (walking sideways on a gentle hill), reduce the correction to 40% — sideways traversals don't need the same hip lift because the leg length discrepancy is smaller. Store this blend weight per foot and per frame. Then cross-blend it with the previous frame’s weight using a lerp of 0.15 per frame. That prevents a one-frame stutter when the character steps from a flat tile onto a ramp edge. One more edge: stairs. Stair normals are mostly flat (the tread), but the solver will occasionally hit the riser normal, which is vertical. Don't blend into a vertical normal — it flips the foot into a wall-climbing pose. Instead, discard any normal whose dot product with Vector3.up is below 0.1. Cache the last valid normal. Your character walks up stairs, not up the wall. That distinction alone removed three bug reports from our QA backlog.
Tools, Setup, and Environment Realities
Unity: using Animancer or custom IK with RaycastHit.normal
Most Unity teams I see treat contact normals as an afterthought. They hook a raycast into their foot-ik solver, grab the terrain angle, and call it done. The odd part is—they never feed that angle back into the procedural solver that places the foot. Without that feed, the character slides up slopes like a soap bar on wet tile. In Animancer, the fix lives in a simple OnAnimatorIK callback. Pull RaycastHit.normal, transform it into the avatar's local space, and shift the foot target along that surface plane. Not glamorous. But without it, the solver treats every slope like flat ground — hip pop guaranteed. Custom IK setups are worse: one team cached the normal but never reoriented the toe bone, so the foot clipped through stairs. We fixed that by projecting the target point onto the triangle defined by three nearby normals. Overkill? On uneven rubble, it saved two hours of polish per level.
Unreal: Control Rig and foot placement node
Unreal's Control Rig ships with a foot-placement node, but it ignores grounded normals by default. The node pulls the impact point — not the surface orientation. That sounds fine until your character stands on a 30-degree rock face. The foot stays level, the ankle breaks the collision mesh, and the artist cries. The fix: insert a Get Hit Result Under Foot node, read the normal, then feed that into a Lerp (Quaternion) blended against the world up vector. The catch — that blend can't be aggressive. I have seen teams set blend weight to 0.8 on a slight slope; the character's torso pitched like a ship in a storm. Keep the blend under 0.4 unless the slope exceeds 20 degrees. One concrete anecdote: a studio porting a parkour prototype from Maya had the foot snap perfectly on a 45-degree wall, but the hip twist looked broken because the solver used the normal only on the last frame. They cached it one frame early — problem gone.
Performance: avoiding per-frame raycasts with caching
Raycasts every frame on every foot? That kills frame time on consoles. The trick is caching the contact normal for the duration the foot is planted. Store the normal from the previous frame's hit; when the foot stays below a speed threshold, reuse that cached value instead of casting again. Wrong order: cache before checking if the foot moved. That hurts — you sample an old normal on a new surface, the foot tilts wrong, and the seam blows out. The better pattern: run the raycast only when the foot lifts above a vertical velocity of 0.3 m/s, or when the distance from the cached hit point exceeds 10 cm. I have stripped 18 raycasts per second per foot this way — on a four-footed creature, that's 72 casts saved. Performance gains come from knowing when not to sample.
— applied to a creature rig with eight contact points; saved 14% of the animation budget.
Flag this for animation: shortcuts cost a day.
Flag this for animation: shortcuts cost a day.
One more pitfall: Unity's RaycastCommand for batch jobs. Don't schedule a batch if any foot is mid-air. The command runs on the job thread, but the solver needs the normal in the same frame to adjust the IK target. A one-frame lag on the normal reintroduces the original problem — the solver works on stale data. Instead, use a single synchronous raycast per foot that does change, then cache the result for 2–3 frames. That buys you headroom on mobile without re-inventing the job system.
Variations for Different Constraints
Steep slopes: limit foot rotation to avoid ankle twist
The moment your character hits a 45-degree incline, the contact normal goes vertical-ish — and your solver happily rotates the foot to match. That hurts. We fixed this by clamping the foot's up-vector against the character's root up, using a max tilt angle derived from the animation set. Something like float slopeDot = dot(contactNormal, characterUp); if (slopeDot < 0.707f) { footRotation = slerp(footRotation, neutralRotation, 0.3f); }. Without that clamp, the ankle inverts past 50 degrees and the knee buckles — one frame of bad data becomes a ragdoll cascade. The trade-off: a clamped foot loses ground contact on the toe or heel, so you need a secondary IK pass that nudges the heel down when the ball of the foot lifts. I have seen teams skip this and blame the solver; it's not the solver, it's the missing limit.
“A foot that twists 70 degrees to match the ground is a foot that snaps the tibia — clamp the normal before it reaches the ankle.”
— technical animator, procedural locomotion team
Stairs: snap foot to edge with normal projection
Stairs wreck smooth motion systems. The contact normal on a stair tread points mostly up, but the edge between steps produces a vector pointing toward the riser — that pulls the foot into the step's vertical face, causing a clip. The fix: project the foot's landing point onto the nearest stair edge using a signed distance field baked from the stair mesh. In practice, we run float edgeDist = SignedDistanceToStairEdge(footWorldPos); if (abs(edgeDist) < 0.05f) { footPos = ProjectOnEdge(footWorldPos); }. This snaps the foot forward onto the tread, not into the riser. The odd part is — this works even when the stair normal is completely flat (0,1,0) because we ignore it entirely for the snap. The catch: on winding stairs or spiral cases, the edge field breaks down; you fall back to a simple raycast from the hip with a max step height of 0.3 units. Most teams skip this: they let the foot float over the stair or slide up the riser, which looks like the character is ice-skating on marble.
Moving platforms: offset raycast origin for dynamic surfaces
A platform that rises while the foot is mid-swing — the raycast from the hip misses the new surface because the foot's previous world position is now inside the platform. What usually breaks first is the foot penetrating the platform top, then the solver pushes it downward through the floor. We offset the raycast origin by the platform's velocity multiplied by the foot's swing duration: Vector3 rayOrigin = hipPos + platformVelocity * swingTime * 0.5f;. Without this, the character rides the platform for one frame then drops — a jitter that feels like a network lag spike. The pitfall: double-counting the offset if the foot is already planted. We gate it: only apply the offset when the foot's vertical speed exceeds 0.5 m/s (i.e., during swing). That said, moving platforms with angular velocity (tilting elevators) require a separate torque compensation — we clamp the foot's angular velocity to the platform's angular velocity plus a 30-degree dead zone. I have debugged this with OnDrawGizmos: draw the offset raycast in red and the standard raycast in green; when the red line hits and the green line misses, you know the fix works.
Pitfalls, Debugging, and What to Check When It Fails
Gimbal lock when rotating foot to extreme normals
You clamp the foot orientation to match a 75-degree rock face, and suddenly the ankle flips 180 degrees through the floor. That's gimbal lock — not a glitch, but what happens when Euler angles collapse a degree of freedom. Most teams skip this: they feed a quaternion from LookRotation(raycast.normal) directly into the IK solver. Wrong order. The solver interprets the intermediate axis as a twist, and the foot spins like a helicopter blade. I have fixed this exact bug twice. The fix is boring but reliable: decompose the rotation into a swing component (the normal direction) and a twist component (the forward alignment). Use Quaternion.FromToRotation for the swing, then apply a separate twist limit. If you can't disambiguate, clamp the swing angle to 45 degrees first. That alone kills 90% of the flips.
Jitter from noisy raycast normals on uneven terrain
A single pebble under the character foot returns a normal that points sideways. The solver reacts immediately — and now the body rocks every frame on what should be flat ground. The catch is that raycast normals on polygon edges are notoriously unstable. One frame you hit the top face, next frame you clip the side edge. The result is a foot that vibrates instead of plants. We fixed this by storing the last three valid normals and blending them with a lerp that favors the current value only when the angle change exceeds 12 degrees. That smooths the noise without adding a visible delay. Visualize the normals as debug lines — if the end point wiggles more than a few centimeters while the foot is stationary, your lerp alpha is too aggressive or too late.
Foot sliding backward on steep slopes due to misaligned IK targets
On a 40-degree hill, the foot contacts the ground, but the IK target projects the hip forward while the toe stays planted. The foot slides. That's not a foot-plant problem — it's a target placement problem. The ankle position should follow the contact normal’s tangent plane, not the world horizontal plane. Most solvers compute the IK target as hipPosition + strideVector and then drop it vertically. On slopes, that vertical drop pushes the target downhill. The foot plants correctly, but the target already moved. Check this by drawing the target position as a sphere. If the sphere drifts more than 2 cm from the contact point while the foot is in stance, recalculate the target using the normal’s projection onto the slope surface. A simple test: move the character to a 30-degree ramp, pause, and watch the foot markers. If they creep downhill frame by frame, your projection is wrong.
'The foot has no idea it's on a mountain. It only knows where you told it to stand — and you told it wrong.'
— muttered by a colleague after three hours of debugging a slide that turned out to be a single misplaced Vector3.ProjectOnPlane call
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!