Editor/Core/Analysis/HandViews.cs
using System.Numerics;
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;
/// <summary>Orthographic hand crops with actual front/back mesh depth, independent of the editor viewport.</summary>
public sealed class HandView
{
public const int Resolution=224;
public Vector3 Center {get;init;}
public Vector3 Right {get;init;}
public Vector3 Up {get;init;}
public Vector3 Normal {get;init;}
public float Extent {get;init;}
public float[] Rgb {get;}=new float[Resolution*Resolution*3];
readonly float[] front=Enumerable.Repeat(float.NegativeInfinity,Resolution*Resolution).ToArray();
readonly float[] back=Enumerable.Repeat(float.PositiveInfinity,Resolution*Resolution).ToArray();
public Vector3 Project(Vector3 point)
{
var d=point-Center;
return new((Vector3.Dot(d,Right)/Extent+.5f)*Resolution,(.5f-Vector3.Dot(d,Up)/Extent)*Resolution,Vector3.Dot(d,Normal));
}
public bool TryLift(Vector3 pixel,out Vector3 point)
{
point=default;if(!Geometry.Finite(pixel))return false;
int x=(int)MathF.Floor(pixel.X),y=(int)MathF.Floor(pixel.Y);
if(x<0||x>=Resolution||y<0||y>=Resolution)return false;
int best=-1;float distance=10;
// Small gaps between separately modelled phalanges are allowed. Larger silhouette misses are rejected.
for(int dy=-2;dy<=2;dy++)for(int dx=-2;dx<=2;dx++)
{
int px=x+dx,py=y+dy;if(px<0||px>=Resolution||py<0||py>=Resolution)continue;
int index=py*Resolution+px;float d=dx*dx+dy*dy;
if(d<distance&&float.IsFinite(front[index])){best=index;distance=d;}
}
if(best<0)return false;
float depth=(front[best]+back[best])*.5f;
point=Center+Right*((pixel.X/Resolution-.5f)*Extent)+Up*((.5f-pixel.Y/Resolution)*Extent)+Normal*depth;
return true;
}
public static HandView[] Render(ImportedCharacter character,Anatomy anatomy,string side)
{
if(!anatomy.Hands.TryGetValue(side,out var frame))return [];
var wrist=anatomy["Hand."+side];float height=anatomy.Height;
bool InHand(Vector3 p)=>Vector3.Distance(p,wrist)<height*.16f&&Vector3.Dot(p-wrist,frame.Forward)>-height*.025f;
var parts=character.Meshes.Where(m=>m.Kind==MeshKind.Body).ToArray();
var vertices=parts.SelectMany(m=>m.Vertices).Where(InHand).ToArray();if(vertices.Length<12)return [];
var result=new List<HandView>();
foreach(float degrees in new[]{0f,35f,-35f,180f,145f,215f})
{
var normal=Vector3.Transform(frame.Normal,Quaternion.CreateFromAxisAngle(frame.Forward,degrees*MathF.PI/180));
var right=Vector3.Normalize(Vector3.Cross(frame.Forward,normal));var up=Vector3.Normalize(Vector3.Cross(normal,right));
float xmin=vertices.Min(p=>Vector3.Dot(p-wrist,right)),xmax=vertices.Max(p=>Vector3.Dot(p-wrist,right));
float ymin=vertices.Min(p=>Vector3.Dot(p-wrist,up)),ymax=vertices.Max(p=>Vector3.Dot(p-wrist,up));
var view=new HandView{Normal=normal,Up=up,Right=right,Center=wrist+right*((xmin+xmax)*.5f)+up*((ymin+ymax)*.5f),Extent=Math.Max(xmax-xmin,ymax-ymin)*1.25f};
Array.Fill(view.Rgb,.08f);
foreach(var part in parts)
{
var normals=new Vector3[part.Vertices.Length];
for(int t=0;t<part.Triangles.Length;t+=3)
{
int a=part.Triangles[t],b=part.Triangles[t+1],c=part.Triangles[t+2];
var n=Vector3.Cross(part.Vertices[b]-part.Vertices[a],part.Vertices[c]-part.Vertices[a]);normals[a]+=n;normals[b]+=n;normals[c]+=n;
}
for(int i=0;i<normals.Length;i++)normals[i]=normals[i].LengthSquared()>1e-12f?Vector3.Normalize(normals[i]):normal;
for(int t=0;t<part.Triangles.Length;t+=3)
{
int a=part.Triangles[t],b=part.Triangles[t+1],c=part.Triangles[t+2];
if(!InHand(part.Vertices[a])||!InHand(part.Vertices[b])||!InHand(part.Vertices[c]))continue;
view.Triangle(view.Project(part.Vertices[a]),view.Project(part.Vertices[b]),view.Project(part.Vertices[c]),normals[a],normals[b],normals[c]);
}
}
result.Add(view);
}
return result.ToArray();
}
void Triangle(Vector3 a,Vector3 b,Vector3 c,Vector3 na,Vector3 nb,Vector3 nc)
{
static float Edge(Vector3 p,Vector3 q,float x,float y)=>(x-p.X)*(q.Y-p.Y)-(y-p.Y)*(q.X-p.X);
float area=Edge(a,b,c.X,c.Y);if(Math.Abs(area)<1e-6f)return;
int xmin=Math.Max(0,(int)MathF.Floor(Math.Min(a.X,Math.Min(b.X,c.X)))),xmax=Math.Min(Resolution-1,(int)MathF.Ceiling(Math.Max(a.X,Math.Max(b.X,c.X))));
int ymin=Math.Max(0,(int)MathF.Floor(Math.Min(a.Y,Math.Min(b.Y,c.Y)))),ymax=Math.Min(Resolution-1,(int)MathF.Ceiling(Math.Max(a.Y,Math.Max(b.Y,c.Y))));
var light=Vector3.Normalize(Normal+Up*.4f+Right*.35f);
for(int y=ymin;y<=ymax;y++)for(int x=xmin;x<=xmax;x++)
{
float u=Edge(b,c,x+.5f,y+.5f)/area,v=Edge(c,a,x+.5f,y+.5f)/area,w=1-u-v;
if(u<0||v<0||w<0)continue;
int index=y*Resolution+x;float z=u*a.Z+v*b.Z+w*c.Z;back[index]=Math.Min(back[index],z);
if(z<=front[index])continue;front[index]=z;
var n=na*u+nb*v+nc*w;if(n.LengthSquared()>1e-12f)n=Vector3.Normalize(n);
float shade=.45f+.5f*Math.Abs(Vector3.Dot(n,light));
Rgb[index*3]=shade*.78f;Rgb[index*3+1]=shade*.68f;Rgb[index*3+2]=shade*.59f;
}
}
}