Scoring/WrinkleMeter.cs
using System;
using System.Collections.Generic;
using SquishyTerry.Sim;

namespace SquishyTerry.Scoring;

/// <summary>
/// Measures how wrinkled the face is right now, on the CPU.
/// <para>
/// The visible wrinkles are the GPU skin cloth buckling where the surface underneath is squeezed.
/// Reading that back from the GPU would stall the game, so this measures the cause instead: every
/// mesh edge that is shorter than at rest (compressed) adds its compression, weighted by the face's
/// wrinkle mask (no wrinkles on the eyes, nose, mouth and hair patches, same as the skin).
/// </para>
/// Engine-agnostic, so the test harness can calibrate it.
/// </summary>
public sealed class WrinkleMeter
{
	/// <summary>Compression below this is ignored (resting noise, not a wrinkle).</summary>
	public float Threshold = 0.03f;

	/// <summary>Scales the raw sum to a friendly "wrinkle count".</summary>
	public float Scale = 1f;

	readonly int[] _a, _b;
	readonly float[] _rest, _weight;

	public WrinkleMeter( int[] indices, Vec3[] restPositions, float[] wrinkleMask )
	{
		var seen = new HashSet<long>();
		var a = new List<int>();
		var b = new List<int>();
		for ( int t = 0; t + 2 < indices.Length; t += 3 )
		{
			for ( int e = 0; e < 3; e++ )
			{
				int i = indices[t + e], j = indices[t + (e + 1) % 3];
				int lo = Math.Min( i, j ), hi = Math.Max( i, j );
				if ( seen.Add( ((long)lo << 32) | (uint)hi ) )
				{
					a.Add( lo );
					b.Add( hi );
				}
			}
		}

		_a = a.ToArray();
		_b = b.ToArray();
		_rest = new float[_a.Length];
		_weight = new float[_a.Length];
		for ( int k = 0; k < _a.Length; k++ )
		{
			_rest[k] = (restPositions[_a[k]] - restPositions[_b[k]]).Length;
			_weight[k] = wrinkleMask is null ? 1f : MathF.Min( wrinkleMask[_a[k]], wrinkleMask[_b[k]] );
		}
	}

	/// <summary>Current wrinkle amount for these positions.</summary>
	public float Measure( Vec3[] positions )
	{
		float sum = 0f;
		for ( int k = 0; k < _a.Length; k++ )
		{
			float w = _weight[k];
			if ( w <= 0f || _rest[k] <= 0f ) continue;
			float compression = 1f - (positions[_a[k]] - positions[_b[k]]).Length / _rest[k];
			if ( compression > Threshold ) sum += w * (compression - Threshold);
		}
		return sum * Scale;
	}
}