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