CharacterKit/EyeColorPresentation.cs

Component that applies a custom iris albedo tint to native human eye materials. It creates material copies, generates tinted iris textures with a compute shader, sets material overrides on a SkinnedModelRenderer, and refreshes textures for a short time after changes.

Native InteropFile Access
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;
	}
}