Static data and utilities for photo-based face correction controls used by the character editor. It defines a Control record with spatial parameters, exposes a cached Controls array built from literal entries, computes VolumeCount, and computes a control Amount from a CharacterAppearanceData instance by summing weighted region contributions.
using System;
namespace CharacterEditor;
/// <summary>Bounded identity corrections beyond the original cosmetic sliders.
/// They use native rest-space volumes, including eyes, inner mouth and merged clothing.</summary>
public static class PhotoFaceCorrections
{
public sealed record Control(string Id, string Region, Vector3 Center, Vector3 Mask, Vector3 ScaleDelta, Vector3 Translation, bool Paired = false, bool Fine = false, bool Depth = false);
const int Revision = 4;
static int loadedRevision;
static Control[] cached;
public static Control[] Controls
{
get
{
if (cached is null || loadedRevision != Revision)
{
cached = CreateControls();
loadedRevision = Revision;
}
return cached;
}
}
static Control[] CreateControls() => new Control[]
{
new("facewidth", null, new(2, 0, 64), new(7, 6, 6), new(0, .22f, 0), Vector3.Zero),
new("faceheight", null, new(3, 0, 63), new(6, 5, 5), new(0, 0, .16f), Vector3.Zero),
new("jawwidth", "jaw", new(3, 0, 60.5f), new(4, 4, 1.8f), new(0, .35f, 0), Vector3.Zero),
new("cheekwidth", "cheeks", new(3, 0, 63), new(4, 4.7f, 2), new(0, .25f, 0), Vector3.Zero),
new("eyespan", "eyes", new(4.4f, 1.65f, 64.5f), new(3, 1.9f, 1.5f), Vector3.Zero, new(0, .16f, 0), true),
new("eyelevel", "eyes", new(4.4f, 1.65f, 64.5f), new(3, 1.9f, 1.5f), Vector3.Zero, new(0, 0, .16f), true),
new("mouthlevel", "mouth", new(4.3f, 0, 61.8f), new(2.8f, 3, 1.6f), Vector3.Zero, new(0, 0, .25f)),
new("noselevel", "nose", new(5.3f, 0, 63.5f), new(2, 1.9f, 1.6f), Vector3.Zero, new(0, 0, .2f)),
// Lid caps fade out before the front of the iris. Moving a whole eye to
// change aperture would also squash the iris and couple width to height.
new("upperlid", "eyes", new(4.35f, 1.4f, 64.7f), new(.75f, 1.15f, .6f), Vector3.Zero, new(0, 0, .15f), true, true),
new("lowerlid", "eyes", new(4.35f, 1.4f, 64.15f), new(.75f, 1.15f, .5f), Vector3.Zero, new(0, 0, .10f), true, true),
new("outereyeheight", "eyes", new(4.25f, 2.0f, 64.4f), new(1.0f, .75f, .7f), Vector3.Zero, new(0, 0, .17f), true, true),
new("outereyewidth", "eyes", new(4.25f, 2.0f, 64.4f), new(1.0f, .75f, .7f), Vector3.Zero, new(0, .18f, 0), true, true),
new("innereyeheight", "eyes", new(4.6f, .75f, 64.35f), new(.8f, .55f, .55f), Vector3.Zero, new(0, 0, .10f), true, true),
new("innerbrow", "forehead", new(4.75f, .75f, 65.0f), new(1.3f, .9f, .7f), Vector3.Zero, new(0, 0, .20f), true, true),
new("outerbrow", "forehead", new(4.2f, 2.2f, 65.05f), new(1.4f, 1.05f, .8f), Vector3.Zero, new(0, 0, .20f), true, true),
new("bridgewidth", "nose", new(5.3f, 0, 64.0f), new(1.1f, 1.15f, 1.0f), new(0, .30f, 0), Vector3.Zero, false, true),
new("rootdepth", "nose", new(5.1f, 0, 64.5f), new(1.0f, .9f, .8f), Vector3.Zero, new(.20f, 0, 0), false, true, true),
new("tipwidth", "nose", new(6.0f, 0, 63.1f), new(.8f, .9f, .55f), new(0, .30f, 0), Vector3.Zero, false, true),
new("mouthcornerheight", "mouth", new(5.1f, 1.1f, 61.6f), new(1.0f, .85f, .65f), Vector3.Zero, new(0, 0, .18f), true, true),
new("mouthcornerwidth", "mouth", new(5.1f, 1.1f, 61.6f), new(1.0f, .85f, .65f), Vector3.Zero, new(0, .18f, 0), true, true),
new("cupidheight", "mouth", new(5.7f, 0, 61.95f), new(.8f, .65f, .35f), Vector3.Zero, new(0, 0, .12f), false, true),
new("lowerlipcurve", "mouth", new(5.6f, 0, 61.3f), new(.8f, .95f, .4f), Vector3.Zero, new(0, 0, .12f), false, true),
new("jawangleheight", "jaw", new(1.9f, 2.7f, 61.6f), new(1.8f, 1.25f, 1.5f), Vector3.Zero, new(0, 0, .25f), true, true),
new("jawcontourwidth", "jaw", new(3.0f, 2.2f, 61.0f), new(2.2f, 1.45f, 1.4f), Vector3.Zero, new(0, .30f, 0), true, true),
new("templewidth", "forehead", new(2.5f, 3.0f, 65.8f), new(3.0f, 1.6f, 1.7f), Vector3.Zero, new(0, .30f, 0), true, true),
new("chinpointwidth", "jaw", new(5.0f, 0, 60.2f), new(1.3f, 2.0f, .85f), new(0, .35f, 0), Vector3.Zero, false, true),
// Ridge projection is independent of eyebrow height and nasal-root depth.
// The lower falloff stays above the eyeballs and leaves the nose tip alone.
new("browdepth", "forehead", new(4.8f, 0, 65.7f), new(1.8f, 3.4f, 1.5f), Vector3.Zero, new(.20f, 0, 0), false, true, true)
};
public static int VolumeCount => Controls.Sum(c => c.Paired ? 2 : 1);
public static float Amount(CharacterAppearanceData appearance, Control control)
{
var shape = appearance.BaseShape;
return shape is null ? 0 : Math.Clamp(shape.PhotoFaces.Sum(face => FaceRegions.PhotoShare(shape, face, control.Region) * face.Strength * face.Corrections.GetValueOrDefault(control.Id)), -1, 1);
}
}