CharacterKit/UnderwearPresentation.cs
using System;
using System.Threading.Tasks;
namespace CharacterEditor;
/// <summary>Updated native fabric artwork on the existing, morphable underwear meshes.</summary>
public sealed class UnderwearPresentation : IDisposable
{
public const string UpdatedMaterialPath = "materials/makeup/underwear/updated.vmat";
public const string OriginalMaterialPath = "models/citizen_human/bodies/underwear.vmat";
public static bool IsUnderwearMaterial(Material material) => material?.Name is UpdatedMaterialPath or OriginalMaterialPath;
public static bool CoversTop(Clothing item)
{
if (item is null) return false;
if (item.HideBody.ToString().Split(',').Any(x => x.Trim() == "Chest")) return true;
// Cloud tops often retain the visible body mesh instead of hiding Chest.
// That does not mean the built-in bra should render through their fabric.
return item.Category.ToString() is "Tops" or "TShirt" or "Sweatshirt" or "Hoodie" or "Shirt" or "Knitwear" or "Dress" or "Jumpsuit";
}
static Material updated, original;
SkinnedModelRenderer target;
Model model;
readonly HashSet<int> overrides = new();
public static async Task LoadAsync(CharacterAppearanceData data)
{
if (data?.BaseShape is null)
return;
if (data.UnderwearStyle == "original")
{
if (original is { IsError: false }) return;
original = await Material.LoadAsync(OriginalMaterialPath);
if (original is null || original.IsError)
throw new InvalidOperationException("The original underwear material is unavailable.");
return;
}
if (updated is not null) return;
var carrier = await Material.LoadAsync(UpdatedMaterialPath);
var source = await Material.LoadAsync("models/citizen_human/bodies/female/female_body_mode_light.vmat");
if (carrier is null || carrier.IsError || source is null || source.IsError)
throw new InvalidOperationException("The updated underwear material is unavailable.");
updated = carrier.CreateCopy("characterkit-updated-underwear.vmat");
updated.Set("g_tNormal", source.GetTexture("g_tNormal"));
}
public void Apply(SkinnedModelRenderer body, CharacterAppearanceData data)
{
if (!body.IsValid() || body.Model is null || data is null)
return;
if (target != body || model != body.Model)
{
Dispose();
target = body;
model = body.Model;
}
bool useOriginal = data.UnderwearStyle == "original";
if (!useOriginal && updated is null)
return;
for (int i = 0; i < body.Materials.Count; i++)
{
var slot = body.Materials.GetOriginal(i);
if (!IsUnderwearMaterial(slot))
continue;
// Unified meshes now compile with the cutout as their source material.
// Original remains an explicit opaque override; legacy source slots can
// still restore their own original without retaining any override.
var desired = useOriginal ? slot.Name == UpdatedMaterialPath ? original : null : updated;
if (useOriginal && slot.Name == UpdatedMaterialPath && original is null) continue;
if (body.Materials.GetOverride(i) != desired)
body.Materials.SetOverride(i, desired);
if (desired is null) overrides.Remove(i);
else overrides.Add(i);
}
}
public void Dispose()
{
if (target.IsValid() && target.Model == model)
foreach (var i in overrides)
target.Materials.SetOverride(i, null);
overrides.Clear();
}
}