Template builder that detects a winged creature body plan from a skeleton graph and constructs a RigSkeleton with pelvis, spine, neck/head, optional tail, three-joint wings and optional two-joint legs.
using AutoRig.Analyze;
using AutoRig.Mesh;
using AutoRig.Rig;
namespace AutoRig.Solve.Organic;
// s&box compat: the engine defines Vector2/Vector3 in the GLOBAL namespace, which
// shadows using-directive imports - alias explicitly to System.Numerics.
using Vector3 = System.Numerics.Vector3;
/// <summary>
/// Snaps a winged body plan (bird/dragon-like) to a named joint set: pelvis and a
/// spine chain along the horizontal body, neck/head at the front, optional tail at
/// the rear, three-joint wings from the dominant wide tip pair, and optional
/// two-joint legs from a second low pair. Returns null when the plan cannot be
/// identified.
/// </summary>
public static class WingedTemplate
{
public static RigSkeleton? Build( SkeletonGraph graph, SymmetryPlane symmetry, Aabb3 bounds )
{
ArgumentNullException.ThrowIfNull( graph );
if ( graph.Tips.Count < 4 )
return null;
var up = TemplateUtil.UpAxis( bounds.Size );
var upExtent = MathF.Max( 1e-6f, Vector3.Dot( bounds.Size, new Vector3(
MathF.Abs( up.X ), MathF.Abs( up.Y ), MathF.Abs( up.Z ) ) ) );
var bottom = Vector3.Dot( bounds.Min, up );
float HeightOf( Vector3 p ) => (Vector3.Dot( p, up ) - bottom) / upExtent;
var tipPositions = graph.Tips.Select( t => graph.Nodes[t].Position ).ToList();
var (pairs, unpaired) = CategoryDetector.PairTips(
tipPositions, symmetry, bounds.Size.Length() * 0.15f );
if ( pairs.Count < 1 || unpaired.Count == 0 )
return null;
// Wings = the widest pair.
var bySpan = pairs.OrderByDescending( p =>
Vector3.Distance( tipPositions[p.A], tipPositions[p.B] ) ).ToList();
var wingsPair = bySpan[0];
// Head: the highest unpaired tip.
var headTipIndex = unpaired.OrderByDescending( u => HeightOf( tipPositions[u] ) ).First();
var headPosition = tipPositions[headTipIndex];
var (wingLTip, wingRTip) = TemplateUtil.SideOf( tipPositions[wingsPair.A], symmetry ) > 0
? (wingsPair.A, wingsPair.B)
: (wingsPair.B, wingsPair.A);
var wingL = TemplateUtil.BranchPolyline( graph, graph.Tips[wingLTip] );
var wingR = TemplateUtil.BranchPolyline( graph, graph.Tips[wingRTip] );
var headBranch = TemplateUtil.BranchPolyline( graph, graph.Tips[headTipIndex] );
if ( wingL.Count < 2 || wingR.Count < 2 || headBranch.Count < 2 )
return null;
Vector3 Sym( Vector3 left, Vector3 right )
=> (left + CategoryDetector.Mirror( right, symmetry )) * 0.5f;
Vector3 Flatten( Vector3 p ) => p - up * Vector3.Dot( p, up );
// Body axis: shoulders (wing attachments) toward the head define front.
Vector3 OnPlane( Vector3 p ) => (p + CategoryDetector.Mirror( p, symmetry )) * 0.5f;
var shoulders = OnPlane( Sym( wingL[0], wingR[0] ) );
var forward = Flatten( headPosition - shoulders );
if ( forward.LengthSquared() < 1e-8f )
return null;
forward = Vector3.Normalize( forward );
// Pelvis: behind the shoulders by a fraction of the body length; when a low
// second pair exists (legs), use its attachments instead.
List<Vector3>? legL = null, legR = null;
float PairHeight( (int A, int B) p )
=> (HeightOf( tipPositions[p.A] ) + HeightOf( tipPositions[p.B] )) * 0.5f;
foreach ( var pair in bySpan.Skip( 1 ).OrderBy( PairHeight ) )
{
if ( PairHeight( pair ) > 0.45f )
break;
var (l, r) = TemplateUtil.SideOf( tipPositions[pair.A], symmetry ) > 0
? (pair.A, pair.B)
: (pair.B, pair.A);
var candidateL = TemplateUtil.BranchPolyline( graph, graph.Tips[l] );
var candidateR = TemplateUtil.BranchPolyline( graph, graph.Tips[r] );
if ( candidateL.Count >= 2 && candidateR.Count >= 2 )
{
legL = candidateL;
legR = candidateR;
}
break;
}
var bodyLength = Vector3.Dot( bounds.Size, new Vector3(
MathF.Abs( forward.X ), MathF.Abs( forward.Y ), MathF.Abs( forward.Z ) ) );
var pelvis = legL is not null && legR is not null
? OnPlane( Sym( legL[0], legR[0] ) )
: shoulders - forward * bodyLength * 0.35f;
Vector3 SpineAt( float t ) => Vector3.Lerp( pelvis, shoulders, t );
var skeleton = new RigSkeleton();
int Add( string name, int parent, Vector3 position )
{
skeleton.Joints.Add( new RigJoint { Name = name, Parent = parent, Position = position } );
return skeleton.Joints.Count - 1;
}
var pelvisJ = Add( "pelvis", -1, pelvis );
var spine0 = Add( "spine_0", pelvisJ, SpineAt( 0.35f ) );
var spine1 = Add( "spine_1", spine0, SpineAt( 0.7f ) );
var spine2 = Add( "spine_2", spine1, SpineAt( 1f ) );
var neck = Add( "neck_0", spine2, TemplateUtil.Along( headBranch, 0.2f ) );
Add( "head", neck, TemplateUtil.Along( headBranch, 0.65f ) );
// Optional tail: an unpaired non-head tip behind the pelvis.
foreach ( var u in unpaired )
{
if ( u == headTipIndex )
continue;
if ( Vector3.Dot( Flatten( tipPositions[u] - pelvis ), -forward ) <= 0f )
continue;
var branch = TemplateUtil.BranchPolyline( graph, graph.Tips[u] );
if ( branch.Count < 2 )
continue;
var tail0 = Add( "tail_0", pelvisJ, TemplateUtil.Along( branch, 0.4f ) );
Add( "tail_1", tail0, TemplateUtil.Along( branch, 0.9f ) );
break;
}
// Wings: attachment / mid / tip, three joints each side.
void AddWing( string suffix, bool isLeft )
{
Vector3 At( float t ) => isLeft
? Sym( TemplateUtil.Along( wingL, t ), TemplateUtil.Along( wingR, t ) )
: CategoryDetector.Mirror(
Sym( TemplateUtil.Along( wingL, t ), TemplateUtil.Along( wingR, t ) ), symmetry );
var upper = Add( $"wing_upper_{suffix}", spine2, At( 0f ) );
var lower = Add( $"wing_lower_{suffix}", upper, At( 0.5f ) );
Add( $"wing_tip_{suffix}", lower, At( 0.95f ) );
}
AddWing( "L", isLeft: true );
AddWing( "R", isLeft: false );
// Optional legs: attachment + ankle per side.
if ( legL is not null && legR is not null )
{
Vector3 LegAt( float t, bool isLeft ) => isLeft
? Sym( TemplateUtil.Along( legL, t ), TemplateUtil.Along( legR, t ) )
: CategoryDetector.Mirror(
Sym( TemplateUtil.Along( legL, t ), TemplateUtil.Along( legR, t ) ), symmetry );
var legUpperL = Add( "leg_upper_L", pelvisJ, LegAt( 0f, true ) );
Add( "ankle_L", legUpperL, LegAt( 0.9f, true ) );
var legUpperR = Add( "leg_upper_R", pelvisJ, LegAt( 0f, false ) );
Add( "ankle_R", legUpperR, LegAt( 0.9f, false ) );
}
skeleton.Validate();
return skeleton;
}
}