Static helper that loads and caches materials and textures used for human makeup cosmetics. It validates shader schema and texture sizes, exposes accessors for mask, eye detail, liner detail, marking, and surface textures, and loads assets asynchronously into dictionaries.
using System;
using System.Threading.Tasks;
namespace CharacterEditor;
/// <summary>Native compiled materials carry the mask textures and compute shader to remote clients.</summary>
public static class HumanMakeupAssets
{
public const string ShaderPath = "shaders/fate_pending/human_cosmetics.shader";
public const string CarrierPath = "materials/makeup/cosmetics_compositor.vmat";
static readonly Dictionary<string, Material> masks = new();
static readonly Dictionary<string, Material> markings = new();
static Material compositor;
static Texture linerDetailTexture;
static readonly Dictionary<string, Material> eyeDetails = new();
static readonly Dictionary<string, Texture> surfaces = new();
public static async Task EnsureLoadedAsync(string id)
{
compositor ??= await Material.LoadAsync(CarrierPath);
var variables = compositor?.Shader?.Schema?.Variables.Select(x => x.Name).ToArray() ?? Array.Empty<string>();
if (compositor?.ShaderName != ShaderPath || !compositor.Shader.IsValid || !new[]
{
"EyeDetail",
"LinerDetail",
"LinerOptions",
"WingTip",
"Liner",
"LowerEye"
}.All(variables.Contains) || variables.Contains("SkinTint"))
{
compositor = null;
throw new InvalidOperationException("The current human cosmetics shader is unavailable or has an incompatible schema.");
}
if (!masks.TryGetValue(id, out var material) || material is null)
{
material = await Material.LoadAsync($"materials/makeup/{id}.vmat");
var texture = material?.GetTexture("g_tColor");
if (texture is null || texture.IsError || texture.Width != 2048 || texture.Height != 2048)
throw new InvalidOperationException("A human makeup mask is unavailable: " + id);
masks[id] = material;
}
if (!eyeDetails.ContainsKey(id))
{
var detail = await Material.LoadAsync($"materials/makeup/{id}_eye.vmat");
if (detail?.GetTexture("g_tColor")is not { IsError: false, Width: 2048, Height: 2048 })
throw new InvalidOperationException("An eye makeup mask is unavailable: " + id);
eyeDetails[id] = detail;
}
if (linerDetailTexture is null)
{
linerDetailTexture = await Texture.LoadAsync("textures/makeup/eyeliner_details.vtex");
if (linerDetailTexture is not { IsError: false, Width: 2048, Height: 2048 })
{
linerDetailTexture = null;
throw new InvalidOperationException("The eyeliner detail mask is unavailable.");
}
}
if (!markings.ContainsKey(id))
{
var marking = await Material.LoadAsync($"materials/makeup/{id}_marking.vmat");
if (marking?.GetTexture("g_tColor")is not { IsError: false, Width: 2048, Height: 2048 })
throw new InvalidOperationException("A human marking mask is unavailable: " + id);
markings[id] = marking;
}
foreach (var surface in new[]
{
id,
id is "human02" or "human04" ? "female" : "male"
}
)
foreach (var kind in new[]
{
"position_hi",
"position_lo",
"normal"
}
)
{
var key = surface + "_" + kind;
if (surfaces.ContainsKey(key))
continue;
var texture = await Texture.LoadAsync($"textures/makeup/{key}.vtex");
if (texture is not { IsError: false, Width: 1024, Height: 1024 })
throw new InvalidOperationException("A marking surface is unavailable: " + key);
surfaces[key] = texture;
}
}
public static Texture Mask(string skinPath) => masks[System.IO.Path.GetFileNameWithoutExtension(skinPath)].GetTexture("g_tColor");
public static Texture EyeDetail(string skinPath) => eyeDetails[System.IO.Path.GetFileNameWithoutExtension(skinPath)].GetTexture("g_tColor");
public static Texture LinerDetail => linerDetailTexture;
public static Texture Marking(string skinPath) => markings[System.IO.Path.GetFileNameWithoutExtension(skinPath)].GetTexture("g_tColor");
public static Texture Surface(string skinPath, string kind, bool head)
{
var id = System.IO.Path.GetFileNameWithoutExtension(skinPath);
var key = (head ? id : id is "human02" or "human04" ? "female" : "male") + "_" + kind;
return surfaces[key];
}
}