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

namespace SquishyTerry.Sim;

/// <summary>
/// Shape-matching cluster (Müller et al. 2005). Linear regions snap rigidly to their best-fit
/// rotation; the quadratic mode (used by the mouth) allows some deformation with beta = 0.8.
/// Original: 712-byte struct, init f_bj, solve f_yb.
/// </summary>
public sealed class ShapeRegion
{
	public bool Enabled;
	public Quat Rotation = Quat.Identity;
	public bool Quadratic;
	public int[] Indices = Array.Empty<int>();
	public Vec3[] RestPositions = Array.Empty<Vec3>();
	public Vec3 RestCentroid;

	// Precomputed at Init (all constant): rest offsets q = RestPositions - RestCentroid, their
	// quadratic terms q~ (9 per vertex) and Aqq^-1 (row-major 9x9). The solve loops below spell out
	// the math by hand (the editor build is unoptimized) in the original evaluation order.
	Vec3[] _restOffset = Array.Empty<Vec3>();
	double[] _restQuad = Array.Empty<double>();
	readonly double[] _aqqInv = new double[81];
	readonly float[] _apq = new float[9];
	readonly float[] _rm = new float[9];
	readonly double[] _apqt = new double[27];
	readonly double[] _a = new double[27];
	static readonly float[] s_extractScratch = new float[9];

	/// <param name="quadratic">Allow quadratic deformation (the mouth) instead of a rigid fit.</param>
	/// <param name="indices">Vertices in the region.</param>
	/// <param name="positions">Rest positions (all vertices).</param>
	public void Init( bool quadratic, IReadOnlyList<int> indices, IReadOnlyList<Vec3> positions )
	{
		Enabled = true;
		Quadratic = quadratic;
		Indices = new int[indices.Count];
		RestPositions = new Vec3[indices.Count];

		float sx = 0, sy = 0, sz = 0;
		for ( int i = 0; i < indices.Count; i++ )
		{
			Indices[i] = indices[i];
			var p = positions[indices[i]];
			RestPositions[i] = p;
			sx += p.X; sy += p.Y; sz += p.Z;
		}
		float n = (uint)indices.Count;
		RestCentroid = new Vec3( sx / n, sy / n, sz / n );
		Rotation = Quat.Identity;

		_restOffset = new Vec3[RestPositions.Length];
		for ( int i = 0; i < RestPositions.Length; i++ )
			_restOffset[i] = RestPositions[i] - RestCentroid;

		if ( !quadratic ) return;

		var q = new double[9];
		_restQuad = new double[RestPositions.Length * 9];
		for ( int i = 0; i < RestPositions.Length; i++ )
		{
			Quadratic9( _restOffset[i], q );
			for ( int k = 0; k < 9; k++ ) _restQuad[i * 9 + k] = q[k];
		}

		var aqq = new double[9, 9];
		foreach ( var rp in RestPositions )
		{
			Quadratic9( rp - RestCentroid, q );
			for ( int r = 0; r < 9; r++ )
				for ( int c = 0; c < 9; c++ )
					aqq[r, c] += q[r] * q[c];
		}
		var inv = new double[9, 9];
		Invert9( aqq, inv );
		for ( int r = 0; r < 9; r++ )
			for ( int c = 0; c < 9; c++ )
				_aqqInv[r * 9 + c] = inv[r, c];
	}

	static void Quadratic9( Vec3 v, Span<double> q )
	{
		q[0] = v.X; q[1] = v.Y; q[2] = v.Z;
		q[3] = v.X * v.X; q[4] = v.Y * v.Y; q[5] = v.Z * v.Z;
		q[6] = v.Y * v.X; q[7] = v.Z * v.Y; q[8] = v.Z * v.X;
	}

	/// <summary>Project the region's vertices in <paramref name="p"/> onto the matched shape.</summary>
	public void Solve( Vec3[] p )
	{
		int[] idx = Indices;
		Vec3[] rel = _restOffset;
		int count = idx.Length;
		float cx = 0, cy = 0, cz = 0;
		for ( int i = 0; i < count; i++ )
		{
			ref Vec3 v = ref p[idx[i]];
			cx += v.X; cy += v.Y; cz += v.Z;
		}
		float n = (uint)count;
		cx /= n; cy /= n; cz /= n;

		// Apq = sum (p - c)(q - q0)^T, column-major.
		float a0 = 0, a1 = 0, a2 = 0, a3 = 0, a4 = 0, a5 = 0, a6 = 0, a7 = 0, a8 = 0;
		for ( int i = 0; i < count; i++ )
		{
			ref Vec3 q = ref rel[i];
			ref Vec3 v = ref p[idx[i]];
			float dx = v.X - cx, dy = v.Y - cy, dz = v.Z - cz;
			a0 += q.X * dx; a1 += q.X * dy; a2 += q.X * dz;
			a3 += q.Y * dx; a4 += q.Y * dy; a5 += q.Y * dz;
			a6 += q.Z * dx; a7 += q.Z * dy; a8 += q.Z * dz;
		}
		var apq = _apq;
		apq[0] = a0; apq[1] = a1; apq[2] = a2;
		apq[3] = a3; apq[4] = a4; apq[5] = a5;
		apq[6] = a6; apq[7] = a7; apq[8] = a8;
		ExtractRotation( apq, ref Rotation, 30 );

		if ( !Quadratic )
		{
			// p = c + Rotation.Rotate( q )
			float rx = Rotation.X, ry = Rotation.Y, rz = Rotation.Z, rw = Rotation.W;
			for ( int i = 0; i < count; i++ )
			{
				ref Vec3 q = ref rel[i];
				float tx = 2f * (ry * q.Z - rz * q.Y);
				float ty = 2f * (rz * q.X - rx * q.Z);
				float tz = 2f * (rx * q.Y - ry * q.X);
				ref Vec3 o = ref p[idx[i]];
				o.X = cx + (q.X + rw * tx + (ry * tz - rz * ty));
				o.Y = cy + (q.Y + rw * ty + (rz * tx - rx * tz));
				o.Z = cz + (q.Z + rw * tz + (rx * ty - ry * tx));
			}
			return;
		}

		// A~ = (sum (p - c) q~^T) * Aqq^-1 ; A = 0.8 A~ + 0.2 [R | 0]   (row-major 3x9)
		var apqt = _apqt;
		Array.Clear( apqt );
		var qt = _restQuad;
		for ( int i = 0; i < count; i++ )
		{
			ref Vec3 v = ref p[idx[i]];
			double dx = v.X - cx, dy = v.Y - cy, dz = v.Z - cz;
			int o = i * 9;
			for ( int k = 0; k < 9; k++ )
			{
				double qk = qt[o + k];
				apqt[k] += dx * qk;
				apqt[9 + k] += dy * qk;
				apqt[18 + k] += dz * qk;
			}
		}

		var rm = _rm;
		Rotation.ToMatrix( rm );

		var a = _a;
		var aqqInv = _aqqInv;
		for ( int r = 0; r < 3; r++ )
		{
			int ro = r * 9;
			for ( int k = 0; k < 9; k++ )
			{
				double s = 0;
				for ( int m = 0; m < 9; m++ ) s += apqt[ro + m] * aqqInv[m * 9 + k];
				double rot = k < 3 ? rm[k * 3 + r] : 0.0;
				a[ro + k] = 0.8 * s + 0.2 * rot;
			}
		}

		for ( int i = 0; i < count; i++ )
		{
			int o = i * 9;
			double gx = 0, gy = 0, gz = 0;
			for ( int k = 0; k < 9; k++ )
			{
				double qk = qt[o + k];
				gx += a[k] * qk;
				gy += a[9 + k] * qk;
				gz += a[18 + k] * qk;
			}
			ref Vec3 v = ref p[idx[i]];
			v.X = cx + (float)gx;
			v.Y = cy + (float)gy;
			v.Z = cz + (float)gz;
		}
	}

	/// <summary>
	/// Müller et al. 2016, "A Robust Method to Extract the Rotational Part of Deformations" (f_tsc).
	/// </summary>
	public static void ExtractRotation( ReadOnlySpan<float> a, ref Quat q, int maxIter )
	{
		var s = s_extractScratch;
		for ( int i = 0; i < 9; i++ ) s[i] = a[i];
		ExtractRotation( s, ref q, maxIter );
	}

	/// <summary>
	/// <see cref="ExtractRotation(ReadOnlySpan{float}, ref Quat, int)"/> on a column-major float[9],
	/// with the quaternion/matrix math written out (same arithmetic order as Quat.ToMatrix,
	/// Quat.FromAxisAngle, Quat operator * and Quat.Normalized).
	/// </summary>
	public static void ExtractRotation( float[] a, ref Quat q, int maxIter )
	{
		float a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5], a6 = a[6], a7 = a[7], a8 = a[8];
		float qx = q.X, qy = q.Y, qz = q.Z, qw = q.W;
		for ( int iter = 0; iter < maxIter; iter++ )
		{
			// Quat.ToMatrix (column-major)
			double x = qx, y = qy, z = qz, w = qw;
			float r0 = (float)(1 - 2 * y * y - 2 * z * z);
			float r1 = (float)(2 * x * y + 2 * w * z);
			float r2 = (float)(2 * x * z - 2 * w * y);
			float r3 = (float)(2 * x * y - 2 * w * z);
			float r4 = (float)(1 - 2 * x * x - 2 * z * z);
			float r5 = (float)(2 * y * z + 2 * w * x);
			float r6 = (float)(2 * x * z + 2 * w * y);
			float r7 = (float)(2 * y * z - 2 * w * x);
			float r8 = (float)(1 - 2 * x * x - 2 * y * y);

			// omega = sum_col cross( R col, A col ), dot = sum_col dot( R col, A col )
			float ox = 0, oy = 0, oz = 0, dot = 0;
			ox += r1 * a2 - r2 * a1; oy += r2 * a0 - r0 * a2; oz += r0 * a1 - r1 * a0;
			dot += r0 * a0 + r1 * a1 + r2 * a2;
			ox += r4 * a5 - r5 * a4; oy += r5 * a3 - r3 * a5; oz += r3 * a4 - r4 * a3;
			dot += r3 * a3 + r4 * a4 + r5 * a5;
			ox += r7 * a8 - r8 * a7; oy += r8 * a6 - r6 * a8; oz += r6 * a7 - r7 * a6;
			dot += r6 * a6 + r7 * a7 + r8 * a8;

			float denom = (float)(MathF.Abs( dot ) + 1e-9);
			ox /= denom; oy /= denom; oz /= denom;
			float len = MathF.Sqrt( ox * ox + oy * oy + oz * oz );
			if ( len < 1e-9 ) break;

			// rq = Quat.FromAxisAngle( omega / len, len )
			float half = len * 0.5f;
			float sn = MathF.Sin( half );
			float bx = ox / len * sn, by = oy / len * sn, bz = oz / len * sn, bw = MathF.Cos( half );

			// q = (rq * q).Normalized
			float nx = bw * qx + bx * qw + by * qz - bz * qy;
			float ny = bw * qy - bx * qz + by * qw + bz * qx;
			float nz = bw * qz + bx * qy - by * qx + bz * qw;
			float nw = bw * qw - bx * qx - by * qy - bz * qz;
			float inv = 1.0f / MathF.Sqrt( nx * nx + ny * ny + nz * nz + nw * nw );
			qx = nx * inv; qy = ny * inv; qz = nz * inv; qw = nw * inv;
		}
		q = new Quat( qx, qy, qz, qw );
	}

	/// <summary>9x9 inverse with partial pivoting (the original used an Eigen PartialPivLU).</summary>
	static void Invert9( double[,] m, double[,] inv )
	{
		const int n = 9;
		var a = new double[n, n];
		for ( int r = 0; r < n; r++ )
			for ( int c = 0; c < n; c++ )
				a[r, c] = m[r, c];
		for ( int r = 0; r < n; r++ )
			for ( int c = 0; c < n; c++ )
				inv[r, c] = r == c ? 1 : 0;

		for ( int col = 0; col < n; col++ )
		{
			int pivot = col;
			double best = Math.Abs( a[col, col] );
			for ( int r = col + 1; r < n; r++ )
			{
				double v = Math.Abs( a[r, col] );
				if ( v > best ) { best = v; pivot = r; }
			}
			if ( pivot != col )
			{
				for ( int c = 0; c < n; c++ )
				{
					(a[col, c], a[pivot, c]) = (a[pivot, c], a[col, c]);
					(inv[col, c], inv[pivot, c]) = (inv[pivot, c], inv[col, c]);
				}
			}
			double d = a[col, col];
			for ( int c = 0; c < n; c++ ) { a[col, c] /= d; inv[col, c] /= d; }
			for ( int r = 0; r < n; r++ )
			{
				if ( r == col ) continue;
				double f = a[r, col];
				if ( f == 0 ) continue;
				for ( int c = 0; c < n; c++ )
				{
					a[r, c] -= f * a[col, c];
					inv[r, c] -= f * inv[col, c];
				}
			}
		}
	}
}