CharacterKit/CharacterBaseShape.cs

Defines CharacterBaseShape which stores head blend weights, photo-based face blends, region blends, body/facial feature scalars and validation/normalization/clone helpers. Also defines UnifiedHuman static helper for applying unified human model settings, choosing materials and skin/head mappings based on skin id and gender.

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
		};
	}
}