Code/CharacterKit/CharacterMaturity.cs

Utility class providing surface deformation presets for face and body "maturity" edits. It computes a child interpolation factor from a maturity float and returns arrays of SurfaceEdit records that describe ellipsoid masks, scales and translations for applying age-related soft-tissue changes to character appearance.

using System;

namespace CharacterEditor;

/// <summary>
/// Conservative school-age to adult surface proportions, applied after identity
/// and feature edits. These are authoring controls, not an estimate of age.
/// Limb lengths and overall stature remain the native skeleton's responsibility.
/// </summary>
public static class CharacterMaturity
{
	// Same rest-space ellipsoid convention as CharacterDeformation. Translation
	// is in normalized mask coordinates; the native volume applies Mask afterward.
	public readonly record struct SurfaceEdit(string Name, Vector3 Center, Vector3 Mask, Vector3 Scale, Vector3 Translation);

	/// <summary>0 = adult, 1 = the school-age endpoint. Midpoint retains 25% of the change.</summary>
	public static float ChildAmount(float maturity)
	{
		if (!float.IsFinite(maturity)) return 0;
		float t = Math.Clamp(maturity, 0, 1);
		float remaining = 1 - t * t * (3 - 2 * t);
		return remaining * remaining;
	}

	/// <summary>
	/// Keep the list stable at the neutral endpoint so edits do not recreate the
	/// volume hierarchy. Adult coefficients are exact identities and are disabled
	/// by CharacterDeformation, including in all older saved appearances.
	/// </summary>
	public static SurfaceEdit[] FaceEdits(CharacterAppearanceData appearance)
	{
		float child = ChildAmount(appearance.EffectiveFaceMaturity);
		return new[]
		{
			// Shorten the lower face toward the eye plane. The ellipsoid ends
			// below the eyes and fades before the chest/head attachment boundary.
			// Paired-photo evidence supports roughly 6–7% eye-to-chin shortening
			// at the school-age endpoint; the field must be measured on each face.
			new SurfaceEdit("Facial maturity lower face", new(3.2f, 0, 61.1f), new(5.5f, 4.4f, 3.2f),
				new(1, 1, 1 - .075f * child), new(0, 0, .07734375f * child)),
			// Profile changes are separate from vertical shortening. Retain the
			// selected identity's nose width, curvature, nostrils and bridge.
			new SurfaceEdit("Facial maturity nose", new(5.7f, 0, 63.05f), new(1.7f, 1.5f, 1.2f),
				Vector3.One, new(-.12f * child, 0, 0)),
			new SurfaceEdit("Facial maturity nasal root", new(5.1f, 0, 64.15f), new(1.4f, 1.05f, .85f),
				Vector3.One, new(-.035f * child, 0, 0)),
			new SurfaceEdit("Facial maturity chin", new(4.4f, 0, 60.1f), new(2, 2.4f, 1.55f),
				Vector3.One, new(-.055f * child, 0, 0)),
			// Jaw width has a smaller age effect than lower-face height. Keep
			// this gentle so a broad adult jaw remains a broad child jaw.
			new SurfaceEdit("Facial maturity jaw width", new(3, 0, 60.65f), new(4.5f, 4.1f, 2),
				new(1, 1 - .024f * child, 1), Vector3.Zero),
			// These small soft-tissue/ridge adjustments are art direction informed
			// by the reference profiles, not measured universal growth rates. The
			// masks end above/below the eye sockets so eyelid alignment is retained.
			new SurfaceEdit("Facial maturity brow softness", new(4.8f, 0, 65.7f), new(1.8f, 3.4f, 1.4f),
				Vector3.One, new(-.045f * child, 0, 0)),
			new SurfaceEdit("Facial maturity left cheek", new(4, 2.1f, 62.6f), new(2.4f, 2, 1.45f),
				Vector3.One, new(.045f * child, .02f * child, 0)),
			new SurfaceEdit("Facial maturity right cheek", new(4, -2.1f, 62.6f), new(2.4f, 2, 1.45f),
				Vector3.One, new(.045f * child, -.02f * child, 0))
		};
	}

	/// <summary>
	/// Gently reduce adult torso contours without changing the selected gender
	/// blend or overwriting manual features. Broad overlapping volumes apply the
	/// identical field to the body, underwear and bone-merged clothes. No volume
	/// changes leg length, foot height, or animation joint positions.
	/// </summary>
	public static SurfaceEdit[] BodyEdits(CharacterAppearanceData appearance)
	{
		float child = ChildAmount(appearance.BodyMaturity);
		float chestFemale = 1 - BodyRegions.MaleAmount(appearance, "chest");
		float figureFemale = 1 - BodyRegions.MaleAmount(appearance, "figure");
		return new[]
		{
			// A broad upper-body envelope avoids a band between chest and waist.
			// Narrowing is supplemental to native bone proportions, not a new rig.
			new SurfaceEdit("Body maturity upper torso", new(0, 0, 50.5f), new(9, 15, 10),
				new(1 - .09f * child, 1 - .11f * child, 1), Vector3.Zero),
			// One front envelope spans both sides of the chest and its lower
			// transition. Pull the mature contour inward while preserving the
			// ribcage; separate left/right caps can leave a crease between them.
			new SurfaceEdit("Body maturity chest contour", new(2.5f, 0, 49.8f), new(4, 8.5f, 6),
				new(1 - (.06f + .08f * chestFemale) * child, 1, 1), Vector3.Zero),
			// A little less waist-to-hip contrast at the child endpoint, with
			// generous falloff across the abdomen and the chest/legs mesh split.
			new SurfaceEdit("Body maturity waist", new(0, 0, 43.5f), new(8, 10, 8),
				new(1 - .02f * child, 1 + .035f * figureFemale * child, 1), Vector3.Zero),
			new SurfaceEdit("Body maturity hips", new(0, 0, 35.5f), new(9, 11, 9),
				new(1 - .03f * child, 1 - (.04f + .025f * figureFemale) * child, 1), Vector3.Zero)
		};
	}
}