Code/CharacterKit/CharacterBaseShape.cs

CharacterBaseShape model and UnifiedHuman helper. CharacterBaseShape stores head blend weights, photo face blends, region blends and body parameters, provides validation, normalization and deep copy. UnifiedHuman contains constants and functions to enable unified human base shape, compute presentation skin, apply morphs to a SkinnedModelRenderer, pick head/body materials and body tone tints.

File Access
using System;

namespace CharacterEditor;
/// <summary>Version 2 identity shape, independent of skin. Weights are in human01..human07 order.</summary>
public sealed class CharacterBaseShape
{
	public float[] HeadWeights { get; set; } = new[]
	{
		0f,
		0f,
		1f,
		0f,
		0f,
		0f,
		0f
	};
	public List<PhotoFace> PhotoFaces { get; set; } = new();
	public Dictionary<string, FaceRegionBlend> Regions { get; set; } = new();

	[System.Text.Json.Serialization.JsonIgnore]
	public float TotalInfluence => HeadWeights.Sum() + (PhotoFaces?.Sum(x => x.Influence) ?? 0);

	public float NativeShare(int i) => TotalInfluence > 0 ? HeadWeights[i] / TotalInfluence : (i == 2 ? 1 : 0);
	public float PhotoShare(PhotoFace face) => TotalInfluence > 0 ? face.Influence / TotalInfluence : 0;
	public float MaleBody { get; set; } = 1;
	public string ClothingVariant { get; set; }
	public float BreastSize { get; set; } = .5f;
	public float BulgeSize { get; set; } = .5f;

	[System.Text.Json.Serialization.JsonIgnore]
	public bool Valid => HeadWeights?.Length == 7 && HeadWeights.All(Unit) && PhotoFaces is not null && PhotoFaces.Count <= 4 && PhotoFaces.All(x => x?.Valid == true) && PhotoFaces.Select(x => x.Id).Distinct().Count() == PhotoFaces.Count && Regions is not null && Regions.Count <= FaceRegions.Ids.Length && Regions.All(x => FaceRegions.Ids.Contains(x.Key) && x.Value?.Valid(PhotoFaces) == true) && Unit(MaleBody) && Unit(BreastSize) && Unit(BulgeSize) && ClothingVariant is null or "male" or "female";

	static bool Unit(float x) => float.IsFinite(x) && x >= 0 && x <= 1;
	public float[] Normalized()
	{
		if (!Valid)
			return new[]
			{
				0f,
				0f,
				1f,
				0f,
				0f,
				0f,
				0f
			};
		float sum = TotalInfluence;
		return sum > 0 ? HeadWeights.Select((x, i) => (x + PhotoFaces.Sum(f => f.Influence * f.HeadWeights[i])) / sum).ToArray() : new[]
		{
			0f,
			0f,
			1f,
			0f,
			0f,
			0f,
			0f
		};
	}

	public CharacterBaseShape DeepCopy() => new()
	{
		HeadWeights = HeadWeights?.ToArray(),
		PhotoFaces = PhotoFaces?.Select(x => x?.DeepCopy()).ToList(),
		Regions = Regions?.ToDictionary(x => x.Key, x => x.Value.DeepCopy()),
		MaleBody = MaleBody,
		BreastSize = BreastSize,
		BulgeSize = BulgeSize,
		ClothingVariant = ClothingVariant
	};
}

public static class UnifiedHuman
{
	public const string ModelPath = "models/characterkit/unified_human.vmdl";
	public static void Enable(CharacterAppearanceData data)
	{
		if (data.BaseShape is not null)
			return;
		bool female = AppearanceCatalog.IsFemale(data);
		var index = AppearanceCatalog.Skins.ToList().FindIndex(s => s.ResourcePath == data.SkinPath);
		data.BaseShape = new()
		{
			MaleBody = female ? 0 : 1,
			HeadWeights = new float[7],
			BreastSize = female ? data.BodyFeatureSize : .5f,
			BulgeSize = female ? .5f : data.BodyFeatureSize
		};
		data.BaseShape.HeadWeights[Math.Max(0, index)] = 1;
		data.Version = 2;
	}

	public static float MaleAmount(CharacterAppearanceData a) => a.BaseShape?.MaleBody ?? (AppearanceCatalog.IsFemale(a) ? 0 : 1);
	public static string PresentationSkin(CharacterAppearanceData a) => a.BaseShape is null ? a.SkinPath : (MaleAmount(a) >= .5f ? "models/citizen_clothes/body/human03.clothing" : "models/citizen_clothes/body/human04.clothing");
	public static void Apply(SkinnedModelRenderer body, CharacterAppearanceData a)
	{
		if (a.BaseShape is null)
			return;
		var weights = FaceRegions.ReferenceWeights(a.BaseShape);
		for (int i = 0; i < 7; i++)
			if (i != 2)
				body.Morphs.Set($"ce_human{i + 1:00}", weights[i]);
		FaceRegions.Apply(body, a.BaseShape, weights);
		body.Morphs.Set("ce_body_female", 1 - a.BaseShape.MaleBody);
	}

	public static string HeadMaterial(string skin)
	{
		var id = System.IO.Path.GetFileNameWithoutExtension(skin);
		var suffix = id switch
		{
			"human01" => "malehead01",
			"human02" => "femalehead01",
			"human03" => "malehead02",
			"human04" => "femalehead02",
			"human05" => "malehead03",
			"human06" => "malehead04",
			_ => "malehead05"
		};
		return $"models/citizen_human/heads/citizen_human_{suffix}/citizen_human_{suffix}.vmat";
	}

	public static Material SkinMaterial(Material slot, string skin, bool unifiedBody = true)
	{
		var original = HumanMarkingModels.NativeMaterial(slot);
		if (original.Name.Contains("/heads/") && original.ShaderName == "shaders/skin.shader" && !original.Name.EndsWith("/mouth.vmat"))
			return Material.Load(HeadMaterial(skin));
		if (original.Name.Contains("/bodies/") && original.ShaderName == "shaders/skin.shader")
		{
			// The unified surface uses one body material family. Female chest normal maps
			// encode breast contours that become distorted on the flattened male endpoint.
			var id = System.IO.Path.GetFileNameWithoutExtension(skin);
			bool female = !unifiedBody && (id is "human02" or "human04"), dark = id is "human01" or "human02" or "human06";
			return Material.Load($"models/citizen_human/bodies/{(female ? "female" : "male")}/{(female ? (dark ? "female_body_mode_dark" : "female_body_mode_light") : (dark ? "body_mode_dark" : "body_male_light"))}.vmat");
		}

		return original;
	}

	public static Color BodyToneTint(Material material, string skin)
	{
		if (!material.Name.Contains("/bodies/"))
			return Color.White;
		return BodyToneTint(skin);
	}

	public static Color BodyToneTint(string skin)
	{
		// Albedo ratios of the native female/male materials sampled just inside
		// the shared neck boundary. Keep the female faces' authored complexion.
		return System.IO.Path.GetFileNameWithoutExtension(skin) switch
		{
			"human02" => new Color(1.0192f, .9194f, .8924f),
			"human04" => new Color(1.0856f, 1.0137f, .8969f),
			_ => Color.White
		};
	}
}