Class IkTargetSpaceDriver
- Namespace
- CodeSmile.AnyMotion.AnimateYourself.Spaces /
- Assembly
- ApiSource.dll
Space C: places the feet, and optionally the hands, at the positions the gait asked for, using an analytic two-bone IK solve.
public sealed class IkTargetSpaceDriver : IGaitDriverSpace
Remarks
The solver is written here rather than delegated to Final IK on purpose: Final IK and PuppetMaster are paid third-party assets and cannot be hard dependencies (O4). This gives a baseline that works with nothing installed; Final IK becomes an optional higher-quality substitute later.
Why this space exists at all: "footstep distance" is a position, and no rotational space can express a position portably. It is also the only one of the three that can express R10 — feet and hands sliding along colliders instead of penetrating them — because avoidance is a correction applied to a target point.
Torso, head and pelvis still come from the rotational part of the pose, so this space is a hybrid rather than a pure IK space. That is the expected shape of the final answer: positions for limb endpoints, rotations for everything else.
Properties
BindError
Reason Bind(Animator, RigMetrics, BindPose) failed, or null.
public string BindError { get; }
Property Value
- string
BoneStretchEnabled
O4 (knee pop) from the ik-pop-flip-prevention scout report: BoneStretch. Off leaves the solver bit-for-bit as it was before the stretch existed — see ResolveStretchFactor(float, float, float, float).
public bool BoneStretchEnabled { get; set; }
Property Value
- bool
Remarks
Set every frame by GaitDriver.ApplyNow from the gait's own parameters, the same pattern
as LookAtTarget, rather than threaded through GaitPose alongside
SoftReachStart. Both routes reach the same place; this one is a
property of how the chain is solved rather than of the pose being solved for, and it costs the
pose struct nothing. See Plan/backlog/engineering.md for the open question of whether
the two solver settings should end up on the same route.
DisplayName
Short label for the comparison table.
public string DisplayName { get; }
Property Value
- string
Kind
public GaitDriverSpaceKind Kind { get; }
Property Value
LastLeftSoleLift
L05: metres the last Apply raised the left foot's IK target by to put its sole on the ground. Negative means the foot was hovering and was lowered. 0 while the compensation is off or the foot is swinging. Reported for measurement and for the inspector, and read by nothing.
public float LastLeftSoleLift { get; }
Property Value
- float
LastRightSoleLift
L05: same as LastLeftSoleLift, for the right foot.
public float LastRightSoleLift { get; }
Property Value
- float
LastSoleEvaluations
L05: how many candidate poses the last Apply placed and measured. This counts the placements that set up the pose at rest and sample the surface, which are the pelvis plus one per stance foot, and every pose CodeSmile.AnyMotion.AnimateYourself.Spaces.SoleContactSolver placed after them, so it is the whole per-frame cost of the compensation and the number to watch when that cost is in question.
public int LastSoleEvaluations { get; }
Property Value
- int
LastSolePelvisRaise
L05: metres the last Apply raised the pelvis by. Never negative.
public float LastSolePelvisRaise { get; }
Property Value
- float
LastSoleSolveReachedContact
L05: false when the last Apply's search ran out of travel before a stance sole reached the surface. That is a real state, not an error: on a rig where bending the knee pitches the foot enough to cancel the rise, no lift inside CodeSmile.AnyMotion.AnimateYourself.Spaces.SoleContactSolver.MaxLiftMetres grounds the sole.
public bool LastSoleSolveReachedContact { get; }
Property Value
- bool
LastSolveClamped
Set true when a target was outside the leg's reach on the last Apply — or, L11 (arm layering), when an externally assigned hand target was not fully honoured (the reach distance was clamped, or the target fell outside the authored reach window and the arm fell back towards its ordinary swing).
public bool LastSolveClamped { get; }
Property Value
- bool
LeftHandTarget
L11 (arm layering): what the left hand reaches for via IK, blended against the position the
gait's own arm swing produces by HandTargetWeight, scaled down further
when the target falls outside the reach window — see ApplyArm(ArmAngles, LimbTarget, float, float, float, float, float, Transform, float, Transform, float, HumanBodyBones, HumanBodyBones, HumanBodyBones, float, float, float, bool). Null leaves the
arm on its ordinary swing. Set every frame by GaitDriver.ApplyNow, the same pattern as
LookAtTarget.
public Transform LeftHandTarget { get; set; }
Property Value
- Transform
LookAtTarget
L01: what the head/neck/chest chain turns towards. Null disables look-at — the tracked angle
eases back towards facing forward at the same TrackingSpeed
as any other target change, rather than snapping back. Set every frame by
GaitDriver.ApplyNow from its own serialized field, so assigning a new target at runtime
needs no rebind.
public Transform LookAtTarget { get; set; }
Property Value
- Transform
MaxBoneStretch
O4: MaxBoneStretch. Ignored while BoneStretchEnabled is false. Set every frame, same as that property.
public float MaxBoneStretch { get; set; }
Property Value
- float
RightHandTarget
L11: same as LeftHandTarget, for the right hand.
public Transform RightHandTarget { get; set; }
Property Value
- Transform
SoleGeometrySource
L05: what the reduced foot geometry was built from, or why there is none. Reads "not measured" until Bind(Animator, RigMetrics, BindPose) has run.
public string SoleGeometrySource { get; }
Property Value
- string
SoleGroundingEnabled
L05 (foot planting on uneven ground), sole-height compensation: SoleGrounding. Off leaves this space bit-for-bit as it was before the compensation existed — the whole block is skipped, not applied with a zero lift.
public bool SoleGroundingEnabled { get; set; }
Property Value
- bool
Remarks
Set every frame by GaitDriver.ApplyNow from the gait's own parameters, the same route
BoneStretchEnabled takes and for the same reason: it describes how the chain is
solved, not what pose it is solving for.
SolePelvisShare
L05: SolePelvisShare. Ignored while SoleGroundingEnabled is false. Set every frame, same as that property.
public float SolePelvisShare { get; set; }
Property Value
- float
SoleSolverIterations
L05: SoleSolverIterations, the ceiling on candidate poses per foot per pass. Ignored while SoleGroundingEnabled is false. Set every frame, same as the two properties above.
public int SoleSolverIterations { get; set; }
Property Value
- int
SupportsAnimationRigging
False when this space overwrites whatever Animation Rigging produced, making the two mutually exclusive.
public bool SupportsAnimationRigging { get; }
Property Value
- bool
SupportsLimbTargets
False when the space cannot place a hand or foot at a chosen point, which R10 requires.
public bool SupportsLimbTargets { get; }
Property Value
- bool
SupportsMasking
False when the space can only write the whole body, so partial-body jank is impossible.
public bool SupportsMasking { get; }
Property Value
- bool
Methods
Apply(in GaitPose, float)
Writes the pose to the rig. weight blends between the bind pose at 0 and
the full gait at 1, which the distance LOD tiers of R4 need in order to fade a tier in and out
rather than switch it.
public void Apply(in GaitPose pose, float weight)
Parameters
| Type | Name | Description |
|---|---|---|
pose |
||
weight |
Bind(Animator, RigMetrics, BindPose)
Prepares the space for one rig. Returns false and sets BindError on failure.
public bool Bind(Animator animator, RigMetrics metrics, BindPose bindPose)
Parameters
| Type | Name | Description |
|---|---|---|
animator |
||
metrics |
||
bindPose |
Reference pose to restore to before each write, resolved once by the caller so that all spaces share it. Never read this from the live Transforms: doing so lets an animated pose become the reference, after which rotations stack on a pose that already contains a frame of gait. |
Returns
- bool
BlendGroundNormal(Vector3, float, Vector3, float)
L08: the ground normal the pelvis should tilt towards, blended from both feet's L05 ground raycasts and weighted the same way their height correction already is — Apply(in GaitPose, float)'s per-foot ground weight, 0 for a missed raycast or a foot still mid-swing. Both weights 0 (both feet airborne, or anchoring off) reads as level ground rather than an undefined blend, since there is nothing to measure a slope from.
public static Vector3 BlendGroundNormal(Vector3 leftNormal, float leftWeight, Vector3 rightNormal, float rightWeight)
Parameters
| Type | Name | Description |
|---|---|---|
leftNormal |
||
leftWeight |
||
rightNormal |
||
rightWeight |
Returns
- Vector3
ClampArmReachDistance(float, float, float, float)
L11: how far along the reach direction the hand target may be placed. The upper limit is
maxExtension01 of the arm's full span rather than the span itself, so a
target beyond reach leaves the elbow slightly bent instead of locked straight. The lower limit is
the two segments' difference, below which no elbow angle exists.
public static float ClampArmReachDistance(float distance, float upperLength, float lowerLength, float maxExtension01)
Parameters
| Type | Name | Description |
|---|---|---|
distance |
||
upperLength |
||
lowerLength |
||
maxExtension01 |
Returns
- float
ClampGroundDelta(bool, float, float)
L05 no-ground policy: clamps a raw ground-height delta to the authored maximum drop, and reports whether the clamp fired — either the raycast found nothing, or what it found was farther below than the limit allows. Pure and rig-independent, so it is testable without a scene; CodeSmile.AnyMotion.AnimateYourself.Spaces.IkTargetSpaceDriver.ResolveGroundContact(UnityEngine.Vector3, UnityEngine.Transform, float) is the only caller.
public static (float Delta, bool Missed) ClampGroundDelta(bool hitFound, float rawDelta, float maxDropMetres)
Parameters
| Type | Name | Description |
|---|---|---|
hitFound |
||
rawDelta |
||
maxDropMetres |
Returns
- (float Delta, bool Missed)
ClampLookAngle(float, float)
L01: clamps a yaw or pitch angle to fullRangeDegrees of total sweep,
symmetric about facing forward — half the range each side, so
YawLimit/PitchLimit
at their default of 180 allow ±90°, and at 360 allow the full circle a Horrific head-spin needs.
public static float ClampLookAngle(float angleDegrees, float fullRangeDegrees)
Parameters
| Type | Name | Description |
|---|---|---|
angleDegrees |
||
fullRangeDegrees |
Returns
- float
ComputeCompromisePelvisDrop(float, float, float)
O6 (scout-ik-pop-flip-prevention.md §3): the smooth, C∞ compromise between the two legs'
ComputeGroundInducedPelvisDrop(float, float, float) results, replacing
Min(Max(leftDeficit, rightDeficit), maxSafeDrop) — the exact rule Johansen (2009 §7.5)
identifies and rejects, because a Max() of two competing per-leg values has a
second-order discontinuity in the hip trajectory wherever the two legs' deficits cross.
Ported from the reference formula
hipHeight = hLowestDesired + (minToAvg·minToMax)/(minToAvg+minToMax) — a leg's own
required drop plays the role of the reference system's per-leg "desired height" there, since a
larger deficit means more drop is required to keep that leg from stretching past its reach,
the same direction of "more of this value demands more compromise" the reference system's
height plays; and maxSafeDrop plays the role of its per-leg max-height
safety ceiling.
worstDeficit is pre-clamped to maxSafeDrop exactly as
the reference formula clamps lowestDesired to lowestMax first, and
minToAvg is floored at 0 for the same reason: without it, a pathological case where
both legs' unclamped deficits already exceed maxSafeDrop could leave the
average above the clamped worst value, which the reference formula's own domain (physically
bounded sphere intersections) never has to guard against but this one, built from an
independently-clamped input, does. The harmonic-mean-shaped term is provably
≤ min(minToAvg, minToMax), so the result never exceeds maxSafeDrop — no
leg is ever driven to invert past the knees — and it degrades to worstDeficit
itself (0 when neither leg has a deficit) whenever there is no conflict between the two legs,
preserving the gait's own intended bend exactly as the old rule did in that case.
public static float ComputeCompromisePelvisDrop(float leftDeficit, float rightDeficit, float maxSafeDrop)
Parameters
| Type | Name | Description |
|---|---|---|
leftDeficit |
||
rightDeficit |
||
maxSafeDrop |
Returns
- float
ComputeGroundInducedPelvisDrop(float, float, float)
L05: pelvis drop attributable to the ground correction alone, not to the gait pose's own uncorrected reach. A hip held at a fixed height while the foot reaches forward already pushes the straight-line hip-to-ankle distance close to a straight leg's full reach — ordinary gait geometry, present on perfectly flat ground, that CodeSmile.AnyMotion.AnimateYourself.Gait.WalkGait.Evaluate(in CodeSmile.AnyMotion.AnimateYourself.Gait.GaitContext, float, float, float, float, float, CodeSmile.AnyMotion.AnimateYourself.Gait.WalkGait.LegCycle, CodeSmile.AnyMotion.AnimateYourself.Gait.WalkGait.LegCycle, bool)'s own Bob only partly cancels and that CodeSmile.AnyMotion.AnimateYourself.Spaces.IkTargetSpaceDriver.SolveTwoBone(UnityEngine.Transform, UnityEngine.Transform, UnityEngine.Transform, UnityEngine.Vector3, UnityEngine.Vector3, float, float, float)'s own clamp has always silently absorbed (an imperceptibly shortened stride). ComputePelvisDrop(float, float) alone cannot tell that apart from "the ground correction pushed this out of reach", so subtracting the uncorrected deficit is what keeps flat ground at zero drop regardless of how tight the stride's own reach margin already is.
public static float ComputeGroundInducedPelvisDrop(float correctedDistance, float uncorrectedDistance, float maxLegReach)
Parameters
| Type | Name | Description |
|---|---|---|
correctedDistance |
||
uncorrectedDistance |
||
maxLegReach |
Returns
- float
ComputeLookAngles(Vector3)
L01 (look-at / head tracking): the yaw/pitch, in degrees, that would turn the character's
forward axis to face rootLocalDirection — a direction already expressed in
the character's own frame (Root.InverseTransformDirection), not world space. Returned as
(x: yaw, y: pitch). Yaw is rotation about the root's up axis, positive turning right —
the same sign every other yaw in WalkGaitParameters already uses. Pitch is
rotation about the root's right axis, positive tilting down, matching
Lean's own convention ("positive hunches forward")
— so a target above the anchor point yields a negative pitch. A zero-length direction
(target exactly at the anchor point) returns zero for both, rather than the undefined angle
Atan2(0, 0) would otherwise give.
public static Vector2 ComputeLookAngles(Vector3 rootLocalDirection)
Parameters
| Type | Name | Description |
|---|---|---|
rootLocalDirection |
Returns
- Vector2
ComputePelvisDrop(float, float)
L05: how far the pelvis must additionally drop so a leg whose ground-corrected target is farther than its own reach is not left stretching. 0 when the target is already within reach.
public static float ComputePelvisDrop(float hipToTargetDistance, float maxLegReach)
Parameters
| Type | Name | Description |
|---|---|---|
hipToTargetDistance |
||
maxLegReach |
Returns
- float
ComputeStepShortenMetres(float, float)
L08: how many metres to pull a leg's foot target back towards directly under the hip (Apply(in GaitPose, float)'s use against Position's Z), attempting to close a ground-induced reach deficit before falling back to lowering the pelvis for whatever is left. Simply the deficit scaled by the authored StepShortenRatio — deliberately not a pelvis/step split computed here: Apply(in GaitPose, float) decides the actual remaining pelvis need by re-running ComputeGroundInducedPelvisDrop(float, float, float) against the shortened position, not by assuming this amount of shortening resolves that much of the deficit. A deficit that is mostly vertical — the ordinary case standing on a slope, where fore/aft reach was never the problem — is barely reduced by shortening Z, so the pelvis still receives nearly the full deficit it would have without step shortening at all. Pre-allocating a fixed percentage to "pelvis share" instead, as an earlier version of this function did, starved the pelvis of slack it still needed whenever shortening could not actually use its share, which is what let a character standing idle on a slope lock its knees straight at ratio 1: the deficit there is almost entirely vertical, all of it was assigned away from the pelvis, and nothing was left to absorb it.
public static float ComputeStepShortenMetres(float deficit, float stepShortenRatio)
Parameters
| Type | Name | Description |
|---|---|---|
deficit |
||
stepShortenRatio |
Returns
- float
ConfigureGroundConformance(LayerMask, float, float)
L05: sets the physics/scene wiring the ground raycast needs. Called once from
GaitDriver.Bind, separate from Bind(Animator, RigMetrics, BindPose) itself because these values are not
rig data — they describe this project's colliders, not this character's measurements.
public void ConfigureGroundConformance(LayerMask groundLayers, float probeUp, float probeDown)
Parameters
| Type | Name | Description |
|---|---|---|
groundLayers |
||
probeUp |
||
probeDown |
Dispose()
Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
public void Dispose()
LookAtWeightShare(float, float)
L01: one bone's fraction of the total look rotation, from its own authored weight
(HeadWeight and its two siblings) against the sum
of all three. The three do not need to add up to 1 — dividing by their sum normalises them
regardless. A sum at or below 0 (every weight authored to 0) returns 0 rather than dividing by
zero, which disables the rotation instead of producing NaN.
public static float LookAtWeightShare(float boneWeight, float totalWeight)
Parameters
| Type | Name | Description |
|---|---|---|
boneWeight |
||
totalWeight |
Returns
- float
MirrorGroundNormalHorizontal(Vector3)
L08: a ground normal's horizontal projection always points downhill — the direction of steepest descent, by construction of what a surface normal is. Tilting the pelvis directly towards BlendGroundNormal(Vector3, float, Vector3, float)'s result would therefore lean the character downhill: nose-up walking uphill, nose-down walking down, banked away from a side-hill — the opposite of how a person actually balances on a slope, which is to lean into it (forward walking up, back walking down, into the hill on a traverse) to keep their centre of mass over their feet. Mirroring the normal's horizontal component before tilting towards it corrects this for both the fore/aft and the side-to-side case at once, with the same construction: negating a component never changes a vector's length, so a unit normal in stays a unit normal out.
public static Vector3 MirrorGroundNormalHorizontal(Vector3 groundNormal)
Parameters
| Type | Name | Description |
|---|---|---|
groundNormal |
Returns
- Vector3
ResolveArmIkWeight(bool, float, float, float)
L11 (arm layering): whether and how much ApplyArm(ArmAngles, LimbTarget, float, float, float, float, float, Transform, float, Transform, float, HumanBodyBones, HumanBodyBones, HumanBodyBones, float, float, float, bool) should reach for an external
hand target instead of the ordinary rotational swing — 0 (no IK at all) whenever
hasTarget is false or the arm's segments cannot be measured
(upperLength/lowerLength at or below 0), regardless of
handTargetWeight, so an unmeasurable rig or an unassigned target behaves
exactly as before this mechanism existed rather than reaching for an undefined point.
public static float ResolveArmIkWeight(bool hasTarget, float handTargetWeight, float upperLength, float lowerLength)
Parameters
| Type | Name | Description |
|---|---|---|
hasTarget |
||
handTargetWeight |
||
upperLength |
||
lowerLength |
Returns
- float
ResolveArmPoleDirection(Vector3, Vector3, float, Vector3)
L11: which way the elbow points. A relaxed human elbow points backwards and a little outwards,
and keeps doing so whether the hand is hanging, swinging or reaching, so this is fixed in the
character's own frame — poleOutwardDegrees from
backAxis towards outwardAxis. Deriving it from the target
instead, as the previous version did, moved the elbow whenever the target moved and produced the
reported elbow snap as a target passed behind the character.
public static Vector3 ResolveArmPoleDirection(Vector3 backAxis, Vector3 outwardAxis, float poleOutwardDegrees, Vector3 aimDirection)
Parameters
| Type | Name | Description |
|---|---|---|
backAxis |
||
outwardAxis |
||
poleOutwardDegrees |
||
aimDirection |
Returns
- Vector3
Remarks
CodeSmile.AnyMotion.AnimateYourself.Spaces.IkTargetSpaceDriver.SolveTwoBone(UnityEngine.Transform, UnityEngine.Transform, UnityEngine.Transform, UnityEngine.Vector3, UnityEngine.Vector3, float, float, float) uses only the part of the pole perpendicular to the aim direction.
That part vanishes when the two are collinear, and the solver then falls back to a world axis
which does not know which arm it is placing, so both elbows could bend the same way. Sliding the
pole towards outwardAxis as the two approach collinear keeps that fallback
unreachable, and the outward axis is never itself collinear with an aim direction the base pole
is also collinear with, because the two are poleOutwardDegrees apart.
ResolveAzimuthExcess(float, float, float)
L11: how many degrees azimuthDegrees lies outside the allowed window
[-acrossLimit, +behindLimit], measured to whichever edge is nearer and correct across the
±180 wrap. 0 when inside.
public static float ResolveAzimuthExcess(float azimuthDegrees, float acrossLimitDegrees, float behindLimitDegrees)
Parameters
| Type | Name | Description |
|---|---|---|
azimuthDegrees |
||
acrossLimitDegrees |
||
behindLimitDegrees |
Returns
- float
ResolveMissedFootDelta(float, bool, float)
L05 no-ground policy, second stage: what to apply for a foot whose own raycast missed. A miss
carries no real information about the ground below, so stretching the leg to reach an
authored clamp point — and, through ComputeGroundInducedPelvisDrop(float, float, float), dropping
the pelvis to make that reach possible — reads as the leg straining for a rung that is not
there: a foot hanging past a ledge comes to rest fully extended, and the other, genuinely
planted leg has to bend further to absorb a pelvis drop it never needed. Matching the other
foot's own resolved delta instead keeps a missed foot within a normal stride's reach, at the
cost of a floating-foot look when it hangs over a drop — an accepted trade, not a physically
accurate one. Falls back to clampedDelta (the authored drop clamp) only
when the other foot has also missed, since there is then nothing to match; the character's
capsule collider can still be holding it up even when both feet miss.
public static float ResolveMissedFootDelta(float clampedDelta, bool otherMissed, float otherDelta)
Parameters
| Type | Name | Description |
|---|---|---|
clampedDelta |
||
otherMissed |
||
otherDelta |
Returns
- float
ResolveReachAzimuth(Vector3, float)
L11: the direction from a shoulder to a reach target, as a horizontal bearing in degrees — 0
straight forward, +90 straight out to this arm's own side, ±180 straight behind, negative across
the body's centre line. Measured in root space and mirrored by side, so the
two arms produce the same number for mirrored targets and one set of limits covers both.
public static float ResolveReachAzimuth(Vector3 directionRootSpace, float side)
Parameters
| Type | Name | Description |
|---|---|---|
directionRootSpace |
||
side |
Returns
- float
ResolveReachElevation(Vector3)
L11: how far a reach direction is from hanging straight down, in degrees — 0 hanging, 90 level with the shoulder, 180 straight overhead.
public static float ResolveReachElevation(Vector3 directionRootSpace)
Parameters
| Type | Name | Description |
|---|---|---|
directionRootSpace |
Returns
- float
ResolveReachability(Vector3, float, float, float, float, float)
L11: how much of an assigned hand target's reach the arm should actually attempt, 0..1. 1 inside
the authored reach window, falling to 0 over falloffDegrees outside it, at
which point the arm is back on its ordinary gait swing.
public static float ResolveReachability(Vector3 directionRootSpace, float side, float upLimitDegrees, float acrossLimitDegrees, float behindLimitDegrees, float falloffDegrees)
Parameters
| Type | Name | Description |
|---|---|---|
directionRootSpace |
||
side |
||
upLimitDegrees |
||
acrossLimitDegrees |
||
behindLimitDegrees |
||
falloffDegrees |
Returns
- float
Remarks
Fading the attempt rather than clamping the target is a deliberate choice, not a simplification. The unreachable directions form a connected region behind the body; a target crossing it is at some point equally far from both edges of the window, and a clamp must jump from one edge to the other there. That jump is the reported "the far arm flips around 180° very quickly". Giving up on the reach has no such point, and matches what a person does with something out of reach.
The azimuth excess is scaled by the sine of the elevation, so azimuth stops mattering as the arm approaches hanging straight down or reaching straight up — at either pole every azimuth is the same pose, and without the scaling a hand hanging at the side a fraction of a degree behind the window would read as unreachable.
ResolveStretchCeiling(float)
O4: the largest stretch that still leaves the joint straightening as its target moves
further away, given how wide softReachStart makes the softening band.
ResolveStretchFactor(float, float, float, float) caps MaxBoneStretch
at this, so no authored combination of the two can produce the inversion described below.
public static float ResolveStretchCeiling(float softReachStart)
Parameters
| Type | Name | Description |
|---|---|---|
softReachStart |
Returns
- float
Remarks
Why a cap is needed at all, and why it depends on the band width: the stretch grows the chain
across the band while the target is also moving out across it. If the chain grows faster than
the target recedes, the joint bends more the further the limb reaches — the leg reads as
inflating rather than extending, which is a worse artefact than the reach shortfall the stretch
was added to remove. Measured on a 0.42 m + 0.40 m leg at SoftReachStart 0.90: the knee
angle rises 128.3° → 143.4° across the band unstretched and 128.3° → 138.1° at
MaxBoneStretch 0.05, but at 0.15 it rises to 129.2° and then falls to 119.8° before
recovering — a 9.4° inversion in the middle of the band.
The threshold is analytic, not fitted. Writing k for the stretch factor, both
segments scale by k and SoftenReach(float, float, float) is homogeneous of degree one, so
the solved distance is k · SoftenReach(d/k, span) and the law of cosines' argument
reduces to a fixed increasing function of d/k. The joint therefore straightens
exactly while d/k increases, which holds while k' < k/d. Smoothstep's
slope peaks at 1.5 in the middle of the band; substituting there and solving for the
stretch gives 2(1−s) / (0.5 + 2.5s) — 0.042 at the default s of 0.94, 0.073
at 0.90, 0.035 at 0.95, 0.16 at 0.80. A numeric sweep of the real functions agrees with the formula and
confirms the middle of the band is the binding point.
ResolveStretchFactor(float, float, float, float)
O4 (knee pop) from the ik-pop-flip-prevention scout report, second half of the Kovar fix: how much longer each of the chain's two segments may become at this reach distance, as a multiplier on its bind-pose length. 1 is no stretch. SoftenReach(float, float, float) alone removes the pop by giving up reach — the end effector settles short of a target it was asked to hit. Letting the segments grow inside that same band recovers the reach without letting the joint straighten, because the softening still applies, now against the longer span.
public static float ResolveStretchFactor(float distance, float fullReach, float softReachStart, float maxStretch)
Parameters
| Type | Name | Description |
|---|---|---|
distance |
Requested distance from the chain's origin joint to its end effector. |
|
fullReach |
The chain's unstretched span, the same value SoftenReach(float, float, float) is given. |
|
softReachStart |
||
maxStretch |
MaxBoneStretch, or 0 when BoneStretch is off — both produce exactly 1 here. Capped by ResolveStretchCeiling(float). |
Returns
- float
Remarks
Shape, and why each property is load-bearing:
- Exactly 1 at or below CodeSmile.AnyMotion.AnimateYourself.Spaces.IkTargetSpaceDriver.SoftReachKnee(float, float), so nothing inside the chain's ordinary working range is touched and a disabled stretch is bit-for-bit inert rather than approximately inert.
- Smoothstep across the band, so the first derivative is zero at both ends. A linear ramp would step the derivative at the band's lower edge, which is a new discontinuity of exactly the kind O4 exists to remove — the whole point of the softening half is that a derivative jump, not a large value, is what reads as a pop.
-
Saturated at
1 + maxStretchfrom full extension outward, so an unreachable target cannot grow the limb without bound. - The band is measured against the unstretched span, so this is a plain function of the requested distance rather than a fixed point that would need iterating.
The same multiplier is applied to both segments, so the chain is scaled about the joint it hangs from rather than reshaped: each segment grows by the same fraction of its own length and their ratio is preserved, which is what keeps the bend angle geometrically similar to the unstretched solve and keeps a rig with unusual segment proportions (a short shin, a long forearm) from receiving a stretch sized by the other segment.
ResolveStretchedJointPositions(Vector3, Vector3, Vector3, float, out Vector3, out Vector3)
O4: where a chain's two joints move to once ResolveStretchFactor(float, float, float, float) has asked for a
stretch — a uniform scaling of the whole chain about origin, expressed in
world space.
public static void ResolveStretchedJointPositions(Vector3 origin, Vector3 midWorld, Vector3 endWorld, float stretch, out Vector3 stretchedMid, out Vector3 stretchedEnd)
Parameters
| Type | Name | Description |
|---|---|---|
origin |
||
midWorld |
||
endWorld |
||
stretch |
||
stretchedMid |
||
stretchedEnd |
Remarks
World space rather than each bone's localPosition on purpose: a humanoid rig may carry
an unmapped bone (a twist bone) between two mapped ones, and then the mapped child's
localPosition is only part of the joint-to-joint offset. Scaling the measured
joint-to-joint vectors scales the segment lengths the solver actually uses, whatever the
hierarchy between them looks like, and it is equally correct under a uniform character scale
because both vectors are measured in the same frame they are written back to.
Both outputs are absolute positions computed from the pre-move inputs, so the caller may
assign stretchedMid first even though that drags the end joint along
with it.
This writes to the rig, so it is the one part of the stretch that has to be undone. It is:
Apply restores every mapped bone's bind-pose position and rotation before writing
anything (HumanoidBoneSet.RestoreBindPose), and Dispose() restores it
once more, so the authored lengths come back on the first frame the option is switched off,
on unbind, and when the component is removed. Nothing here is written to the avatar or to
the bind pose itself.
SlerpReachDirection(Vector3, Vector3, Vector3, float)
L11: rotates fromDirection a fraction t of the way
towards toDirection, along the great circle between them. Blending the hand's
direction is what keeps a partial reach on the short path; blending the two bone rotations
instead took the short path in orientation space, which carried the hand sideways and was the
reported "the fore/aft swing becomes a left/right sway".
public static Vector3 SlerpReachDirection(Vector3 fromDirection, Vector3 toDirection, Vector3 fallbackAxis, float t)
Parameters
| Type | Name | Description |
|---|---|---|
fromDirection |
||
toDirection |
||
fallbackAxis |
||
t |
Returns
- Vector3
Remarks
For exactly opposite directions — a target straight overhead while the arm hangs straight down —
the plane of rotation is undefined. fallbackAxis decides it rather than
leaving it to the library: pass this arm's own outward axis, which is the plane a person raises
an arm through and keeps the two arms mirror-symmetric.
SmoothDirectionTowards(Vector3, Vector3, float, float)
L08 (slope adaptation): SmoothTowards(float, float, float, float)'s exponential-smoothing step, for a
direction instead of a scalar — Slerp in place of Lerp, same smoothingSeconds/deltaTime semantics, same snap-to-target
shortcut for a fresh bind or a large gap. Used for the blended ground normal the pelvis tilts
towards, so a sudden change in what either foot's raycast finds does not rotate the pelvis in
one frame.
public static Vector3 SmoothDirectionTowards(Vector3 current, Vector3 target, float smoothingSeconds, float deltaTime)
Parameters
| Type | Name | Description |
|---|---|---|
current |
||
target |
||
smoothingSeconds |
||
deltaTime |
Returns
- Vector3
SmoothTowards(float, float, float, float)
L05: one exponential-smoothing step of current towards
target, framerate-independent — after smoothingSeconds
of elapsed time the gap has closed by ~63 % regardless of how that time was split across
frames, matching CodeSmile.AnyMotion.AnimateYourself.Gait.WalkGait.AdvanceIdleWeight(float, bool, float, float)'s framerate-independence goal but
for an unbounded metres value, where a fixed-duration MoveTowards has no natural step
size to use. smoothingSeconds at or below 0 snaps straight to
target — the pre-2026-08-06 behaviour when smoothing is disabled — and a
very large deltaTime converges to target on its own, so
a long gap (a paused editor, a hitch) never needs special-casing.
deltaTime at or below 0 instead returns current
unchanged: zero elapsed time can only mean nothing has moved yet, never an instant jump. Fixed
2026-08-05 — this used to return target for that case too, indistinguishable
from "smoothing disabled", which snapped instead of holding whenever two calls landed on the
same Time.realtimeSinceStartup value.
public static float SmoothTowards(float current, float target, float smoothingSeconds, float deltaTime)
Parameters
| Type | Name | Description |
|---|---|---|
current |
||
target |
||
smoothingSeconds |
||
deltaTime |
Returns
- float
SoftenReach(float, float, float)
O4 (knee pop) from the ik-pop-flip-prevention scout report: softens reach distance as it
approaches full extension instead of hard-clamping it there — asymptotically approaching but
never reaching fullReach, so the
bend angle's sensitivity to a moving target stays continuous through what used to be a clamp
boundary. Measured: the hard clamp's sensitivity steps from 5.74 to exactly 0 °/1%L at
distance == fullReach; that derivative jump, not the peak sensitivity, is what reads as
a pop. Below s of full reach this is the identity function, so a distance
already inside the chain's ordinary working range is unaffected.
public static float SoftenReach(float distance, float fullReach, float s)
Parameters
| Type | Name | Description |
|---|---|---|
distance |
||
fullReach |
||
s |
Fraction of |
Returns
- float
- See Also
TrackLookAngle(float, float, float, float, bool, ref float, float)
L01: advances a currently-tracked yaw or pitch towards targetDegrees, capped
at trackingSpeedDegreesPerSecond. useEasing false is a
constant-rate turn (UnityEngine.Mathf.MoveTowardsAngle(float, float, float)) — starts and stops instantly, which
reads as mechanical on purpose (a turret, a robot). useEasing true (the
default) accelerates from rest and decelerates into the target
(UnityEngine.Mathf.SmoothDampAngle(float, float, ref float, float)) instead of the exponential-decay smoothing
SmoothTowards(float, float, float, float)/SmoothDirectionTowards(Vector3, Vector3, float, float) use elsewhere: exponential
decay only decelerates — it is fastest at the instant the target changes — so it cannot produce
the accel/decel this was asked for. velocity is SmoothDampAngle's own
state and is reset to 0 whenever easing is off, so switching easing back on does not resume with
a stale velocity from the constant-rate mode. The ease time constant is fixed
(CodeSmile.AnyMotion.AnimateYourself.Spaces.IkTargetSpaceDriver.LookAtEaseSeconds) rather than authored — trackingSpeedDegreesPerSecond
alone is the exposed knob, matching what was actually asked for.
The result is always wrapped to canonical (-180, 180] before returning — fixed 2026-08-05,
user-reported (a stuck head/torso orientation that only cleared on Reset Pose, and did not
recover when the target moved back to the front). Without the wrap, current
can drift to a value that is only equivalent to the intended angle modulo 360, not
equal to it — e.g. 380° instead of the intended 20° — most often after the target
repeatedly crossed the yaw branch cut (a target passing directly behind the character, or
the character turning while a roughly-stationary target sits behind it). SmoothDampAngle
internally recomputes its target as current + Mathf.DeltaAngle(current, target); once
current is stuck at a drifted-but-equivalent value, DeltaAngle
reports zero for any later targetDegrees that is itself in that
same equivalence class, so the recomputed target becomes identical to current
and SmoothDampAngle stops moving — permanently, since nothing in that call ever
re-examines the drifted value once it stops changing. Wrapping the return value here keeps
current canonical going into every subsequent call, so it can never
accumulate past (-180, 180] in the first place. The frozen, unwrapped value also read as a
wrong upper-body posture rather than merely a wrong head: split across Head/Neck/Chest by
weight and combined with a nonzero pitch, rotations about the two different axes do not
commute, so distributing an unwrapped 380° yaw does not compose back to the same visual
result as the equivalent, wrapped 20° would — even though a pure single-axis
rotation provably would not have shown any difference (composing rotations about one shared
axis is exactly angle addition, which 380° and 20° satisfy identically modulo 360).
public static float TrackLookAngle(float current, float targetDegrees, float trackingSpeedDegreesPerSecond, float deltaTime, bool useEasing, ref float velocity, float smoothTime = 0.25)
Parameters
| Type | Name | Description |
|---|---|---|
current |
||
targetDegrees |
||
trackingSpeedDegreesPerSecond |
||
deltaTime |
||
useEasing |
||
velocity |
||
smoothTime |
Returns
- float
Events
FootMissedGround
L05: fired when a foot's ground raycast found nothing, or found ground farther below the
no-ground clamp than MaxUnsupportedFootDrop
allows. true is the left foot. Placeholder: the frozen payload shape is R13 (public
event surface, risks.md), not due until the P7 schema freeze — do not treat this signature
as stable.
public event Action<bool> FootMissedGround
Event Type
- Action<bool>