Code/CharacterKit/BodyRegions.cs

Defines BodyRegionBlend for per-region gender blend weights and BodyRegions static utilities for body-region IDs, labels, masking by vertex position, morph naming, and applying gender/maturity morphs to a SkinnedModelRenderer based on CharacterAppearanceData.

Native Interop
using System;

namespace CharacterEditor;

/// <summary>Relative regional weights. An absent or empty override follows the overall body.</summary>
public sealed class BodyRegionBlend
{
	public float OverallInfluence { get; set; } = 1;
	public float FemaleInfluence { get; set; }
	public float MaleInfluence { get; set; }
	[System.Text.Json.Serialization.JsonIgnore]
	public float Total => OverallInfluence + FemaleInfluence + MaleInfluence;
	[System.Text.Json.Serialization.JsonIgnore]
	public bool Valid => Unit(OverallInfluence) && Unit(FemaleInfluence) && Unit(MaleInfluence);
	static bool Unit(float value) => float.IsFinite(value) && value >= 0 && value <= 1;
	public float Resolve(float overall) => Total > 0 ? (OverallInfluence * overall + MaleInfluence) / Total : overall;
	public BodyRegionBlend DeepCopy() => new() { OverallInfluence = OverallInfluence, FemaleInfluence = FemaleInfluence, MaleInfluence = MaleInfluence };
}

/// <summary>Shared rest-space masks for native body endpoints and fitted garments.</summary>
public static class BodyRegions
{
	public static readonly string[] Ids = { "figure", "chest", "shoulders", "upperarms", "forearms", "hands", "thighs", "calves", "feet" };
	public static string Label(string id) => id switch
	{
		"overall" => "Overall", "figure" => "Figure & hips", "chest" => "Chest",
		"shoulders" => "Shoulders", "upperarms" => "Upper arms", "forearms" => "Forearms",
		"hands" => "Hands", "thighs" => "Thighs", "calves" => "Calves", "feet" => "Feet", _ => id
	};
	public static float MaleAmount(CharacterAppearanceData appearance, string region)
	{
		float overall = UnifiedHuman.MaleAmount(appearance);
		return appearance.BaseShape?.BodyOverrides?.TryGetValue(region, out var blend) == true ? blend.Resolve(overall) : overall;
	}
	public static string Morph(string region, bool subtract = false) => $"ce_gender_{region}_{(subtract ? "subtract" : "add")}";
	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);
	}
	// A smooth partition (sum <= 1). The head/neck retain Overall. The authoring
	// exporter uses these same equations for the compiled body morphs.
	public static float Mask(string region, Vector3 point)
	{
		float z = point.z, y = MathF.Abs(point.y);
		float legs = 1 - Smooth(32, 38, z), upper = 1 - legs;
		float armShare = Smooth(7.5f, 11.5f, y) * (1 - Smooth(53, 57, z));
		float arm = upper * armShare, torso = upper * (1 - armShare);
		float shoulder = Smooth(4.5f, 7.5f, y) * Smooth(49, 53, z) * (1 - Smooth(58, 60, z));
		// Keep the waist with Figure; share only the upper abdomen/lower ribcage.
		// Positive morph composition below handles the shading at this join.
		float chestShare = Smooth(42, 49, z);
		return region switch
		{
			"feet" => legs * (1 - Smooth(4, 7, z)),
			"calves" => legs * Smooth(4, 7, z) * (1 - Smooth(20, 24, z)),
			"thighs" => legs * Smooth(20, 24, z),
			"hands" => arm * (1 - Smooth(35, 39, z)),
			"forearms" => arm * Smooth(35, 39, z) * (1 - Smooth(43, 47, z)),
			"upperarms" => arm * Smooth(43, 47, z) * (1 - Smooth(49, 53, z)),
			"shoulders" => arm * Smooth(49, 53, z) + torso * shoulder,
			"chest" => torso * (1 - shoulder) * chestShare * (1 - Smooth(56, 60, z)),
			"figure" => torso * (1 - shoulder) * (1 - chestShare),
			_ => 0
		};
	}
	public static void Apply(SkinnedModelRenderer renderer, CharacterAppearanceData appearance)
	{
		if (appearance.BaseShape is null) return;
		float overallFemale = 1 - appearance.BaseShape.MaleBody;
		float child = CharacterMaturity.ChildAmount(appearance.BodyMaturity);
		// The school-age torso has not acquired the adult chest/waist contours.
		// Resolve this in the shared positive morph partition so garments and
		// exposed skin use the same smooth chest-to-figure transition. Saved
		// gender weights remain intact and return continuously with maturity.
		float Female(string region)
		{
			float adult = 1 - MaleAmount(appearance, region);
			return adult * (1 - (region == "chest" ? .94f : region == "figure" ? .70f : 0) * child);
		}
		// Some imported morph normal frames cannot be undone by a mirrored target.
		// Start from the least-female requested region and only add the remaining
		// positive differences. This is the same positional partition, with no
		// female/subtract cancellation. Unmodified bodies keep their native morph.
		float baseline = MathF.Min(overallFemale, Ids.Min(Female));
		renderer.Morphs.Set("ce_body_female", baseline, 0);
		foreach (var region in Ids)
		{
			renderer.Morphs.Set(Morph(region), MathF.Max(0, Female(region) - baseline), 0);
			renderer.Morphs.Set(Morph(region, true), 0, 0);
		}
		renderer.Morphs.Set(Morph("remainder"), overallFemale - baseline, 0);
	}
}