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