Data class for character teeth appearance. Stores upper and lower tooth style, linked-rows flag, color values, provides computed properties, validation, cloning, equality check, available styles, and methods to prepare/apply teeth morphs to a skinned model.
using System;
using System.Text.Json.Serialization;
namespace CharacterEditor;
/// <summary>Portable dental appearance. Omitted older data keeps the stock mouth.</summary>
public sealed class CharacterTeethData
{
// Keep the original upper-row field for portable appearances made before
// independent rows. Missing lower-row data always means stock lower teeth.
public string Style { get; set; } = "natural";
public string LowerStyle { get; set; } = "natural";
public bool LinkedRows { get; set; }
[JsonIgnore]
public string EffectiveLowerStyle => LinkedRows ? Style : LowerStyle;
public bool CustomColor { get; set; }
public float ColorR { get; set; } = 1;
public float ColorG { get; set; } = 1;
public float ColorB { get; set; } = 1;
[JsonIgnore]
public Color Color
{
get => new(ColorR, ColorG, ColorB);
set
{
ColorR = value.r;
ColorG = value.g;
ColorB = value.b;
}
}
[JsonIgnore]
public bool Valid => Styles.Any(x => x.Id == Style) && Styles.Any(x => x.Id == LowerStyle) && new[]
{
ColorR,
ColorG,
ColorB
}.All(x => float.IsFinite(x) && x >= 0 && x <= 1);
public CharacterTeethData DeepCopy() => new()
{
Style = Style,
LowerStyle = LowerStyle,
LinkedRows = LinkedRows,
CustomColor = CustomColor,
ColorR = ColorR,
ColorG = ColorG,
ColorB = ColorB
};
public static bool Same(CharacterTeethData a, CharacterTeethData b) => ReferenceEquals(a, b) || a is not null && b is not null && a.Style == b.Style && a.LowerStyle == b.LowerStyle && a.LinkedRows == b.LinkedRows && a.CustomColor == b.CustomColor && a.ColorR == b.ColorR && a.ColorG == b.ColorG && a.ColorB == b.ColorB;
public static IReadOnlyList<(string Id, string Label)> Styles { get; } = new[]
{
("natural", "Natural"),
("vampire", "Vampire"),
("missing_front", "Missing front tooth"),
("missing_multiple", "Missing teeth")
};
public static void Prepare(CharacterAppearanceData appearance)
{
if (appearance?.BaseShape is null && appearance?.Teeth is { } teeth && (teeth.Style != "natural" || teeth.EffectiveLowerStyle != "natural"))
UnifiedHuman.Enable(appearance);
}
public static void ApplyShape(SkinnedModelRenderer body, CharacterAppearanceData appearance)
{
if (appearance.BaseShape is null)
return;
for (int i = 1; i < Styles.Count; i++)
body.Morphs.Set("ce_teeth_" + Styles[i].Id, appearance.Teeth.Style == Styles[i].Id ? 1 : 0, 0);
for (int i = 1; i < Styles.Count; i++)
body.Morphs.Set("ce_teeth_lower_" + Styles[i].Id, appearance.Teeth.EffectiveLowerStyle == Styles[i].Id ? 1 : 0, 0);
}
}