Effigy.Tests/TwistWeightsTests.cs
using System;
using System.Linq;
namespace Effigy.Tests;
/// <summary>
/// Spreading a limb's weight onto the twist bones that share its length.
///
/// WHAT THIS IS GUARDING. Wrong twist weights are invisible until something drives the twist
/// bones, and nothing does until the model carries citizen's constraint list - so the damage
/// surfaces one change after the mistake, looking like a bug in whatever landed last. The
/// property worth pinning is the one that failed: the parent owns the FAR end of the bone, and no
/// twist may ever sit beyond it.
/// </summary>
public static class TwistWeightsTests
{
public static void Run()
{
Report.Section( "twist weights: the parent owns the far end of the bone" );
TestStationOrder();
Report.Section( "twist weights: citizen's limbs spread without inverting" );
TestCitizen();
}
/// <summary>
/// THE ONE THAT MADE A STICK FIGURE. A skeleton built from a table of transforms never sets
/// Bone.Length, so every bone reports the default 1. Placing the parent's station at that
/// length put it an inch down a seven inch forearm - before the twist at 3.9 - so everything
/// past the elbow went to a bone driven at a fraction of the joint's roll, and the limb
/// collapsed onto its own axis.
/// </summary>
static void TestStationOrder()
{
var s = new Skeleton();
s.AddBone( "arm_lower_L", -1, Xform.Identity );
s.AddBone( "arm_lower_L_twist0", 0, Xform.Identity );
s.AddBone( "arm_lower_L_twist1", 0, Xform.Translate( new Vec3( 0, 3.9f, 0 ) ) );
s.AddBone( "hand_L", 0, Xform.Translate( new Vec3( 0, 7.7f, 0 ) ) );
// Three vertices: at the elbow, midway, and out at the wrist.
var mesh = new PolyMesh();
mesh.Positions.Add( new Vec3( 0, 0.2f, 0 ) );
mesh.Positions.Add( new Vec3( 0, 3.9f, 0 ) );
mesh.Positions.Add( new Vec3( 0, 7.5f, 0 ) );
mesh.Faces.Add( new Face( new[] { 0, 1, 2 } ) );
mesh.Skin = new SkinWeights();
for ( var i = 0; i < 3; i++ )
mesh.Skin.Vertices.Add( new[] { new BoneWeight( 0, 1f ) } );
TwistWeights.Spread( mesh, s );
string Owner( int v ) => s.Bones[mesh.Skin[v].OrderByDescending( w => w.Weight ).First().Bone].Name;
Report.Check( $"at the elbow the weight goes to twist0 (got {Owner( 0 )})",
Owner( 0 ) == "arm_lower_L_twist0" );
Report.Check( $"midway it goes to twist1 (got {Owner( 1 )})",
Owner( 1 ) == "arm_lower_L_twist1" );
Report.Check( $"at the wrist it stays on the bone itself (got {Owner( 2 )})",
Owner( 2 ) == "arm_lower_L", "the parent's station was placed short - see Extent" );
foreach ( var v in Enumerable.Range( 0, 3 ) )
{
var total = mesh.Skin[v].Sum( w => w.Weight );
Report.Check( $"vertex {v} still sums to 1 ({total:0.####})", MathF.Abs( total - 1f ) < 1e-4f );
}
}
/// <summary>The real skeleton, where the lengths are all the default and the geometry has to
/// supply the answer instead.</summary>
static void TestCitizen()
{
var citizen = CitizenSkeleton.Build();
var mesh = new PolyMesh();
var bones = new[] { "arm_lower_L", "arm_upper_R", "leg_upper_L", "leg_lower_R" };
// One vertex out at each limb's far end, where the parent must still own it.
foreach ( var name in bones )
{
var b = citizen.IndexOf( name );
var far = citizen.WorldBind( b ).Origin + citizen.WorldBind( b ).Y.Normal * 6f;
mesh.Positions.Add( far );
}
mesh.Faces.Add( new Face( new[] { 0, 1, 2 } ) );
mesh.Skin = new SkinWeights();
foreach ( var name in bones )
mesh.Skin.Vertices.Add( new[] { new BoneWeight( citizen.IndexOf( name ), 1f ) } );
TwistWeights.Spread( mesh, citizen );
for ( var i = 0; i < bones.Length; i++ )
{
var owner = citizen.Bones[mesh.Skin[i].OrderByDescending( w => w.Weight ).First().Bone].Name;
Report.Check( $"{bones[i]} still owns its own far end (got {owner})",
!owner.Contains( "twist", StringComparison.Ordinal ) || owner.StartsWith( bones[i] ),
"a twist took geometry beyond the end of its parent" );
}
}
}