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