Static helper that loads and caches human makeup materials, eye details, liner textures and marking surfaces for character customization. It validates shader schema and texture sizes, exposes cached Texture/Material getters and an async EnsureLoadedAsync(id) to preload required assets.
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];
}
}