Sim/Shell.cs
using System;
using System.Collections.Generic;

namespace SquishyTerry.Sim;

/// <summary>
/// A shell over part of a mesh: the triangles on one side of a clipping plane, pushed out along the
/// mesh normals. It has no dynamics of its own and is re-evaluated from the mesh every step.
/// The original game used this for the hair (72-byte class, ctor f_luc); Terry also uses it for
/// the eyelids.
/// </summary>
public sealed class Shell
{
	/// <summary>How a shell vertex follows the underlying mesh.</summary>
	public struct Attachment
	{
		/// <summary>True: offset along the normal of mesh vertex A. False: lerp along edge A-B.</summary>
		public bool IsVertex;
		public int A, B;
		/// <summary>Normal offset (vertex) or lerp factor (edge).</summary>
		public float Value;
	}

	public readonly List<Attachment> Attachments = new();
	public Vec3[] Pos;
	public Vec3[] Normals;
	public int[] Indices;
	/// <summary>Mesh vertices on the shell side of the plane; face triangles made only of these are hidden under the hair.</summary>
	public bool[] Inside;
	/// <summary>Inside, plus both ends of cut edges; wrinkles are disabled on these.</summary>
	public bool[] Touched;

	// Attachments flattened for Update (built once at the end of the constructor).
	readonly bool[] _isVertex;
	readonly int[] _a, _b;
	readonly float[] _value, _oneMinusValue;

	/// <summary>The original hair: top/back of the head, up to 0.1 thick away from the hairline.</summary>
	public static Shell CreateHair( IReadOnlyList<Vec3> facePos, IReadOnlyList<int> faceIdx ) =>
		new( facePos, faceIdx, new Vec3( 0, -0.2f, 0 ), new Vec3( 0, 0.675463f, -0.737394f ), dist =>
		{
			float s = Math.Clamp( dist * 6.666667f, 0.0f, 1.0f );
			return 0.1f * s * s * (3.0f - 2.0f * s);
		} );

	/// <param name="facePos">Positions the plane test runs against (the mesh's rest shape).</param>
	/// <param name="faceIdx">Mesh triangles.</param>
	/// <param name="planePoint">A point on the clipping plane.</param>
	/// <param name="planeNormal">Plane normal; the shell covers the side it points to.</param>
	/// <param name="offsetForDistance">Normal offset for a vertex at a given distance from the plane.</param>
	public Shell( IReadOnlyList<Vec3> facePos, IReadOnlyList<int> faceIdx, Vec3 planePoint, Vec3 planeNormal, Func<float, float> offsetForDistance )
	{
		int n = facePos.Count;
		Inside = new bool[n];
		Touched = new bool[n];
		var vertexMap = new Dictionary<int, int>();
		var edgeMap = new Dictionary<long, int>();
		var idx = new List<int>();

		float Dist( int v ) => Vec3.Dot( facePos[v] - planePoint, planeNormal );

		int VertexAttach( int v )
		{
			Inside[v] = true;
			Touched[v] = true;
			if ( vertexMap.TryGetValue( v, out int existing ) ) return existing;
			Attachments.Add( new Attachment { IsVertex = true, A = v, Value = offsetForDistance( Dist( v ) ) } );
			vertexMap[v] = Attachments.Count - 1;
			return Attachments.Count - 1;
		}

		int EdgeAttach( int v1, int v2, float d1, float d2 )
		{
			Touched[v1] = true;
			Touched[v2] = true;
			int lo = Math.Min( v1, v2 ), hi = Math.Max( v1, v2 );
			float dLo = MathF.Abs( v1 > v2 ? d2 : d1 );
			float dHi = MathF.Abs( v1 > v2 ? d1 : d2 );
			long key = ((long)lo << 32) | (uint)hi;
			if ( edgeMap.TryGetValue( key, out int existing ) ) return existing;
			Attachments.Add( new Attachment { IsVertex = false, A = lo, B = hi, Value = dLo / (dLo + dHi) } );
			edgeMap[key] = Attachments.Count - 1;
			return Attachments.Count - 1;
		}

		void OneIn( int a, int b, int c, float da, float db, float dc )
		{
			if ( !(da > 0 && db < 0 && dc < 0) ) return;
			int j = EdgeAttach( a, b, da, db );
			int k = EdgeAttach( a, c, da, dc );
			int h = VertexAttach( a );
			idx.Add( h ); idx.Add( j ); idx.Add( k );
		}

		void TwoIn( int a, int b, int c, float da, float db, float dc )
		{
			if ( !(da > 0 && db > 0 && dc < 0) ) return;
			int m = EdgeAttach( a, c, da, dc );
			int k = EdgeAttach( b, c, db, dc );
			int va = VertexAttach( a );
			int vb = VertexAttach( b );
			idx.Add( va ); idx.Add( vb ); idx.Add( k );
			idx.Add( VertexAttach( a ) ); idx.Add( k ); idx.Add( m );
		}

		for ( int t = 0; t + 2 < faceIdx.Count; t += 3 )
		{
			int k = faceIdx[t], d = faceIdx[t + 1], nn = faceIdx[t + 2];
			float dk = Dist( k ), dd = Dist( d ), dn = Dist( nn );
			if ( dn >= 0 && dk >= 0 && dd >= 0 )
			{
				int w = VertexAttach( k );
				int x = VertexAttach( d );
				int y = VertexAttach( nn );
				idx.Add( w ); idx.Add( x ); idx.Add( y );
				continue;
			}
			OneIn( k, d, nn, dk, dd, dn );
			OneIn( d, nn, k, dd, dn, dk );
			OneIn( nn, k, d, dn, dk, dd );
			TwoIn( k, d, nn, dk, dd, dn );
			TwoIn( d, nn, k, dd, dn, dk );
			TwoIn( nn, k, d, dn, dk, dd );
		}

		Indices = idx.ToArray();
		Pos = new Vec3[Attachments.Count];
		Normals = new Vec3[Attachments.Count];

		_isVertex = new bool[Attachments.Count];
		_a = new int[Attachments.Count];
		_b = new int[Attachments.Count];
		_value = new float[Attachments.Count];
		_oneMinusValue = new float[Attachments.Count];
		for ( int i = 0; i < Attachments.Count; i++ )
		{
			var a = Attachments[i];
			_isVertex[i] = a.IsVertex;
			_a[i] = a.A;
			_b[i] = a.B;
			_value[i] = a.Value;
			_oneMinusValue[i] = 1.0f - a.Value;
		}
	}

	/// <summary>Re-evaluate the shell from the mesh's current positions and normals.</summary>
	public void Update( Vec3[] facePos, Vec3[] faceNormals )
	{
		// Flattened attachments and hand-written Vec3 math (the editor build is unoptimized);
		// same arithmetic order as the Attachment/operator form.
		Vec3[] pos = Pos;
		bool[] isVertex = _isVertex;
		int[] ia = _a, ib = _b;
		float[] value = _value, oneMinus = _oneMinusValue;
		int count = pos.Length;
		for ( int i = 0; i < count; i++ )
		{
			ref Vec3 o = ref pos[i];
			float v = value[i];
			ref Vec3 pa = ref facePos[ia[i]];
			if ( isVertex[i] )
			{
				ref Vec3 na = ref faceNormals[ia[i]];
				o.X = pa.X + na.X * v; o.Y = pa.Y + na.Y * v; o.Z = pa.Z + na.Z * v;
			}
			else
			{
				ref Vec3 pb = ref facePos[ib[i]];
				float u = oneMinus[i];
				o.X = pb.X * v + pa.X * u; o.Y = pb.Y * v + pa.Y * u; o.Z = pb.Z * v + pa.Z * u;
			}
		}
		Geometry.ComputeNormals( pos, Indices, Normals );
	}
}