Component that applies a custom iris albedo for native human eye materials. It creates per-material copies, composes a new iris texture with a compute shader using a desired color, sets material overrides on a SkinnedModelRenderer, and refreshes textures over a short time window.
using System;
namespace CharacterEditor;
/// <summary>Changes only the iris albedo of the curated native human eyes.</summary>
public sealed class EyeColorPresentation : IDisposable
{
static ComputeShader compositor;
static Material carrier;
readonly Dictionary<int, Material> copies = new();
readonly Dictionary<int, Texture> textures = new();
SkinnedModelRenderer target;
Model model;
string group;
Color? applied;
CharacterAppearanceData requested;
DateTime started, nextRefresh;
public static async System.Threading.Tasks.Task EnsureLoadedAsync()
{
carrier ??= await Material.LoadAsync("materials/makeup/iris_compositor.vmat");
if (carrier?.ShaderName != "shaders/human_iris.shader" || !carrier.Shader.IsValid || !carrier.Shader.Schema.Variables.Any(x => x.Name == "IrisColor"))
{
carrier = null;
throw new InvalidOperationException("The human iris shader is unavailable.");
}
}
public void Apply(SkinnedModelRenderer body, CharacterAppearanceData data, bool refresh = false)
{
if (!body.IsValid() || body.Model is null)
return;
if (target != body || model != body.Model || group != body.MaterialGroup)
{
Dispose();
target = body;
model = body.Model;
group = body.MaterialGroup;
applied = null;
}
if (data?.CustomEyeColor != true)
{
foreach (var i in copies.Keys)
body.Materials.SetOverride(i, null);
Dispose();
applied = null;
return;
}
var color = data.EyeColor;
requested = data.DeepCopy();
if (applied != color)
{
started = DateTime.UtcNow;
nextRefresh = started.AddMilliseconds(250);
}
for (var i = 0; i < body.Materials.Count; i++)
{
var original = body.Materials.GetOriginal(i);
if (original?.ShaderName != "shaders/eyeball.shader")
continue;
if (!copies.TryGetValue(i, out var copy))
copies[i] = copy = original.CreateCopy($"fate-iris-{body.Id}-{i}.vmat");
if (refresh || applied != color || !textures.ContainsKey(i))
{
var source = original.GetTexture("g_tColor");
source.MarkUsed(Math.Max(source.Width, source.Height));
if (!textures.TryGetValue(i, out var texture))
{
texture = Texture.Create(source.Width, source.Height).WithFormat(ImageFormat.RGBA8888).WithUAVBinding().WithName("Local human iris").Finish();
textures[i] = texture;
}
var attributes = new RenderAttributes();
attributes.Set("BaseColor", source);
attributes.Set("IrisColor", new Vector4(color.r, color.g, color.b, 1));
attributes.Set("Result", texture);
compositor ??= new("shaders/human_iris.shader");
compositor.DispatchWithAttributes(attributes, source.Width, source.Height, 1);
copy.Set("g_tColor", texture);
}
body.Materials.SetOverride(i, copy);
}
applied = color;
}
public void Refresh()
{
if (!target.IsValid() || requested?.CustomEyeColor != true)
return;
for (var i = 0; i < target.Materials.Count; i++)
{
var material = target.Materials.GetOriginal(i);
if (material?.ShaderName == "shaders/eyeball.shader" && material.GetTexture("g_tColor")is Texture source)
source.MarkUsed(Math.Max(source.Width, source.Height));
}
var now = DateTime.UtcNow;
if ((now - started).TotalSeconds < 3 && now >= nextRefresh)
{
Apply(target, requested, true);
nextRefresh = now.AddMilliseconds(250);
}
}
public void Dispose()
{
foreach (var texture in textures.Values)
texture.Dispose();
textures.Clear();
copies.Clear();
applied = null;
requested = null;
}
}