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;
}
}