StylizedPlayerHeads.cs

Procedural generator for stylized player head meshes in a soccer game. Defines head trait records and named identities, then builds face, eyes, features and hair geometry as vertex lists with a palette texture material, caching the result per identity.

using Sandbox;
using System;
using System.Collections.Generic;

namespace Socer;

// Signed deviations from a common adult head. These describe identity, never
// player rating, club, position or nationality. One art direction resolves them.
public sealed record HeadTraits
{
    public float Forehead {get;init;}
    public float Cheeks {get;init;}
    public float Jaw {get;init;}
    public float Chin {get;init;}
    public float Length {get;init;}
    public float EyeGap {get;init;}
    public float EyeOpening {get;init;}
    public float NoseWidth {get;init;}
    public float NoseLength {get;init;}
    public float MouthWidth {get;init;}
    public float LipVolume {get;init;}
    public float Brow {get;init;}
    public float SkullDepth {get;init;}
    public float EyeTilt {get;init;}
    public float EyeLevel {get;init;}
    public float NoseTip {get;init;}
    public float BrowThickness {get;init;}
    public float BrowTilt {get;init;}
    public float Ears {get;init;}
}
public enum HeadHaircut { SweptBack, SideSweep }
public sealed record HeadIdentity(string Id,HeadTraits Traits,string Skin,string Hair,string Eyes,bool TiedHair,
    HeadHaircut Haircut=HeadHaircut.SweptBack,float Beard=0,string BeardColor="#4b382e");
public sealed record HeadMeshPart(string Name,Model Model,Color Tint);
public sealed record HeadAsset(Vector3 Scale,IReadOnlyList<HeadMeshPart> Parts,int Triangles,Color SkinTone);

public static class StylizedPlayerHeads
{
    public const int Revision=17;
    public const float Exaggeration=1.25f;
    [SkipHotload] static readonly Dictionary<HeadIdentity,HeadAsset> Cache=new();
    static int cachedRevision;
    [SkipHotload] static Material whiteMaterial;
    public static HeadIdentity Haaland {get;}=new(PlayerAppearance.HaalandId,
        new HeadTraits{Forehead=.7f,Cheeks=.3f,Jaw=.8f,Chin=.85f,Length=.55f,
            EyeGap=-.08f,EyeOpening=-.7f,NoseWidth=.5f,NoseLength=.12f,
            MouthWidth=.4f,LipVolume=.35f,Brow=.25f,SkullDepth=.15f},
        "#edc0a6","#c7ad78","#72a7bc",true);
    public static HeadIdentity Messi {get;}=new(PlayerAppearance.MessiId,
        new HeadTraits{Forehead=-.18f,Cheeks=.2f,Jaw=-.35f,Chin=-.4f,Length=-.35f,
            EyeGap=-.12f,EyeOpening=-.4f,EyeTilt=.24f,EyeLevel=.85f,NoseWidth=-.18f,NoseLength=.8f,NoseTip=.7f,
            MouthWidth=-.1f,LipVolume=-.25f,Brow=-.12f,BrowThickness=.85f,BrowTilt=-.15f,Ears=.45f},
        "#e2b295","#302822","#79604a",false,HeadHaircut.SideSweep,.85f,"#4c382d");
    public static HeadIdentity Neutral {get;}=new("STYLE-NEUTRAL",new HeadTraits(),"#dca98a","#644533","#9c7653",false);
    public static HeadIdentity Round {get;}=new("STYLE-ROUND",new HeadTraits{Length=-.6f,Cheeks=.6f,Jaw=-.7f,Chin=-.45f,EyeOpening=.5f,NoseWidth=.7f,MouthWidth=.35f},"#b77750","#302925","#654335",false);
    public static HeadIdentity Narrow {get;}=new("STYLE-NARROW",new HeadTraits{Length=.65f,Cheeks=-.5f,Jaw=-.5f,Chin=-.3f,NoseLength=.6f,EyeGap=-.25f,EyeOpening=-.15f},"#d6a27c","#302e31","#586b56",false);
    public static HeadIdentity Find(string id)=>id switch
    {
        PlayerAppearance.HaalandId=>Haaland,PlayerAppearance.MessiId=>Messi,"STYLE-NEUTRAL"=>Neutral,"STYLE-ROUND"=>Round,"STYLE-NARROW"=>Narrow,_=>null
    };
    static float Deviation(float value)=>1+.18f*Exaggeration*Math.Clamp(value,-1,1);
    static Color Ink(string hex)
    {
        var c=Color.Parse(hex)??Color.White;
        float Linear(float x)=>x<=.04045f?x/12.92f:MathF.Pow((x+.055f)/1.055f,2.4f);
        return new Color(Linear(c.r),Linear(c.g),Linear(c.b));
    }
    public static HeadAsset Get(HeadIdentity identity)
    {
        if(cachedRevision!=Revision){Cache.Clear();cachedRevision=Revision;}
        if(Cache.TryGetValue(identity,out var asset))return asset;
        if(PlayerAppearance.HasModel(identity.Id))
        {
            var model=FootballFigureAssets.Head(identity.Id);
            return Cache[identity]=new HeadAsset(new Vector3(.17f),new[]{new HeadMeshPart("Skin",model,Color.White),new HeadMeshPart("Sculpt",model,Color.White)},
                identity.Id==PlayerAppearance.MessiId?12838:16120,Ink(identity.Skin));
        }
        return Cache[identity]=new Sculpt(identity).Build();
    }

    // One surface and feature layout for every identity. Primary masses are
    // resolved first, secondary anatomical planes next, then small colour marks.
    sealed class Sculpt
    {
        readonly HeadIdentity id;
        readonly HeadTraits t;
        readonly List<(string Name,List<Vertex> Vertices,Color Tint,bool Ink)> paint=new();
        int triangles;
        readonly float eyeGap,eyeHalfHeight,noseWidth,mouthWidth,EyeV,NoseV;
        const float EyeHalfWidth=.102f;
        public Sculpt(HeadIdentity identity)
        {
            id=identity;t=id.Traits;
            EyeV=.415f+.06f*t.EyeLevel;NoseV=.595f+.035f*t.EyeLevel;
            eyeGap=.192f*Deviation(t.EyeGap);eyeHalfHeight=.038f*Deviation(t.EyeOpening)*(.92f+t.EyeOpening*.12f);
            noseWidth=.059f*Deviation(t.NoseWidth);mouthWidth=.142f*Deviation(t.MouthWidth);
        }
        Vector3 Landmark(int i)=>i switch
        {
            0=>new(.005f,.005f,.005f),1=>new(.34f,.25f,.31f),
            2=>new(.44f*Deviation(t.Forehead),.30f,.39f),3=>new(.465f*Deviation(t.Forehead),.298f,.40f),
            4=>new(.47f*Deviation(t.Forehead*.6f+t.Cheeks*.4f),.292f,.40f),5=>new(.475f*Deviation(t.Cheeks),.292f,.37f),
            6=>new(.45f*Deviation(t.Cheeks),.265f,.32f),7=>new(.425f*Deviation(t.Jaw),.275f,.28f),
            8=>new(.40f*Deviation(t.Jaw),.285f,.235f),9=>new(.345f*Deviation(t.Chin),.295f,.19f),
            _=>new(.24f*Deviation(t.Chin),.27f,.155f)
        };
        Vector3 Profile(float v)
        {
            float position=Math.Clamp(v,0,1)*10;int i=Math.Min(9,(int)position);float f=position-i;
            var a=Landmark(Math.Max(0,i-1));var b=Landmark(i);var c=Landmark(i+1);var d=Landmark(Math.Min(10,i+2));
            var p=.5f*(2*b+(-a+c)*f+(2*a-5*b+4*c-d)*f*f+(-a+3*b-3*c+d)*f*f*f);
            if(v<.2f)
            {
                float crown=MathF.Sqrt(Math.Max(.00001f,1-MathF.Pow(1-v/.2f,2)));
                p=Landmark(2)*crown;
            }
            p.z*=Deviation(t.SkullDepth);return p;
        }
        static float Bell(float y,float centre,float width,float v,float level,float height)
            =>MathF.Exp(-MathF.Pow((y-centre)/width,2)-MathF.Pow((v-level)/height,2));
        Vector3 Point(float yaw,float v)
        {
            var p=Profile(v);float co=MathF.Cos(yaw),si=MathF.Sin(yaw);
            float jaw=Math.Clamp((v-.64f)/.18f,0,1);
            float lateral=MathF.CopySign(MathF.Pow(Math.Abs(si),1-jaw*.26f),si);
            float y=p.x*lateral,z=.5f-v+.09f*MathF.Pow(v,6)*(1-co)*.5f;
            float x=co>=0?p.y*MathF.Pow(co,.60f-jaw*.10f):co*p.z;
            if(co>0)
            {
                // Cheek planes, hooded sockets and a brow shelf are geometry.
                x+=.022f*Deviation(t.Cheeks)*Bell(Math.Abs(y),.29f,.14f,v,.54f,.10f);
                x-=.021f*Bell(Math.Abs(y),eyeGap,.086f,v,EyeV,.040f);
                x+=.023f*Deviation(t.Brow)*Bell(Math.Abs(y),eyeGap,.105f,v,EyeV-.065f,.045f);
                // Bridge, blunt nose tip and two wings form one continuous nose.
                x+=.070f*Deviation(t.NoseLength)*Bell(y,0,.050f,v,.50f+.04f*t.EyeLevel,.11f);
                float tipPower=4-2*Math.Clamp(t.NoseTip,0,1);
                x+=.108f*Deviation(t.NoseLength)*MathF.Exp(-MathF.Pow(Math.Abs(y/noseWidth),tipPower)-MathF.Pow(Math.Abs((v-NoseV)/.044f),tipPower));
                x+=.029f*Bell(Math.Abs(y),noseWidth,.030f,v,NoseV+.012f,.030f);
                // Restrained lip rolls and chin pad, without photographic folds.
                x+=.017f*Deviation(t.LipVolume)*Bell(y,0,mouthWidth,v,.729f,.020f);
                x+=.023f*Deviation(t.LipVolume)*Bell(y,0,mouthWidth*.85f,v,.762f,.023f);
                x+=.027f*Deviation(t.Chin)*Bell(y,0,.18f,v,.885f,.067f);
                x+=id.Beard*.018f*Bell(y,0,.34f,v,.87f,.16f);
            }
            return new Vector3(x,y,z);
        }
        Vector3 Normal(float yaw,float v)
        {
            const float e=.001f;
            return Vector3.Cross(Point(yaw,Math.Min(1,v+e))-Point(yaw,Math.Max(0,v-e)),Point(yaw+e,v)-Point(yaw-e,v)).Normal;
        }
        Vector3 FacePoint(float y,float v,float lift=0)
        {
            float width=Profile(v).x,jaw=Math.Clamp((v-.64f)/.18f,0,1);
            float sine=MathF.CopySign(MathF.Pow(Math.Clamp(Math.Abs(y)/width,0,.995f),1/(1-jaw*.26f)),y);
            var p=Point(MathF.Asin(sine),v);p.x+=lift;return p;
        }
        static void Quad(List<Vertex> vertices,Vertex a,Vertex b,Vertex c,Vertex d)
        {
            if(Vector3.Dot(Vector3.Cross(b.Position-a.Position,c.Position-a.Position),a.Normal)>=0)
                vertices.AddRange(new[]{a,b,c,a,c,d});
            else vertices.AddRange(new[]{a,c,b,a,d,c});
        }
        void Add(string name,List<Vertex> vertices,string color,bool ink=false)
        {
            if(vertices.Count==0)return;
            paint.Add((name,vertices,Ink(color),ink));
            triangles+=vertices.Count/3;
        }
        void Ellipsoid(List<Vertex> vertices,Vector3 centre,Vector3 radius,int rings=14,int sides=24)
        {
            Vertex At(int row,int side)
            {
                float a=MathF.PI*row/rings,b=MathF.Tau*side/sides;
                var unit=new Vector3(MathF.Cos(a),MathF.Sin(a)*MathF.Cos(b),MathF.Sin(a)*MathF.Sin(b));
                return new Vertex(centre+unit*radius,(unit/radius).Normal,Vector3.Up,Vector2.Zero);
            }
            for(int r=0;r<rings;r++)for(int s=0;s<sides;s++)Quad(vertices,At(r,s),At(r+1,s),At(r+1,s+1),At(r,s+1));
        }
        void Skin()
        {
            var vertices=new List<Vertex>();var beard=new List<Vertex>();const int rows=48,sides=80;
            Vector2 Uv(int r,int s)=>new(s*MathF.Tau/sides,r/(float)rows);
            Vertex Surface(Vector2 uv)=>new(Point(uv.x,uv.y),Normal(uv.x,uv.y),Vector3.Up,Vector2.Zero);
            Vertex At(int r,int s)=>Surface(Uv(r,s));
            void Triangle(List<Vertex> target,Vector2 a,Vector2 b,Vector2 c)
            {
                var va=Surface(a);var vb=Surface(b);var vc=Surface(c);
                if(Vector3.Dot(Vector3.Cross(vb.Position-va.Position,vc.Position-va.Position),va.Normal)>=0)
                    target.AddRange(new[]{va,vb,vc});
                else target.AddRange(new[]{va,vc,vb});
            }
            void PaintTriangle(Vector2 a,Vector2 b,Vector2 c)
            {
                var input=new[]{a,b,c};var distance=new[]{BeardDistance(a.x,a.y),BeardDistance(b.x,b.y),BeardDistance(c.x,c.y)};
                if(distance[0]>=0 && distance[1]>=0 && distance[2]>=0){Triangle(beard,a,b,c);return;}
                if(distance[0]<0 && distance[1]<0 && distance[2]<0){Triangle(vertices,a,b,c);return;}
                // Split the existing surface exactly on its colour boundary.
                // Both colours share the same vertices and depth, without steps.
                for(int color=0;color<2;color++)
                {
                    bool Inside(float d)=>color==0?d>=0:d<0;
                    var polygon=new List<Vector2>();
                    for(int i=0;i<3;i++)
                    {
                        int next=(i+1)%3;var from=input[i];var to=input[next];
                        bool start=Inside(distance[i]),end=Inside(distance[next]);
                        if(start)polygon.Add(from);
                        if(start==end)continue;
                        float lo=0,hi=1;
                        for(int step=0;step<16;step++)
                        {
                            float mid=(lo+hi)*.5f;var p=from+(to-from)*mid;
                            if(Inside(BeardDistance(p.x,p.y))==start)lo=mid;else hi=mid;
                        }
                        polygon.Add(from+(to-from)*((lo+hi)*.5f));
                    }
                    for(int i=1;i+1<polygon.Count;i++)Triangle(color==0?beard:vertices,polygon[0],polygon[i],polygon[i+1]);
                }
            }
            for(int r=0;r<rows;r++)for(int s=0;s<sides;s++)
            {
                if(id.Beard<=0)Quad(vertices,At(r,s),At(r+1,s),At(r+1,s+1),At(r,s+1));
                else
                {
                    PaintTriangle(Uv(r,s),Uv(r+1,s),Uv(r+1,s+1));
                    PaintTriangle(Uv(r,s),Uv(r+1,s+1),Uv(r,s+1));
                }
            }
            for(int s=0;s<sides;s++)
            {
                var bottom=new Vertex(new Vector3(0,0,-.455f),Vector3.Down,Vector3.Forward,Vector2.Zero);
                (id.Beard>0?beard:vertices).AddRange(new[]{bottom,At(rows,s+1),At(rows,s)});
            }
            // Ears belong to the same skin sculpt and shading as the head.
            for(int side=-1;side<=1;side+=2)
                Ellipsoid(vertices,new Vector3(-.018f,side*(Landmark(5).x+.024f),-.025f),new Vector3(.050f,.033f,.093f)*Deviation(t.Ears));
            Add("Skin",vertices,id.Skin);
            Add("Beard",beard,id.BeardColor);
        }
        Vector3 EyeSurface(int side,float y,float z,float lift)
        {
            float normalized=y/EyeHalfWidth;
            float aperture=eyeHalfHeight*MathF.Pow(Math.Max(0,1-normalized*normalized),.65f);
            z=Math.Clamp(z,-aperture,aperture);
            float v=EyeV-z-side*y*t.EyeTilt*.18f;
            float bulge=.022f*Math.Max(0,1-normalized*normalized)*Math.Max(0,1-MathF.Pow(z/eyeHalfHeight,2));
            return FacePoint(side*eyeGap+y,v,.012f+bulge+lift);
        }
        void Eyes()
        {
            var white=new List<Vertex>();var iris=new List<Vertex>();var pupil=new List<Vertex>();var glint=new List<Vertex>();
            for(int side=-1;side<=1;side+=2)
            {
                Vertex At(int r,int c)
                {
                    float y=(c/16f*2-1)*EyeHalfWidth;
                    float open=eyeHalfHeight*MathF.Pow(Math.Max(0,1-MathF.Pow(y/EyeHalfWidth,2)),.65f);
                    return new Vertex(EyeSurface(side,y,(r/8f*2-1)*open,0),Vector3.Forward,Vector3.Up,Vector2.Zero);
                }
                for(int r=0;r<8;r++)for(int c=0;c<16;c++)Quad(white,At(r,c),At(r+1,c),At(r+1,c+1),At(r,c+1));
                void Disc(List<Vertex> list,float radius,float lift,float offsetY=0,float offsetZ=0)
                {
                    Vertex D(float a,float length)=>new(EyeSurface(side,offsetY+MathF.Cos(a)*length,offsetZ+MathF.Sin(a)*length,lift),Vector3.Forward,Vector3.Up,Vector2.Zero);
                    for(int s=0;s<28;s++)Quad(list,D(0,0),D(s*MathF.Tau/28,radius),D((s+1)*MathF.Tau/28,radius),D(0,0));
                }
                Disc(iris,.031f,.002f);Disc(pupil,.013f,.004f);Disc(glint,.005f,.006f,-.008f,.010f);
            }
            Add("Eye whites",white,"#ede9dc");Add("Irises",iris,id.Eyes);Add("Pupils",pupil,"#26333b",true);Add("Eye glints",glint,"#ffffff");
        }
        void Ribbon(List<Vertex> vertices,Func<float,Vector3> centre,float halfWidth,int steps=24)
        {
            Vertex At(int i,int side)
            {
                float u=i/(float)steps;var p=centre(u);
                float taper=MathF.Pow(MathF.Sin(MathF.PI*u),.4f);
                float v=.5f-p.z;
                float lift=Math.Max(.007f,p.x-FacePoint(p.y,v).x);
                p=FacePoint(p.y,v+(side==0?1:-1)*halfWidth*taper,lift);
                return new Vertex(p,Vector3.Forward,Vector3.Up,Vector2.Zero);
            }
            for(int i=0;i<steps;i++)Quad(vertices,At(i,0),At(i+1,0),At(i+1,1),At(i,1));
        }
        void Features()
        {
            var brows=new List<Vertex>();var lids=new List<Vertex>();var lip=new List<Vertex>();var mouth=new List<Vertex>();var nostrils=new List<Vertex>();
            for(int side=-1;side<=1;side+=2)
            {
                Ribbon(brows,u=>
                {
                    float y=side*(eyeGap+(.5f-u)*.184f);
                    float v=EyeV-.073f+(.008f+.018f*t.BrowTilt)*(u-.5f)-.009f*MathF.Sin(MathF.PI*u);
                    return FacePoint(y,v,.006f);
                },.010f*(1+t.BrowThickness*.75f));
                Ribbon(lids,u=>
                {
                    float y=(u*2-1)*EyeHalfWidth;
                    float z=eyeHalfHeight*MathF.Pow(Math.Max(0,1-MathF.Pow(y/EyeHalfWidth,2)),.65f);
                    return EyeSurface(side,y,z,.003f);
                },.0035f);
                Ribbon(nostrils,u=>FacePoint(side*noseWidth*.59f+(u-.5f)*.030f,NoseV+.025f-.004f*MathF.Sin(MathF.PI*u),.003f),.0028f,12);
            }
            // Small lip colour accent follows the sculpted lip, not a skin image.
            Ribbon(lip,u=>FacePoint((u*2-1)*mouthWidth*.93f,.762f-.003f*MathF.Cos(u*MathF.Tau),.004f),.0095f);
            Ribbon(mouth,u=>FacePoint((u*2-1)*mouthWidth,.744f+.002f*MathF.Cos(u*MathF.Tau),.004f),.0026f);
            Add("Brows",brows,id.Hair,true);Add("Lid rims",lids,"#8b6650",true);
            Add("Lip colour",lip,"#c99a8c",true);Add("Mouth line",mouth,"#986d5d",true);Add("Nostrils",nostrils,"#89604b",true);
        }
        float Hairline(float yaw)
        {
            float co=MathF.Cos(yaw),si=MathF.Sin(yaw);
            float fringe=id.Haircut==HeadHaircut.SideSweep?.008f*co*co*Math.Max(0,co)+.025f*si*Math.Max(0,co):0;
            return .218f-.022f*t.Forehead+.52f*(1-co)*.5f-.045f*si*si*Math.Max(0,co)+fringe;
        }
        Vector3 HairPoint(float yaw,float v)
        {
            var p=Point(yaw,v);var n=Normal(yaw,v);
            // Broad authored locks rather than noisy strands. All directions
            // follow the scalp and sweep towards the tied back silhouette.
            bool sideSweep=id.Haircut==HeadHaircut.SideSweep;
            float progress=v/Hairline(yaw),front=Math.Max(0,MathF.Cos(yaw));
            float flow=p.y/(.48f+.25f*p.x)+(sideSweep?.4f*progress:0);
            float lockWave=.030f*MathF.Cos(flow*MathF.PI*3.5f)*MathF.Sin(MathF.PI*v/Hairline(yaw));
            float quiff=sideSweep?.095f*front*(1+.35f*MathF.Sin(yaw))*MathF.Sin(MathF.PI*progress):0;
            return p*1.04f+n*(.013f+lockWave)+Vector3.Up*(.012f+quiff);
        }
        Vector3 HairNormal(float yaw,float v)
        {
            const float e=.001f;
            return Vector3.Cross(HairPoint(yaw,Math.Min(Hairline(yaw),v+e))-HairPoint(yaw,Math.Max(0,v-e)),HairPoint(yaw+e,v)-HairPoint(yaw-e,v)).Normal;
        }
        void Hair()
        {
            var vertices=new List<Vertex>();const int rows=24,sides=64;
            Vertex At(int r,int s)
            {
                float yaw=s*MathF.Tau/sides,v=r/(float)rows*Hairline(yaw);
                return new Vertex(HairPoint(yaw,v),HairNormal(yaw,v),Vector3.Up,Vector2.Zero);
            }
            for(int r=0;r<rows;r++)for(int s=0;s<sides;s++)Quad(vertices,At(r,s),At(r+1,s),At(r+1,s+1),At(r,s+1));
            Add("Hair",vertices,id.Hair);
            if(id.TiedHair)
            {
                var bun=new List<Vertex>();Ellipsoid(bun,new Vector3(-.43f,0,.38f),new Vector3(.11f,.13f,.105f),16,28);
                Add("Bun",bun,id.Hair);
            }
        }
        float BeardDistance(float yaw,float v)
        {
            // Colour belongs to the sculpt's own triangles: no intersecting
            // translucent shell, and no beard at all on a clean-shaven profile.
            if(id.Beard<=0)return -1;
            yaw=MathF.Atan2(MathF.Sin(yaw),MathF.Cos(yaw));
            float ay=Math.Abs(MathF.Sin(yaw));
            float Smooth(float from,float to,float value){float u=Math.Clamp((value-from)/(to-from),0,1);return u*u*(3-2*u);}
            float start=.78f-.17f*Smooth(.25f,.7f,ay)-.20f*Smooth(.83f,1,ay);
            float opening=Math.Clamp((.43f-ay)/.21f,0,1);opening=opening*opening*(3-2*opening);
            start=start*(1-opening)+.785f*opening;
            float y=Math.Abs(Point(yaw,v).y),span=y/mouthWidth;
            float main=Math.Min(v-start,MathF.PI*.59f-Math.Abs(yaw));
            float moustacheTop=.692f-.014f*MathF.Exp(-MathF.Pow((span-.45f)/.3f,2))+.026f*span*span*span;
            float moustacheBottom=.726f-.007f*span*span;
            float moustache=Math.Min(Math.Min(v-moustacheTop,moustacheBottom-v),
                Math.Min(Math.Min((1.12f-span)*.1f,y-.003f),.5f-Math.Abs(yaw)));
            return Math.Max(main,moustache);
        }
        public HeadAsset Build()
        {
            Skin();Eyes();Features();Hair();
            // One palette material and one mesh per visible head. A separate
            // sculpt inspection mesh is cached but hidden during ordinary play.
            var all=new List<Vertex>();var sculpt=new List<Vertex>();
            using var palette=new Bitmap(paint.Count*8,8);
            float Srgb(float c)=>c<=.0031308f?12.92f*c:1.055f*MathF.Pow(c,1/2.4f)-.055f;
            for(int i=0;i<paint.Count;i++)
            {
                var part=paint[i];var color=new Color(Srgb(part.Tint.r),Srgb(part.Tint.g),Srgb(part.Tint.b));
                for(int y=0;y<8;y++)for(int x=0;x<8;x++)palette.SetPixel(i*8+x,y,color);
                foreach(var vertex in part.Vertices)
                {
                    all.Add(new Vertex(vertex.Position,vertex.Normal,Vector3.Up,new Vector2((i+.5f)/paint.Count,.5f)));
                    if(!part.Ink)sculpt.Add(vertex);
                }
            }
            var material=Material.Load("materials/default.vmat").CreateCopy($"socer-figurine-{id.Id}");
            material.Set("g_tColor",palette.ToTexture());material.Set("g_flModelTintAmount",1f);
            if(whiteMaterial==null)
            {
                using var white=new Bitmap(2,2);white.Clear(Color.White);
                whiteMaterial=Material.Load("materials/default.vmat").CreateCopy("socer-figurine-sculpt");
                whiteMaterial.Set("g_tColor",white.ToTexture());whiteMaterial.Set("g_flModelTintAmount",1f);
            }
            Model MeshModel(List<Vertex> vertices,Material shader)
            {
                var bounds=new BBox(new Vector3(-.65f),new Vector3(.65f));
                var mesh=new Mesh(shader);mesh.CreateVertexBuffer(vertices.Count,vertices);mesh.Bounds=bounds;
                return Model.Builder.AddMesh(mesh).WithViewBounds(bounds).Create();
            }
            var parts=new[]{new HeadMeshPart("Skin",MeshModel(all,material),Color.White),new HeadMeshPart("Sculpt",MeshModel(sculpt,whiteMaterial),Color.White)};
            return new(new Vector3(.33f,.285f,.36f*Deviation(t.Length)),parts,triangles,Ink(id.Skin));
        }
    }
}