Code/CharacterKit/CharacterGrowthPose.cs

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.

Native Interop
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]));
		}
	}
}