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
Expand Details ...
Inheritance
object
IkTargetSpaceDriver
Implements

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

GaitDriverSpaceKind

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
GaitPose
pose
float
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
animator
RigMetrics
metrics
BindPose
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
Vector3
leftNormal
float
leftWeight
Vector3
rightNormal
float
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
float
distance
float
upperLength
float
lowerLength
float
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
bool
hitFound
float
rawDelta
float
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
float
angleDegrees
float
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
float
leftDeficit
float
rightDeficit
float
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
float
correctedDistance
float
uncorrectedDistance
float
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
Vector3
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
float
hipToTargetDistance
float
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
float
deficit
float
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
LayerMask
groundLayers
float
probeUp
float
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
float
boneWeight
float
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
Vector3
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
bool
hasTarget
float
handTargetWeight
float
upperLength
float
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
Vector3
backAxis
Vector3
outwardAxis
float
poleOutwardDegrees
Vector3
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
float
azimuthDegrees
float
acrossLimitDegrees
float
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
float
clampedDelta
bool
otherMissed
float
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
Vector3
directionRootSpace
float
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
Vector3
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
Vector3
directionRootSpace
float
side
float
upLimitDegrees
float
acrossLimitDegrees
float
behindLimitDegrees
float
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
float
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
float
distance

Requested distance from the chain's origin joint to its end effector.

float
fullReach

The chain's unstretched span, the same value SoftenReach(float, float, float) is given.

float
softReachStart

SoftReachStart.

float
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 + maxStretch from 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
Vector3
origin
Vector3
midWorld
Vector3
endWorld
float
stretch
Vector3
stretchedMid
Vector3
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
Vector3
fromDirection
Vector3
toDirection
Vector3
fallbackAxis
float
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
Vector3
current
Vector3
target
float
smoothingSeconds
float
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
float
current
float
target
float
smoothingSeconds
float
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
float
distance
float
fullReach
float
s

Fraction of fullReach where softening begins (the "knee" of the curve). SoftReachStart.

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
float
current
float
targetDegrees
float
trackingSpeedDegreesPerSecond
float
deltaTime
bool
useEasing
float
velocity
float
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>