A utility that adjusts a baked human model pose to child-like proportions. It caches model bone data, computes per-bone scale/endpoint factors based on named bones, and applies position/IK corrections (including manual IK blends and preserved two-handed weapon grips) to output transforms.
using System;
namespace CharacterEditor;
/// <summary>
/// Applies school-age proportions to an already evaluated native human pose.
/// This class owns no animation clock, graph, bone overrides or scene callbacks.
/// Its caller composes the result with ground contact before submitting one pose.
/// All input/output transforms are world-space, indexed by the model's bones.
/// </summary>
public sealed class CharacterGrowthPose
{
Model model;
int[] parents = Array.Empty<int>(), order = Array.Empty<int>();
Transform[] rest = Array.Empty<Transform>(), scratch = Array.Empty<Transform>(), constraintPose = Array.Empty<Transform>();
Vector3[] childRest = Array.Empty<Vector3>();
float[] endpoints = Array.Empty<float>(), factors = Array.Empty<float>();
float amount = -1, childDrop, childStride = 1, childHeight = 1;
Vector3 worldDrop;
bool compatible;
readonly int[] chainStarts = new int[4], chainMiddles = new int[4], chainEnds = new int[4];
readonly int[][] chainBones = { Array.Empty<int>(), Array.Empty<int>(), Array.Empty<int>(), Array.Empty<int>() };
readonly bool[] manualTargets = new bool[4];
readonly float[] manualWeights = new float[4];
readonly int[] gripTargets = { -1, -1 };
static readonly string[] ManualTargetParameters = { "ik.hand_left.enabled", "ik.hand_right.enabled", "ik.foot_left.enabled", "ik.foot_right.enabled" };
bool runtimeConditionsInitialized;
float sittingWeight;
bool stockTwoHandGrip;
float worldUnit = 1;
/// <summary>Diagnostic for native weapon-grip preservation: -1 none, 0 left, 1 right.</summary>
public int PreservedGripSide { get; private set; } = -1;
public float PreservedGripWeight { get; private set; }
public bool IsActive => compatible && amount > 0;
public int BoneCount => parents.Length;
/// <summary>Rest-space pelvis lowering, before the caller's overall height scale.</summary>
public float PelvisDrop => compatible ? childDrop * MathF.Max(0, amount) : 0;
/// <summary>Estimated standing mesh height relative to the adult at the same object scale.</summary>
public float EstimatedHeightRatio => compatible ? 1 + (childHeight - 1) * MathF.Max(0, amount) : 1;
/// <summary>Length-weighted leg ratio for the controller's stride-speed conversion.</summary>
public float StrideFactor => compatible ? 1 + (childStride - 1) * MathF.Max(0, amount) : 1;
/// <summary>
/// Controller estimate when no renderer is available yet. The live instance's
/// StrideFactor measures the actual upper/lower leg lengths instead.
/// </summary>
public static float EstimateStrideFactor(float maturity)
=> 1 - .19f * CharacterMaturity.ChildAmount(maturity);
public void Configure(SkinnedModelRenderer body, CharacterAppearanceData appearance)
{
if (!body.IsValid())
{
Configure(null, 1, Transform.Zero);
return;
}
ConfigureCore(body.Model, appearance?.BodyMaturity ?? 1, body.WorldTransform);
bool graph = body.UseAnimGraph && body.SceneModel is not null;
for (int i = 0; i < manualTargets.Length; i++)
{
manualTargets[i] = graph && body.Parameters.GetBool(ManualTargetParameters[i]);
float weight = manualTargets[i] ? 1 : 0;
if (!runtimeConditionsInitialized) manualWeights[i] = weight;
else manualWeights[i] += Math.Clamp(weight - manualWeights[i], -MathF.Max(0, Time.Delta) / .35f, MathF.Max(0, Time.Delta) / .35f);
}
int hold = graph ? body.Parameters.GetInt("holdtype") : 0;
// Only the stock holdtypes with internal hand-to-hand IK are eligible.
// One-handed modes, unarmed poses, custom holds and explicit hand targets
// retain their own interpretation; no grip is invented from their pose.
stockTwoHandGrip = graph && (hold is 1 or 2 or 3 or 6 or 7 or 8)
&& body.Parameters.GetInt("holdtype_handedness") == 0 && manualWeights[0] == 0 && manualWeights[1] == 0;
worldUnit = MathF.Max(.001f, MathF.Abs(body.WorldScale.z));
// The native human exposes the sit enum, with chair poses 1–3 and
// ground poses 4–7. Preserve its pelvis/seat anchor while seated instead
// of applying the standing leg-length correction beneath the chair.
float seated = graph && body.Parameters.GetInt("sit") != 0 ? 1 : 0;
if (!runtimeConditionsInitialized) sittingWeight = seated;
else sittingWeight += Math.Clamp(seated - sittingWeight, -MathF.Max(0, Time.Delta) / .3f, MathF.Max(0, Time.Delta) / .3f);
runtimeConditionsInitialized = true;
worldDrop *= 1 - sittingWeight;
}
public void Configure(Model source, float maturity, Transform bodyWorld)
{
ConfigureCore(source, maturity, bodyWorld);
Array.Clear(manualTargets);
Array.Clear(manualWeights);
stockTwoHandGrip = false;
runtimeConditionsInitialized = false;
sittingWeight = 0;
}
void ConfigureCore(Model source, float maturity, Transform bodyWorld)
{
if (model != source) Cache(source);
float next = CharacterMaturity.ChildAmount(maturity);
if (amount != next)
{
amount = next;
for (int i = 0; i < factors.Length; i++) factors[i] = 1 + (endpoints[i] - 1) * amount;
}
// Keep the correction in model Z, including rotated/scaled actor parents.
worldDrop = bodyWorld.Rotation * (new Vector3(0, 0, -PelvisDrop) * bodyWorld.Scale);
}
void Cache(Model source)
{
model = source;
amount = -1;
compatible = false;
runtimeConditionsInitialized = false;
sittingWeight = 0;
Array.Clear(manualTargets);
Array.Clear(manualWeights);
stockTwoHandGrip = false;
gripTargets[0] = gripTargets[1] = -1;
childDrop = 0;
childStride = childHeight = 1;
int count = source?.BoneCount ?? 0;
parents = new int[count];
order = new int[count];
rest = new Transform[count];
scratch = new Transform[count];
constraintPose = new Transform[count];
childRest = new Vector3[count];
endpoints = new float[count];
factors = new float[count];
var names = new string[count];
for (int i = 0; i < count; i++)
{
names[i] = source.GetBoneName(i);
parents[i] = source.GetBoneParent(i);
rest[i] = source.GetBoneTransform(i);
order[i] = i;
endpoints[i] = factors[i] = 1;
}
int Find(string name) => Array.IndexOf(names, name);
int pelvis = Find("pelvis"), head = Find("head");
int upperL = Find("leg_upper_L"), upperR = Find("leg_upper_R");
int lowerL = Find("leg_lower_L"), lowerR = Find("leg_lower_R");
int ankleL = Find("ankle_L"), ankleR = Find("ankle_R");
// Unsupported skeletons remain exact identity instead of partially applying
// named transformations without a known leg/pelvis reference.
if (pelvis < 0 || head < 0 || upperL < 0 || upperR < 0 || lowerL < 0 || lowerR < 0 || ankleL < 0 || ankleR < 0) return;
int Depth(int i)
{
int depth = 0;
while (i >= 0 && depth <= count)
{
if (i >= count) return count + 1;
i = parents[i];
depth++;
}
return depth;
}
var depths = new int[count];
for (int i = 0; i < count; i++)
{
depths[i] = Depth(i);
if (depths[i] > count) return;
}
Array.Sort(order, (a, b) => depths[a] != depths[b] ? depths[a].CompareTo(depths[b]) : a.CompareTo(b));
chainStarts[0] = Find("arm_upper_L"); chainStarts[1] = Find("arm_upper_R"); chainStarts[2] = upperL; chainStarts[3] = upperR;
chainMiddles[0] = Find("arm_lower_L"); chainMiddles[1] = Find("arm_lower_R"); chainMiddles[2] = lowerL; chainMiddles[3] = lowerR;
chainEnds[0] = Find("hand_L"); chainEnds[1] = Find("hand_R"); chainEnds[2] = ankleL; chainEnds[3] = ankleR;
gripTargets[0] = Find("hand_L_to_R_ikrule"); gripTargets[1] = Find("hand_R_to_L_ikrule");
for (int chain = 0; chain < chainBones.Length; chain++)
{
int start = chainStarts[chain];
bool InChain(int i) { while (i >= 0) { if (i == start) return true; i = parents[i]; } return false; }
chainBones[chain] = start < 0 || chainMiddles[chain] < 0 || chainEnds[chain] < 0
? Array.Empty<int>() : order.Where(InChain).ToArray();
}
for (int i = 0; i < count; i++) endpoints[i] = SegmentEndpoint(names[i]);
float Upper(int a, int b) => (rest[a].Position - rest[b].Position).Length;
float thigh = (Upper(upperL, lowerL) + Upper(upperR, lowerR)) * .5f;
float shin = (Upper(lowerL, ankleL) + Upper(lowerR, ankleR)) * .5f;
if (thigh + shin <= .001f) return;
childStride = (.80f * thigh + .82f * shin) / (thigh + shin);
// Use vertical rest displacement rather than total limb length so slightly
// angled rest legs retain their original sole height at the child endpoint.
float Drop(int upper, int lower, int ankle)
=> (rest[upper].Position.z - rest[lower].Position.z) * .20f
+ (rest[lower].Position.z - rest[ankle].Position.z) * .18f;
childDrop = MathF.Max(0, (Drop(upperL, lowerL, ankleL) + Drop(upperR, lowerR, ankleR)) * .5f);
foreach (int i in order)
{
int parent = parents[i];
childRest[i] = parent < 0 ? rest[i].Position - Vector3.Up * childDrop
: childRest[parent] + (rest[i].Position - rest[parent].Position) * endpoints[i];
}
float height = source.Bounds.Maxs.z - source.Bounds.Mins.z;
if (height > .001f) childHeight = Math.Clamp(1 + (childRest[head].z - rest[head].Position.z) / height, .5f, 1);
compatible = true;
}
static float SegmentEndpoint(string name)
{
// A factor describes the edge ENDING at this bone, not the bone that
// starts the limb. Twist samples use the same region as their endpoint.
if (name is "leg_lower_L" or "leg_lower_R" || name.StartsWith("leg_upper_L_twist", StringComparison.Ordinal) || name.StartsWith("leg_upper_R_twist", StringComparison.Ordinal)) return .80f;
if (name is "ankle_L" or "ankle_R" || name.StartsWith("leg_lower_L_twist", StringComparison.Ordinal) || name.StartsWith("leg_lower_R_twist", StringComparison.Ordinal)) return .82f;
if (name is "arm_lower_L" or "arm_lower_R" || name.StartsWith("arm_upper_L_twist", StringComparison.Ordinal) || name.StartsWith("arm_upper_R_twist", StringComparison.Ordinal)) return .84f;
if (name is "hand_L" or "hand_R" || name.StartsWith("arm_lower_L_twist", StringComparison.Ordinal) || name.StartsWith("arm_lower_R_twist", StringComparison.Ordinal)) return .88f;
if (name is "clavicle_L" or "clavicle_R" or "arm_upper_L" or "arm_upper_R") return .86f;
if (name is "spine_0" or "spine_1" or "spine_2" or "neck_0") return .94f;
if (name == "head") return .90f;
return 1;
}
/// <summary>
/// Reuses the configured buffers. Input and output may be the same array.
/// All descendants follow the changed parent position; face/finger/foot
/// rotations and their local dimensions remain those of the native pose.
/// At adult maturity the complete pose is copied exactly.
/// </summary>
public void ApplyTo(Transform[] input, Transform[] output)
{
PreservedGripSide = -1;
PreservedGripWeight = 0;
if (input is null || output is null || input.Length < BoneCount || output.Length < BoneCount)
throw new ArgumentException("Growth pose arrays must contain every model bone.");
if (!IsActive)
{
if (input != output) Array.Copy(input, output, BoneCount);
return;
}
var native = input;
if (input == output)
{
Array.Copy(input, scratch, BoneCount);
native = scratch;
}
foreach (int i in order)
{
int parent = parents[i];
var position = parent < 0 ? native[i].Position + worldDrop
: output[parent].Position + (native[i].Position - native[parent].Position) * factors[i];
output[i] = new Transform(position, native[i].Rotation, native[i].Scale);
}
bool constrained = false;
for (int i = 0; i < manualWeights.Length; i++) constrained |= manualWeights[i] > 0 && chainBones[i].Length > 0;
if (stockTwoHandGrip)
for (int side = 0; side < 2; side++)
{
if (chainBones[side].Length == 0 || gripTargets[side] < 0) continue;
var hand = native[chainEnds[side]];
var grip = native[gripTargets[side]];
float distance = (hand.Position - grip.Position).Length / worldUnit;
float angle = hand.Rotation.Distance(grip.Rotation);
// Inspect the graph's evaluated relationship. Reload/action graphs
// deliberately release this IK; their separated hand must stay free.
// The narrow tolerance fade avoids switching the correction abruptly
// when the native graph begins its own release/catch transition.
float weight = (1 - Smooth(.04f, .18f, distance)) * (1 - Smooth(1, 6, angle));
if (weight <= .0001f) continue;
PreservedGripSide = side;
PreservedGripWeight = weight;
constrained = true;
break; // Right-handed stock 2H grips prefer the left supporting arm.
}
if (!constrained) return;
Array.Copy(output, constraintPose, BoneCount);
for (int chain = 0; chain < manualWeights.Length; chain++)
if (manualWeights[chain] > 0 && chainBones[chain].Length > 0)
{
var grown = constraintPose[chainEnds[chain]];
var requested = native[chainEnds[chain]];
// Ramp our correction with the stock manual-IK transition instead
// of suddenly restoring an adult-length endpoint on its first frame.
SolveTarget(chain, new Transform(Vector3.Lerp(grown.Position, requested.Position, manualWeights[chain]),
Rotation.Slerp(grown.Rotation, requested.Rotation, manualWeights[chain]), grown.Scale), output);
}
if (PreservedGripSide >= 0)
{
int side = PreservedGripSide;
var hand = output[chainEnds[side]];
var grip = output[gripTargets[side]];
var target = new Transform(Vector3.Lerp(hand.Position, grip.Position, PreservedGripWeight),
Rotation.Slerp(hand.Rotation, grip.Rotation, PreservedGripWeight), hand.Scale);
SolveTarget(side, target, output, shoulderReach: true);
}
}
static float Smooth(float low, float high, float value)
{
float t = Math.Clamp((value - low) / (high - low), 0, 1);
return t * t * (3 - 2 * t);
}
void SolveTarget(int chain, Transform target, Transform[] output, bool shoulderReach = false)
{
int start = chainStarts[chain], middle = chainMiddles[chain], end = chainEnds[chain];
var root = constraintPose[start].Position;
var joint = constraintPose[middle].Position;
var endpoint = constraintPose[end].Position;
float upper = (joint - root).Length, lower = (endpoint - joint).Length;
if (upper < .0001f || lower < .0001f) return;
var toward = target.Position - root;
var axis = toward.LengthSquared > .00000001f ? toward.Normal : (endpoint - root).Normal;
if (axis.LengthSquared < .5f) axis = Vector3.Down;
float epsilon = MathF.Min(upper, lower) * .0001f;
// An engaged stock grip may sit just beyond the shorter arm's reach.
// Permit a small shoulder translation instead of stretching the limb or
// leaving a visible gap at the weapon. Explicit controller IK stays bounded.
if (shoulderReach && toward.Length > upper + lower - epsilon)
{
var shift = axis * MathF.Min(toward.Length - (upper + lower - epsilon), .5f * worldUnit);
root += shift; joint += shift; endpoint += shift;
toward = target.Position - root;
}
float distance = Math.Clamp(toward.Length, MathF.Abs(upper - lower) + epsilon, upper + lower - epsilon);
var reachable = root + axis * distance;
float along = (upper * upper - lower * lower + distance * distance) / (2 * distance);
var bend = joint - root;
bend -= axis * Vector3.Dot(bend, axis);
if (bend.LengthSquared < .000001f)
{
bend = constraintPose[start].Rotation.Forward;
bend -= axis * Vector3.Dot(bend, axis);
}
if (bend.LengthSquared < .000001f) bend = Vector3.Cross(axis, MathF.Abs(axis.z) < .9f ? Vector3.Up : Vector3.Left);
var newJoint = root + axis * along + bend.Normal * MathF.Sqrt(MathF.Max(0, upper * upper - along * along));
output[start] = new Transform(root, Rotation.FromToRotation(joint - root, newJoint - root) * constraintPose[start].Rotation, constraintPose[start].Scale);
output[middle] = new Transform(newJoint, Rotation.FromToRotation(endpoint - joint, reachable - newJoint) * constraintPose[middle].Rotation, constraintPose[middle].Scale);
// Retain the graph's evaluated hand/foot orientation and shortened limb
// lengths. An unreachable controller target never stretches the anatomy.
output[end] = new Transform(reachable, target.Rotation, constraintPose[end].Scale);
foreach (int i in chainBones[chain])
{
if (i == start || i == middle || i == end) continue;
int parent = parents[i];
output[i] = output[parent].ToWorld(constraintPose[parent].ToLocal(constraintPose[i]));
}
}
}