Face region blending utilities for a character editor. Defines FaceRegionBlend which stores overall influence, head and photo weights and validation/copy helpers, and static FaceRegions which lists region ids and provides lookup, weight composition, application to model morphs, and helpers for photo/native shares.
using System;
namespace CharacterEditor;
/// <summary>Optional regional identity weights. An empty region inherits the overall blend.</summary>
public sealed class FaceRegionBlend
{
// Zero preserves older saved overrides. New UI regions start at the midpoint.
public float OverallInfluence { get; set; }
public float[] HeadWeights { get; set; } = new float[7];
public Dictionary<string, float> PhotoWeights { get; set; } = new();
[System.Text.Json.Serialization.JsonIgnore]
public float Total => OverallInfluence + HeadWeights.Sum() + PhotoWeights.Values.Sum();
public bool Valid(IReadOnlyList<PhotoFace> faces) => Unit(OverallInfluence) && HeadWeights?.Length == 7 && HeadWeights.All(Unit) && PhotoWeights is not null && PhotoWeights.Count <= 4 && PhotoWeights.All(x => Unit(x.Value) && faces.Any(f => f.Id == x.Key));
static bool Unit(float x) => float.IsFinite(x) && x >= 0 && x <= 1;
public FaceRegionBlend DeepCopy() => new()
{
OverallInfluence = OverallInfluence,
HeadWeights = HeadWeights.ToArray(),
PhotoWeights = new(PhotoWeights)
};
}
public static class FaceRegions
{
public static readonly string[] Ids =
{
"nose",
"jaw",
"forehead",
"ears",
"eyes",
"mouth",
"cheeks",
"throat"
};
// Throat reuses the existing lower-neck complement, including its pinned
// collar boundary. Retain asset names so older portable kits remain compatible.
public static string MorphRegion(string region) => region == "throat" ? "neck" : region;
public static string Label(string id) => id switch
{
"overall" => "Overall",
"jaw" => "Jaw & chin",
_ => char.ToUpperInvariant(id[0]) + id[1..]
};
public static string ForSlider(string id) => id switch
{
"nose" or "nosewidth" or "nostrils" or "nostrilfullness" or "noseheight" or "columella" => "nose",
"jaw" or "chin" or "chinwidth" or "chinheight" or "submental" => "jaw",
"forehead" or "brow" => "forehead",
"ears" or "eartaper" => "ears",
"eyes" => "eyes",
"mouthwidth" or "mouthprojection" or "upperlip" or "lowerlip" or "philtrum" => "mouth",
"cheeks" or "cheekbones" => "cheeks",
_ => null
};
public static FaceRegionBlend Active(CharacterBaseShape shape, string region) => region is not null && shape.Regions.TryGetValue(region, out var r) && r.Total > 0 ? r : null;
public static float PhotoShare(CharacterBaseShape shape, PhotoFace face, string region)
{
var r = Active(shape, region);
return r is null ? shape.PhotoShare(face) : (r.OverallInfluence * shape.PhotoShare(face) + r.PhotoWeights.GetValueOrDefault(face.Id)) / r.Total;
}
public static float OverallShare(CharacterBaseShape shape, string region) => Active(shape, region)is { } r ? r.OverallInfluence / r.Total : 1;
public static float NativeShare(CharacterBaseShape shape, int index, string region)
{
if (!Ids.Contains(region))
return shape.NativeShare(index);
var r = Active(shape, region);
return r is null ? 0 : r.HeadWeights[index] / r.Total;
}
public static float PhotoEntryShare(CharacterBaseShape shape, PhotoFace face, string region)
{
if (!Ids.Contains(region))
return shape.PhotoShare(face);
var r = Active(shape, region);
return r is null ? 0 : r.PhotoWeights.GetValueOrDefault(face.Id) / r.Total;
}
public static float[] Weights(CharacterBaseShape shape, string region)
{
var r = Active(shape, region);
if (r is null)
return shape.Normalized();
var overall = shape.Normalized();
return r.HeadWeights.Select((w, i) => (r.OverallInfluence * overall[i] + w + shape.PhotoFaces.Sum(f => r.PhotoWeights.GetValueOrDefault(f.Id) * f.HeadWeights[i])) / r.Total).ToArray();
}
public static void RemovePhoto(CharacterBaseShape shape, PhotoFace face)
{
foreach (var r in shape.Regions.Values)
r.PhotoWeights.Remove(face.Id);
shape.PhotoFaces.Remove(face);
}
public static void Apply(SkinnedModelRenderer body, CharacterBaseShape shape, float[] overall)
{
foreach (var region in Ids)
{
var weights = Weights(shape, region);
var morphRegion = MorphRegion(region);
for (int i = 0; i < 7; i++)
if (i != 2)
{
float delta = weights[i] - overall[i];
// Separate signed shapes avoid relying on negative engine morph weights.
body.Morphs.Set($"ce_{morphRegion}_human{i + 1:00}_add", Math.Max(0, delta));
body.Morphs.Set($"ce_{morphRegion}_human{i + 1:00}_subtract", Math.Max(0, -delta));
}
}
}
// Rebase on the regions to avoid quantized global/add/subtract cancellation
// across the whole head. The complementary neck shapes retain Overall there.
public static float[] ReferenceWeights(CharacterBaseShape shape)
{
// Keep the reference independent of Throat, so changing it cannot introduce
// rounding/shading changes on the rest of the face.
var regions = Ids.Where(r => r != "throat").Select(r => Weights(shape, r)).ToArray();
return Enumerable.Range(0, 7).Select(i => regions.Average(w => w[i])).ToArray();
}
}