Editor/Effigy/Rig/CitizenBoneMap.cs
using System;
using System.Collections.Generic;
namespace Effigy;
/// <summary>
/// Correspondences from common humanoid bone namings onto <see cref="CitizenSkeleton"/>.
///
/// WHY A PRESET RATHER THAN A GUESS. <see cref="SkeletonRetarget"/> refuses to infer the map,
/// because a wrong inference looks like bad weights rather than a bad guess. But the naming most
/// rigs arrive in is not a mystery - Unreal's skeleton and everything that copies it use
/// `upperarm_L`, `thigh_L`, `spine_01`, and that convention covers a large fraction of anything
/// somebody drags in. So the guessing happens ONCE, here, in the open, where the decisions can be
/// read and argued with.
///
/// TWO PLACES CITIZEN CANNOT TAKE WHAT IT IS OFFERED, both recorded rather than hidden:
///
/// A PINKY HAS NOWHERE TO GO. Citizen's hand is index, middle, ring and thumb - there is no
/// `finger_pinky_*`. A five-fingered mesh's pinky is mapped onto the ring chain, so it follows the
/// nearest finger instead of going rigid. Several source bones naming one target is a case
/// SkeletonRetarget handles by summing, so this needs nothing special of the caller.
///
/// A BROW HAS NOWHERE TO GO EITHER. Citizen carries eyelids (`face_lid_upper_L`) and eyes, but no
/// brow. Brow bones map onto `head`, which makes them rigid rather than wrong - the alternative is
/// stranding whatever they were weighted to.
/// </summary>
public static class CitizenBoneMap
{
/// <summary>
/// Unreal-style humanoid naming onto citizen's, for both sides.
///
/// Keys are SOURCE bone names, as <see cref="SkeletonRetarget.To"/> wants them. A source
/// skeleton missing some of these is fine; entries with no matching bone are simply never
/// consulted.
/// </summary>
/// <summary>
/// Source bones from <see cref="UnrealStyle"/> that are bound to a citizen bone but must NOT
/// snap onto it — pass this as <c>rideAlong</c> to <see cref="SkeletonRetarget.To"/>.
///
/// Each of these is a bone citizen does not really have. The pinky is bound to the ring finger
/// so it still bends with the hand; the brow to the head; the eyelids to citizen's, which sit
/// on a differently shaped skull five inches away. Left to snap, every one of them drags its
/// geometry across the model — measured at 9.1 inches for the eyelids on the Gearhead, which
/// looked like the face had exploded.
/// </summary>
public static HashSet<string> UnrealStyleRideAlong()
{
var set = new HashSet<string>( StringComparer.OrdinalIgnoreCase );
foreach ( var side in new[] { "L", "R" } )
{
for ( var j = 1; j <= 3; j++ )
set.Add( $"pinky_0{j}_{side}" );
set.Add( $"brow_{side}" );
set.Add( $"lid_{side}" );
set.Add( $"lidup_{side}" );
set.Add( $"lidlo_{side}" );
set.Add( $"iris_{side}" );
set.Add( $"pupil_{side}" );
set.Add( $"thumb_03_{side}" );
}
return set;
}
/// <summary>
/// Which bone each of citizen's bones points at, for <see cref="SkeletonRetarget.Fit"/> - the
/// direction a bone runs is what gets swung onto the source's limb.
///
/// NAMED, NOT DERIVED, because the derivation that works on a clean chain is wrong on citizen.
/// spine_2's children are the neck AND both clavicles; the hand's furthest child is an IK rule
/// bone forty inches away. The hand points at the middle of its knuckles, so no one finger's
/// splay tilts it.
///
/// A bone missing from here keeps its parent's swing: the head, the fingertips, the balls of
/// the feet, every twist, helper and IK bone. That is the right answer for all of them - they
/// hang rigidly off their parent, or the anim graph drives them.
/// </summary>
public static Dictionary<string, string[]> ChainAims()
{
var aims = new Dictionary<string, string[]>( StringComparer.OrdinalIgnoreCase )
{
["pelvis"] = new[] { "spine_0" },
["spine_0"] = new[] { "spine_1" },
["spine_1"] = new[] { "spine_2" },
["spine_2"] = new[] { "neck_0" },
["neck_0"] = new[] { "head" },
};
foreach ( var side in new[] { "L", "R" } )
{
aims[$"clavicle_{side}"] = new[] { $"arm_upper_{side}" };
aims[$"arm_upper_{side}"] = new[] { $"arm_lower_{side}" };
aims[$"arm_lower_{side}"] = new[] { $"hand_{side}" };
aims[$"hand_{side}"] = new[] { $"finger_index_0_{side}", $"finger_middle_0_{side}", $"finger_ring_0_{side}" };
foreach ( var finger in new[] { "index", "middle", "ring" } )
{
aims[$"finger_{finger}_meta_{side}"] = new[] { $"finger_{finger}_0_{side}" };
aims[$"finger_{finger}_0_{side}"] = new[] { $"finger_{finger}_1_{side}" };
aims[$"finger_{finger}_1_{side}"] = new[] { $"finger_{finger}_2_{side}" };
}
aims[$"finger_thumb_0_{side}"] = new[] { $"finger_thumb_1_{side}" };
aims[$"finger_thumb_1_{side}"] = new[] { $"finger_thumb_2_{side}" };
aims[$"leg_upper_{side}"] = new[] { $"leg_lower_{side}" };
aims[$"leg_lower_{side}"] = new[] { $"ankle_{side}" };
aims[$"ankle_{side}"] = new[] { $"ball_{side}" };
}
return aims;
}
/// <summary>
/// Whether a bone of a FITTED citizen should ignore the translation in citizen's clips - the
/// `ignore_Translation` flag on the model's BoneMarkup.
///
/// A clip carries every bone's local translation, which is citizen's bone lengths. Play it on a
/// skeleton fitted into a differently proportioned body and each bone is pulled back to
/// citizen's length, stretching the skin between them exactly as a snapping retarget does. With
/// the flag, the bone keeps its bind translation - the fitted one - and takes only the clip's
/// rotation. Citizen's own markup already does this to its twist, helper and aim bones, which
/// is how it is known what the flag means.
///
/// NOT EVERYTHING. The pelvis keeps the clip's translation: that is the body's height and the
/// bob in the walk. root_IK and its targets keep theirs, because an IK target's translation is
/// the clip saying where the hand or foot goes, and so do the ikrule bones that carry one hand's
/// placement relative to the other.
/// </summary>
public static bool KeepsOwnLength( Skeleton skeleton, int bone )
{
ArgumentNullException.ThrowIfNull( skeleton );
var name = skeleton.Bones[bone].Name;
if ( name.StartsWith( "aim_matrix", StringComparison.OrdinalIgnoreCase ) )
return true;
if ( name.Contains( "ikrule", StringComparison.OrdinalIgnoreCase ) || name == "pelvis" )
return false;
for ( var p = skeleton.Bones[bone].Parent; p >= 0; p = skeleton.Bones[p].Parent )
{
if ( skeleton.Bones[p].Name == "pelvis" )
return true;
}
return false;
}
public static Dictionary<string, string> UnrealStyle()
{
var map = new Dictionary<string, string>( StringComparer.OrdinalIgnoreCase )
{
["pelvis"] = "pelvis",
["spine_01"] = "spine_0",
["spine_02"] = "spine_1",
["spine_03"] = "spine_2",
["chest"] = "spine_2",
["neck"] = "neck_0",
["neck_01"] = "neck_0",
["head"] = "head",
};
foreach ( var side in new[] { "L", "R" } )
{
map[$"clavicle_{side}"] = $"clavicle_{side}";
map[$"upperarm_{side}"] = $"arm_upper_{side}";
map[$"forearm_{side}"] = $"arm_lower_{side}";
map[$"hand_{side}"] = $"hand_{side}";
map[$"thigh_{side}"] = $"leg_upper_{side}";
map[$"calf_{side}"] = $"leg_lower_{side}";
map[$"foot_{side}"] = $"ankle_{side}";
map[$"ball_{side}"] = $"ball_{side}";
map[$"toe_{side}"] = $"ball_{side}";
// Source numbering starts at 01, citizen's at 0.
foreach ( var finger in new[] { "index", "middle", "ring" } )
{
for ( var j = 1; j <= 3; j++ )
map[$"{finger}_0{j}_{side}"] = $"finger_{finger}_{j - 1}_{side}";
}
// See the note above: citizen has no pinky, so it rides the ring finger.
for ( var j = 1; j <= 3; j++ )
map[$"pinky_0{j}_{side}"] = $"finger_ring_{j - 1}_{side}";
// And citizen's thumb is two bones where most rigs have two or three; a third folds
// onto the tip rather than being stranded.
map[$"thumb_01_{side}"] = $"finger_thumb_0_{side}";
map[$"thumb_02_{side}"] = $"finger_thumb_1_{side}";
map[$"thumb_03_{side}"] = $"finger_thumb_2_{side}";
map[$"lid_{side}"] = $"face_lid_upper_{side}";
map[$"lidup_{side}"] = $"face_lid_upper_{side}";
map[$"lidlo_{side}"] = $"face_lid_lower_{side}";
map[$"eye_{side}"] = $"eye_{side}";
map[$"ear_{side}"] = $"ear_{side}";
// An eye built as a stack - eyeball, iris, pupil - is three bones where citizen has
// one. The inner two ride the eye rather than snapping onto it, or the iris and pupil
// both land on the eyeball's centre and disappear inside it.
map[$"iris_{side}"] = $"eye_{side}";
map[$"pupil_{side}"] = $"eye_{side}";
// No citizen brow. Rigid on the head beats stranded.
map[$"brow_{side}"] = "head";
}
return map;
}
/// <summary>
/// The map a playermodel export should use: <see cref="UnrealStyle"/>, plus every one of
/// citizen's own bone names standing for itself.
///
/// WHY BOTH. A rig arrives named one of two ways and neither is wrong. Somebody following the
/// tutorial names bones `upperarm_L` and `thigh_L`, because that is the convention the tutorial
/// teaches and the one most tools produce. Somebody who imported citizen's skeleton, or who read
/// the bone list out of the engine, has `arm_upper_L` and `leg_upper_L` already - and under
/// UnrealStyle alone every one of those bones is UNMAPPED, which strands the geometry weighted
/// to it. An identity entry costs nothing and removes a whole class of "it compiled and came out
/// inside out".
///
/// UNREAL'S SPELLING WINS A COLLISION, because the identity entries are added first and the
/// Unreal ones overwrite them. Only `head`, `hand_L` and `hand_R` are spelled the same in both
/// conventions, and all three mean the same bone, so there is no collision that matters - but
/// the order is fixed rather than incidental, so a future name added to either side cannot
/// quietly change which target an existing rig resolves to.
/// </summary>
public static Dictionary<string, string> Playermodel()
{
var map = new Dictionary<string, string>( StringComparer.OrdinalIgnoreCase );
foreach ( var bone in CitizenSkeleton.Build().Bones )
map[bone.Name] = bone.Name;
foreach ( var pair in UnrealStyle() )
map[pair.Key] = pair.Value;
return map;
}
}