Editor/Effigy/Rig/SkeletonOrder.cs
using System;
using System.Collections.Generic;
using System.Linq;
namespace Effigy;
/// <summary>
/// Put a skeleton into the order an exporter's hierarchy walks it.
///
/// WHY THIS EXISTS. A DMX says what a bone is three times: the dag hierarchy, written by recursing
/// from each root; `jointList`, which is what a vertex's blend indices point into; and
/// `baseStates`, the bind pose the skin is measured against. <see cref="DmxWriter"/> writes the
/// last two in skeleton order and the first depth-first, and those are only the same order if the
/// skeleton HAPPENS to be stored depth-first.
///
/// When they disagree, nothing complains. The model compiles, the bone names, parents and
/// positions all come back correct, and it stands in its bind pose looking perfect - because at
/// bind, every bone's skinning transform is the identity no matter which bind matrix you paired it
/// with. The moment anything animates, each bone is being measured against another bone's bind
/// pose, and the mesh collapses onto its own skeleton. It reads as catastrophically broken weights
/// on a model whose weights are fine.
///
/// So rather than trusting a coincidence, the skeleton is rewritten depth-first and the weights
/// come with it. Then all three orderings are the same one and the question cannot be got wrong.
/// </summary>
public static class SkeletonOrder
{
/// <summary>
/// A copy of <paramref name="skeleton"/> ordered depth-first from its roots, and the map from
/// old bone index to new. Roots keep their relative order, as do siblings, so this is a
/// reshuffle rather than a re-rig - every bone keeps its parent, its local transform and its
/// name.
/// </summary>
public static (Skeleton Ordered, int[] OldToNew) DepthFirst( Skeleton skeleton )
{
ArgumentNullException.ThrowIfNull( skeleton );
var children = new List<int>[skeleton.Count];
for ( var b = 0; b < skeleton.Count; b++ )
children[b] = new List<int>();
var roots = new List<int>();
for ( var b = 0; b < skeleton.Count; b++ )
{
if ( skeleton.Bones[b].Parent < 0 )
roots.Add( b );
else
children[skeleton.Bones[b].Parent].Add( b );
}
var order = new List<int>( skeleton.Count );
void Visit( int b )
{
order.Add( b );
foreach ( var c in children[b] )
Visit( c );
}
foreach ( var r in roots )
Visit( r );
if ( order.Count != skeleton.Count )
throw new InvalidOperationException(
$"Only {order.Count} of {skeleton.Count} bones are reachable from a root - the hierarchy has a cycle" );
var oldToNew = new int[skeleton.Count];
for ( var i = 0; i < order.Count; i++ )
oldToNew[order[i]] = i;
var ordered = new Skeleton();
foreach ( var b in order )
{
var bone = skeleton.Bones[b];
ordered.AddBone( bone.Name, bone.Parent < 0 ? -1 : oldToNew[bone.Parent], bone.Local, bone.Length );
}
return (ordered, oldToNew);
}
/// <summary>Rewrite a mesh's blend indices through <paramref name="oldToNew"/>, in place.</summary>
public static void Remap( PolyMesh mesh, int[] oldToNew )
{
ArgumentNullException.ThrowIfNull( mesh );
ArgumentNullException.ThrowIfNull( oldToNew );
if ( !mesh.IsRigged )
return;
for ( var v = 0; v < mesh.Skin.Count; v++ )
{
mesh.Skin[v] = mesh.Skin[v]
.Select( w => new BoneWeight(
w.Bone >= 0 && w.Bone < oldToNew.Length ? oldToNew[w.Bone] : w.Bone, w.Weight ) )
.ToArray();
}
}
}