Code/CharacterKit/CharacterStature.cs

Static utility for converting character appearance parameters to scale factors used by renderers and animation. Computes uniform Height, GrowthStrideScale, combined StrideScale, and helpers to convert world ground speed into animation-native speed, with fallbacks for invalid inputs.

using System;

namespace CharacterEditor;

/// <summary>
/// Appearance-to-stature conversion shared by previews and world avatars.
/// Overall size and the native human graph's body proportions remain separate.
/// These calculations do not own animation clips, playback clocks or bone poses.
/// </summary>
public static class CharacterStature
{
	/// <summary>
	/// Uniform body-object scale. The saved midpoint is exactly the existing size;
	/// the same transform carries the body, merged clothing and attachments.
	/// Multiply the caller's original local scale by this value, never its last
	/// already-scaled value. Invalid input falls back to the unchanged adult size.
	/// </summary>
	public static float Height(CharacterAppearanceData appearance)
	{
		float value = appearance?.Height ?? .5f;
		if (!float.IsFinite(value)) value = .5f;
		return .8f + .4f * Math.Clamp(value, 0, 1);
	}

	/// <summary>Fallback leg ratio before a renderer's measured growth pose exists.</summary>
	public static float GrowthStrideScale(CharacterAppearanceData appearance)
		=> CharacterGrowthPose.EstimateStrideFactor(appearance?.BodyMaturity ?? 1);

	/// <summary>Combined appearance scale of a ground-plane walking stride.</summary>
	public static float StrideScale(CharacterAppearanceData appearance)
		=> Height(appearance) * GrowthStrideScale(appearance);

	/// <summary>
	/// Combines the actual applied uniform world scale and the configured growth
	/// pose's measured StrideFactor. Pass the controller's coordinate-space scale
	/// explicitly when it supplies local-space rather than world-space speeds.
	/// Invalid optional values use the appearance-only estimates.
	/// </summary>
	public static float StrideScale(CharacterAppearanceData appearance, float actualUniformScale, float actualGrowthStride)
	{
		float uniform = float.IsFinite(actualUniformScale) && actualUniformScale > .001f ? actualUniformScale : Height(appearance);
		float growth = float.IsFinite(actualGrowthStride) && actualGrowthStride > .001f ? actualGrowthStride : GrowthStrideScale(appearance);
		float combined = uniform * growth;
		return float.IsFinite(combined) && combined > .001f ? combined : 1;
	}

	/// <summary>
	/// Converts an unmodified world-space ground speed to native animation speed.
	/// Call once on the controller's input, or restore the original input before
	/// each controller update and apply after it; repeatedly scaling a renderer's
	/// existing parameter would compound. Keep gait selection, directional blend
	/// samples and the global animation playback rate under the controller's control.
	/// </summary>
	public static float CompensateGroundSpeed(float worldSpeed, CharacterAppearanceData appearance)
		=> CompensateGroundSpeed(worldSpeed, StrideScale(appearance));

	/// <summary>Converts raw speed using an already measured world stride scale.</summary>
	public static float CompensateGroundSpeed(float worldSpeed, float strideScale)
	{
		if (!float.IsFinite(worldSpeed)) return 0;
		if (!float.IsFinite(strideScale) || strideScale <= .001f || strideScale == 1) return worldSpeed;
		float adjusted = worldSpeed / strideScale;
		return float.IsFinite(adjusted) ? adjusted : worldSpeed;
	}
}