Experiments/FaceTransferPreview.cs

A UI ScenePanel that loads a face mesh experiment from JSON, builds a Model with morph targets (Existing, Experimental, Target), and renders it with a SkinnedModelRenderer and camera for previewing face transfer morphs.

File Access
using System;
using System.Text.Json;
using Sandbox.UI;

namespace CharacterEditor;
/// <summary>Isolated rest-surface experiment. These morphs are not animated avatar presets.</summary>
public sealed class FaceTransferPreview : ScenePanel
{
	public sealed class Geometry
	{
		public float[][] Source { get; set; }
		public float[][] Target { get; set; }
		public float[][] Existing { get; set; }
		public float[][] Experimental { get; set; }
		public int[][] Triangles { get; set; }
	}

	public string Pair { get; set; } = "human03-to-human07";
	public string Mode { get; set; } = "Target";
	public float Amount { get; set; } = 1;
	public float Yaw { get; set; }
	public string Error { get; private set; }
	public SkinnedModelRenderer Body { get; private set; }
	public CameraComponent ViewCamera { get; private set; }

	string loaded;
	public FaceTransferPreview()
	{
		RenderScene = new Scene();
		using var scope = RenderScene.Push();
		ViewCamera = new GameObject().Components.Create<CameraComponent>();
		ViewCamera.FieldOfView = 30;
		ViewCamera.ZNear = .1f;
		ViewCamera.BackgroundColor = new(.065f, .085f, .11f);
		CharacterStudioLighting.Create();
	}

	static Vector3 V(float[] p) => new(p[0], p[1], p[2]);
	static Vector3[] Normals(Vector3[] p, int[][] triangles)
	{
		var normals = new Vector3[p.Length];
		foreach (var t in triangles)
		{
			var n = Vector3.Cross(p[t[1]] - p[t[0]], p[t[2]] - p[t[0]]);
			foreach (var i in t)
				normals[i] += n;
		}

		return normals.Select(n => n.Normal).ToArray();
	}

	public static Model Build(Geometry data)
	{
		var p = data.Source.Select(V).ToArray();
		var normals = Normals(p, data.Triangles);
		var mesh = new Mesh(Material.Load("materials/default.vmat"));
		var vertices = p.Select((v, i) => new Vertex { Position = v, Normal = normals[i], Tangent = new Vector4(0, 1, 0, 1), TexCoord0 = Vector2.Zero, Color = Color.White }).ToArray();
		mesh.CreateVertexBuffer<Vertex>(vertices.Length, vertices.AsSpan());
		var indices = data.Triangles.SelectMany(t => t).ToArray();
		mesh.CreateIndexBuffer(indices.Length, indices.AsSpan());
		foreach (var mode in new[]
		{
			"Existing",
			"Experimental",
			"Target"
		}

		)
		{
			var target = (mode == "Existing" ? data.Existing : mode == "Experimental" ? data.Experimental : data.Target).Select(V).ToArray();
			var n = Normals(target, data.Triangles);
			mesh.AddMorph(mode, p.Select((v, i) => new MorphDelta(i, target[i] - v, n[i] - normals[i])).ToArray());
		}

		mesh.Bounds = new BBox(new Vector3(-8, -8, 54), new Vector3(9, 9, 72));
		return Model.Builder.AddMesh(mesh).Create();
	}

	public override void Tick()
	{
		base.Tick();
		using var scope = RenderScene.Push();
		if (loaded != Pair)
		{
			loaded = Pair;
			Body?.GameObject.Destroy();
			Error = null;
			try
			{
				if (Pair != "human03-to-human07" && Pair != "human02-to-human04")
					throw new ArgumentException("Unknown experiment pair.");
				var data = JsonSerializer.Deserialize<Geometry>(FileSystem.Mounted.ReadAllText($"experiments/face_transfer/{Pair}.json"));
				Body = new GameObject().Components.Create<SkinnedModelRenderer>();
				Body.Model = Build(data);
				Body.UseAnimGraph = false;
				Body.Tint = new(.65f, .69f, .74f);
			}
			catch (Exception e)
			{
				Error = e.Message;
				Log.Warning($"Face lab: {Error}");
			}
		}

		if (Body.IsValid())
		{
			Body.LocalRotation = Rotation.FromYaw(Yaw);
			foreach (var mode in new[]
			{
				"Existing",
				"Experimental",
				"Target"
			}

			)
				Body.Morphs.Set(mode, Mode == mode ? Math.Clamp(Amount, 0, 1) : 0);
		}

		float distance = MathF.Max(25, 31 * Box.Rect.Width / MathF.Max(1, Box.Rect.Height));
		ViewCamera.WorldPosition = new(distance, 0, 63.2f);
		ViewCamera.WorldRotation = Rotation.LookAt(new Vector3(0, 0, 63.2f) - ViewCamera.WorldPosition);
	}

	public override void Delete(bool immediate = false)
	{
		var scene = RenderScene;
		base.Delete(immediate);
		scene?.Destroy();
	}
}