Sim/FaceStyle.cs
using System;
namespace SquishyTerry.Sim;
/// <summary>
/// Everything that decides what the face looks like. <see cref="Original"/> reproduces the
/// recovered game exactly (it is what the wasm comparison test runs); <see cref="Terry"/> reshapes
/// it into the s&box citizen. All feature positions are directions on the unit sphere, picked
/// before <see cref="HeadShape"/> is applied.
/// </summary>
public sealed class FaceStyle
{
/// <summary>Deforms the initial unit sphere. Null keeps the sphere.</summary>
public Func<Vec3, Vec3> HeadShape;
public Vec3 LeftEye = new( -0.234667f, 0.159318f, 0.958931f );
public Vec3 RightEye = new( 0.234666f, 0.159318f, 0.958931f );
public Vec3 Nose = new( -0.0f, -0.099833f, 0.995004f );
public Vec3 LeftEar = new( -0.867728f, -0.149438f, 0.474042f );
public Vec3 RightEar = new( 0.867728f, -0.149438f, 0.474042f );
public Vec3 Mouth = new( -0.0f, -0.342898f, 0.939373f );
public bool Hair = true;
/// <summary>
/// Flatten feature patches onto a plane fitted to their whole boundary loop. The original uses
/// three neighbouring boundary points, which tilts mirrored features differently.
/// </summary>
public bool FitPatchPlanes;
/// <summary>How far feature patches are pushed onto their plane (1 = fully flat, as in the original).</summary>
public float PatchFlatten = 1.0f;
/// <summary>How many vertex rings around the mouth vertex make up the mouth.</summary>
public int MouthRings = 2;
/// <summary>Mouth rings are squashed onto an ellipse of this size.</summary>
public Vec3 MouthScale = new( 0.16f, 0.08f, 0.16f );
/// <summary>Skin shading: mouthiness above this is drawn as the inside of the mouth (0.15 in the original).</summary>
public float MouthShadeThreshold = 0.15f;
/// <summary>Pulls the mouth corners down (y -= curve * dx^2).</summary>
public float MouthCurve;
public int NoseRings = 1;
public Vec3 NosePatch = new( 0.065f, 0.065f, 0.065f );
public Vec3 NoseSize = new( 0.05f, 0.05f, 0.075f );
public float NoseCut = 0.001f;
/// <summary>Optional sculpt of the nose's unit sphere (+Z out of the face, +Y up) before it is clipped.</summary>
public Func<Vec3, Vec3> NoseShape;
/// <summary>Nose mesh subdivisions (3 in the original).</summary>
public int NoseDetail = 3;
/// <summary>Optional nose shading mask on the sculpted unit sphere: 1 draws in the mouth colour (nostrils).</summary>
public Func<Vec3, float> NoseMask;
public int EyeRings = 3;
public Vec3 EyePatch = new( 0.17f, 0.17f, 0.17f );
public Vec3 EyeSize = new( 0.17f, 0.17f, 0.17f );
public float EyeCut = 0.2f;
public int EarRings = 2;
public Vec3 EarPatch = new( 0.12f, 0.12f, 0.07f );
public Vec3 EarSize = new( 0.05f, 0.12f, 0.08f );
public float EarCut = 0.001f;
public float EarShear = 2.0f;
public float EarRotation = 0.3f;
/// <summary>Optional sculpt of the right ear's unit sphere (x = back, y = up, z = out from the head); mirrored for the left.</summary>
public Func<Vec3, Vec3> EarShape;
/// <summary>Icosphere subdivisions for the ears (3 in the original).</summary>
public int EarDetail = 3;
/// <summary>Skin shells over the top of each eyeball.</summary>
public bool Eyelids;
/// <summary>Lid edge height in eye space (+Y up, +Z out of the face).</summary>
public float LidHeight;
/// <summary>Tilts the lid edge; positive droops it towards the front of the eye.</summary>
public float LidTilt;
public float LidThickness = 0.02f;
public static FaceStyle Original => new();
/// <summary>0..1 nostril depth on the nose's unit sphere: two soft holes on the lower front.</summary>
static float NostrilDepth( float x, float y, float z )
{
if ( z < 0.1f ) return 0f;
float depth = 0f;
for ( int side = -1; side <= 1; side += 2 )
{
// Equal radii (x slightly less to cancel the bulb's flare) so the holes read as circles from the front.
float dx = (x - side * 0.3f) / 0.19f, dy = (y + 0.36f) / 0.205f;
float d = MathF.Sqrt( dx * dx + dy * dy );
if ( d < 1f ) depth = MathF.Max( depth, (1f - d) * (1f - d) * (3f - 2f * (1f - d)) );
}
return depth;
}
public static FaceStyle Terry => new()
{
HeadShape = p =>
{
// Bullet head: round dome on top, long jaw below, and a rounded-square cross-section so
// the face is flat enough for the eyes to look forward.
float y = p.Y >= 0 ? p.Y * 0.95f : p.Y * 1.9f;
float widen = p.Y >= 0 ? 1.0f : 1.0f + 0.35f * p.Y * p.Y;
float r = MathF.Sqrt( p.X * p.X + p.Z * p.Z );
float square = 1.0f;
if ( r > 1e-6f )
{
float c = MathF.Abs( p.X / r ), s = MathF.Abs( p.Z / r );
square = MathF.Pow( c * c * c + s * s * s, -1.0f / 3.0f ) * 0.92f;
}
float k = widen * square;
return new Vec3( p.X * k, y + 0.45f, p.Z * 0.95f * k );
},
LeftEye = new Vec3( -0.52f, -0.2f, 0.843f ).Normalized,
RightEye = new Vec3( 0.52f, -0.2f, 0.843f ).Normalized,
Nose = new Vec3( 0.0f, -0.34f, 0.94f ).Normalized,
Mouth = new Vec3( 0.0f, -0.6f, 0.8f ).Normalized,
LeftEar = new Vec3( -0.9f, -0.25f, 0.357f ).Normalized,
RightEar = new Vec3( 0.9f, -0.25f, 0.357f ).Normalized,
Hair = false,
FitPatchPlanes = true,
PatchFlatten = 1.0f,
MouthRings = 3,
MouthScale = new Vec3( 0.25f, 0.03f, 0.1f ),
MouthCurve = 0.6f,
MouthShadeThreshold = 0.35f,
NoseRings = 2,
NosePatch = new Vec3( 0.11f, 0.11f, 0.11f ),
// Citizen nose: a small, low, rounded bulb, slightly wider at the bottom, with a snub tip so the
// two nostrils show from the front.
NoseSize = new Vec3( 0.13f, 0.112f, 0.08f ),
NoseCut = 0.0f,
NoseDetail = 4,
NoseShape = p =>
{
float x = p.X, y = p.Y, z = p.Z;
// Flatten the underside into a face pointing forward-down, where the nostrils sit; the upper
// part stays a full rounded bulb.
const float ny = -0.857f, nz = 0.514f, plane = 0.35f;
float along = y * ny + z * nz;
if ( along > plane )
{
float pull = (along - plane) * 0.75f;
y -= pull * ny;
z -= pull * nz;
}
// Nostrils: push two holes into that flat underside.
float hole = NostrilDepth( x, y, z );
float inward = 1f - 0.45f * hole;
x *= inward; y *= inward; z *= inward;
// A rounded bulb, only slightly wider at the bottom than the top.
float flare = y < 0 ? 1f + 0.15f * -y : 1f - 0.1f * y;
x *= flare;
return new Vec3( x, y, z );
},
NoseMask = p =>
{
// The sculpted holes are shallow at this size, so darken them like the original does the mouth.
float x = p.X / (p.Y < 0 ? 1f + 0.15f * -p.Y : 1f - 0.1f * p.Y);
// A smooth value (not 0/1) so the shader's threshold cuts a round contour across the triangles.
return Math.Clamp( (NostrilDepth( x, p.Y, p.Z ) - 0.05f) / 0.2f, 0f, 1f );
},
EyeRings = 3,
EyePatch = new Vec3( 0.185f, 0.185f, 0.185f ),
EyeSize = new Vec3( 0.185f, 0.185f, 0.185f ),
EyeCut = 0.1f,
EarSize = new Vec3( 0.075f, 0.19f, 0.12f ),
EarPatch = new Vec3( 0.14f, 0.14f, 0.08f ),
EarShear = 1.3f,
EarDetail = 4,
EarShape = p =>
{
// Local frame: x = back, y = up, z = out from the head. The shear leans the ear back, so
// its front (x < 0) is the side you see.
float x = p.X, y = p.Y, z = p.Z;
// A "C" outline: full at the top, narrowing down into the lobe.
x *= 1f + 0.2f * y;
// Hollow bowl in the front face, leaving a rolled rim around the top, back and lobe.
if ( x < 0f )
{
float dy = y / 0.72f, dz = (z - 0.5f) / 0.42f;
float r = MathF.Sqrt( dy * dy + dz * dz );
if ( r < 1f )
{
float s = 1f - r;
x *= 1f - 1.5f * s * s * (3f - 2f * s);
}
}
return new Vec3( x, y, z );
},
Eyelids = true,
LidHeight = 0.01f,
LidTilt = 0.15f,
LidThickness = 0.02f,
};
}