Editor/Effigy/Rig/TwistWeights.cs
using System;
using System.Collections.Generic;
using System.Linq;
namespace Effigy;
/// <summary>
/// Spread a limb bone's weight onto the twist bones that share its length.
///
/// WHY A RETARGET LEAVES A MODEL LOOKING FINE UNTIL IT MOVES. Citizen's arms and legs are not one
/// bone each. Every upper and lower limb bone carries a pair of twist bones - `arm_lower_L_twist0`
/// at the elbow, `arm_lower_L_twist1` partway to the wrist - which the anim graph drives at
/// fractions of the joint's roll. Citizen's own mesh is weighted across them, so a forearm that
/// rotates ninety degrees at the wrist unwinds gradually along its length, the way an arm does.
///
/// A mesh that arrives from somewhere else has never heard of them. <see cref="SkeletonRetarget"/>
/// maps its single `forearm_L` onto `arm_lower_L` and stops, which is correct as far as it goes -
/// the geometry lands in the right place and the model stands there looking perfect. Then the
/// graph rolls the wrist, the whole rotation lands on one ring of vertices because that is the
/// only bone carrying any weight, and the limb pinches shut. It reads as a broken retarget or bad
/// weights rather than as a rig the mesh does not know about, which is a long way to look for it.
///
/// HOW THE WEIGHT IS SPLIT. The twist bones sit at known points along their parent: twist0 at the
/// head, twist1 a fixed distance down, and the parent itself owns the far end. So each is treated
/// as a station on the bone, and a vertex's weight is split between the two stations it lies
/// between, linearly. That is the same shape as the ramp the graph drives them with, and it needs
/// nothing to be authored - the geometry already says where each vertex sits along the limb.
///
/// This is a correction to derived weights, so it runs BEFORE any paint layer: somebody who has
/// painted a vertex by hand meant it, and gets to overrule this.
/// </summary>
public static class TwistWeights
{
/// <summary>
/// Redistribute <paramref name="mesh"/>'s weights across whatever twist bones
/// <paramref name="skeleton"/> has. Returns how many vertices changed.
///
/// A skeleton with no twist bones is left alone, so this is safe to call unconditionally.
/// Naming follows citizen's `<bone>_twist<n>` convention; anything else is ignored
/// rather than guessed at.
/// </summary>
public static int Spread( PolyMesh mesh, Skeleton skeleton )
{
ArgumentNullException.ThrowIfNull( mesh );
ArgumentNullException.ThrowIfNull( skeleton );
if ( !mesh.IsRigged )
return 0;
// Per parent bone: the stations along it, nearest the head first, each with the bone that
// owns it. The parent itself is always the last station, at its own tail.
var stations = new Dictionary<int, List<(float Along, int Bone)>>();
for ( var b = 0; b < skeleton.Count; b++ )
{
var name = skeleton.Bones[b].Name;
var cut = name.LastIndexOf( "_twist", StringComparison.OrdinalIgnoreCase );
if ( cut < 0 || !int.TryParse( name[(cut + 6)..], out _ ) )
continue;
var parent = skeleton.IndexOf( name[..cut] );
if ( parent < 0 || skeleton.Bones[b].Parent != parent )
continue;
// How far down the parent this twist sits, measured along the parent's own axis. The
// axis is the geometric head-to-child direction, not the frame's +Y - citizen's bones
// are +X-down-the-bone, and measuring along +Y would place every twist at the wrong
// station.
var along = Vec3.Dot(
skeleton.WorldBind( b ).Origin - skeleton.WorldBind( parent ).Origin,
skeleton.BoneDirection( parent ) );
if ( !stations.TryGetValue( parent, out var list ) )
stations[parent] = list = new List<(float, int)>();
list.Add( (along, b) );
}
if ( stations.Count == 0 )
return 0;
foreach ( var (parent, list) in stations )
{
list.Add( (Extent( skeleton, parent, list ), parent) );
list.Sort( ( x, y ) => x.Along.CompareTo( y.Along ) );
}
var changed = 0;
for ( var v = 0; v < mesh.VertexCount; v++ )
{
var weights = mesh.Skin[v];
if ( weights.Length == 0 || !weights.Any( w => stations.ContainsKey( w.Bone ) ) )
continue;
var spread = new Dictionary<int, float>();
foreach ( var w in weights )
{
if ( !stations.TryGetValue( w.Bone, out var list ) )
{
spread[w.Bone] = spread.GetValueOrDefault( w.Bone ) + w.Weight;
continue;
}
var bone = skeleton.WorldBind( w.Bone );
var along = Vec3.Dot( mesh.Positions[v] - bone.Origin, skeleton.BoneDirection( w.Bone ) );
foreach ( var (to, share) in Split( list, along ) )
spread[to] = spread.GetValueOrDefault( to ) + w.Weight * share;
}
mesh.Skin[v] = spread.Where( kv => kv.Value > 1e-5f )
.Select( kv => new BoneWeight( kv.Key, kv.Value ) )
.OrderByDescending( w => w.Weight )
.ToArray();
changed++;
}
return changed;
}
/// <summary>
/// How far the parent bone actually reaches, as the station that owns its far end.
///
/// NOT Bone.Length, which is where this went wrong the first time. A skeleton built from a
/// table of transforms - <see cref="CitizenSkeleton"/> is one - never sets a length, so every
/// bone reports the default 1. Placing the parent's station an inch down a seven inch forearm
/// puts it BEFORE the twist that sits at 3.9, inverting the order and handing everything past
/// the elbow to a bone that is driven at a fraction of the joint's roll. The limb collapses,
/// and only once something actually drives the twists - so it looks like a new bug in whatever
/// changed last.
///
/// The distance to the furthest child that is not itself a twist is the honest measure: it is
/// where the next joint is, which is where this bone's flesh ends. Bone.Length is the fallback
/// for a genuine leaf, and the twists themselves are a floor so the order can never invert
/// again whatever the geometry says.
/// </summary>
static float Extent( Skeleton skeleton, int parent, List<(float Along, int Bone)> twists )
{
var axis = skeleton.BoneDirection( parent );
var origin = skeleton.WorldBind( parent ).Origin;
var reach = 0f;
for ( var b = 0; b < skeleton.Count; b++ )
{
if ( skeleton.Bones[b].Parent != parent || twists.Any( t => t.Bone == b ) )
continue;
reach = MathF.Max( reach, Vec3.Dot( skeleton.WorldBind( b ).Origin - origin, axis ) );
}
if ( reach <= 1e-3f )
reach = MathF.Max( skeleton.Bones[parent].Length, 1e-3f );
// Never inside a twist that is already placed further down.
foreach ( var t in twists )
reach = MathF.Max( reach, t.Along + 1e-3f );
return reach;
}
/// <summary>
/// Which stations a point at <paramref name="along"/> belongs to, and how much of each.
///
/// Between two stations it is a straight ramp; past either end it is entirely the station at
/// that end. Clamping rather than extrapolating matters at the shoulder, where geometry rides
/// above the bone's head and a ramp run backwards would hand it a negative share.
/// </summary>
static IEnumerable<(int Bone, float Share)> Split( List<(float Along, int Bone)> stations, float along )
{
if ( along <= stations[0].Along )
{
yield return (stations[0].Bone, 1f);
yield break;
}
for ( var i = 1; i < stations.Count; i++ )
{
if ( along > stations[i].Along )
continue;
var span = stations[i].Along - stations[i - 1].Along;
var t = span <= 1e-6f ? 1f : (along - stations[i - 1].Along) / span;
yield return (stations[i - 1].Bone, 1f - t);
yield return (stations[i].Bone, t);
yield break;
}
yield return (stations[^1].Bone, 1f);
}
}