A static library of built-in skeleton mapping presets for a humanoid retargeter. It constructs many Profile instances with regex namespace patterns and role->alias tables for various common rig naming conventions (Mixamo, UE mannequin, SMPL, VRM, s&box citizen, etc.). These profiles are returned individually and as an ordered All list used for detection.
#nullable enable annotations
using System.Collections.Generic;
namespace HumanoidRetargeter.Mapping;
/// <summary>
/// Built-in preset profiles, embedded as C# data (the same data is written to
/// <c>Assets/humanoid_retargeter/profiles/*.json</c> by a regenerate-and-diff test so the
/// shipped JSON can never drift from the code).
/// </summary>
public static class ProfileLibrary
{
/// <summary>Mixamo / Adobe rigs: <c>mixamorig[N]:</c> namespace, <c>LeftArm</c> /
/// <c>LeftForeArm</c> / <c>LeftHandIndex1..3</c> style names.</summary>
public static Profile Mixamo { get; } = BuildMixamo();
/// <summary>
/// Reallusion ActorCore / AccuRig / Character Creator rigs (<c>CC_Base_*</c>).
/// Empirical notes from <c>research/rig_actorcore.json</c>:
/// <list type="bullet">
/// <item><c>CC_Base_Hip</c> is the parent of BOTH <c>CC_Base_Pelvis</c> (leg branch) and
/// <c>CC_Base_Waist</c> (spine branch), i.e. the LCA of legs+spine and the true animated
/// hips root → it carries <see cref="BoneRole.Hips"/>; <c>CC_Base_Pelvis</c> is a
/// leg-branch intermediate and stays unmapped.</item>
/// <item>The neck chain is <c>CC_Base_NeckTwist01 → CC_Base_NeckTwist02 → CC_Base_Head</c>;
/// despite the name, <c>NeckTwist01</c> IS the neck bone (there is no plain
/// <c>CC_Base_Neck</c>), so it is the <see cref="BoneRole.Neck"/> alias. NeckTwist02 is
/// left unmapped. All other Twist/ShareBone helpers are excluded (no aliases).</item>
/// <item><c>CC_Base_L_ToeBase</c> is the toe role; the co-located
/// <c>CC_Base_L_ToeBaseShareBone</c> is a helper and must never be mapped.</item>
/// </list>
/// </summary>
public static Profile ActorCoreCc { get; } = BuildActorCoreCc();
/// <summary>Unreal Engine mannequin (UE4/UE5): <c>pelvis</c>, <c>spine_01..05</c>,
/// <c>clavicle_l</c>, <c>thumb_01_l</c>, UE5 <c>*_metacarpal_*</c>; <c>*_twist_*</c>
/// bones have no aliases and are never mapped.</summary>
public static Profile UeMannequin { get; } = BuildUeMannequin();
/// <summary>Rokoko / Xsens style BVH rigs: plain <c>Hips</c>/<c>Spine..Spine4</c>/<c>
/// LeftArm|LeftUpperArm</c> name variants, usually no fingers.</summary>
public static Profile RokokoBvh { get; } = BuildRokokoBvh();
/// <summary>
/// SMPL body model family (AMASS exports, Meshcapade FBX rigs). Joint names per the
/// published model (vchoutas/smplx <c>joint_names.py</c>, Meshcapade wiki):
/// <c>pelvis</c>, sided <c>hip→knee→ankle→foot</c> legs (the "hip" joint IS the thigh;
/// "ankle" is the foot, "foot" is the toe region) and <c>collar→shoulder→elbow→wrist</c>
/// arms ("shoulder" is the upper arm, "wrist" is the hand; the <c>hand</c> joint is a
/// finger stub and stays unmapped). Both spellings occur in the wild: <c>left_hip</c>
/// (model joints) and <c>L_Hip</c> with gendered FBX prefixes <c>m_avg_</c>/<c>f_avg_</c>
/// (SMPL Unity/FBX rigs). No fingers — that is SMPL-X (<see cref="SmplX"/>), kept as a
/// separate preset so a finger-less SMPL rig still reaches full optional coverage.
/// </summary>
public static Profile Smpl { get; } = BuildSmpl(withFingers: false);
/// <summary>
/// SMPL-X: the SMPL body joints (<see cref="Smpl"/>) plus articulated hands —
/// <c>left_thumb1..3</c>/<c>left_index1..3</c>-style finger joints per
/// vchoutas/smplx <c>joint_names.py</c> (jaw/eye joints carry no humanoid role).
/// Evaluated before <see cref="Smpl"/> so it wins the tie on SMPL-X rigs (both score
/// the body fully; only this one maps the fingers).
/// </summary>
public static Profile SmplX { get; } = BuildSmpl(withFingers: true);
/// <summary>
/// NVIDIA SOMA uniform-proportion skeleton (SOMA/SEED BVH exports, e.g.
/// github.com/NVIDIA/soma-retargeter <c>assets/motions/bvh</c>). Mixamo-like upper-body
/// names, but: spine is <c>Spine1→Spine2→Chest</c> (no plain "Spine"), neck is
/// <c>Neck1→Neck2</c>, the legs are <c>LeftLeg→LeftShin</c> — SOMA's <c>LeftLeg</c> is
/// the THIGH (mixamo's is the calf), which is exactly why the mixamo preset must never
/// claim these rigs — and the four fingers have FOUR segments where segment 1 is a
/// metacarpal (<c>LeftHandIndex1..4</c>; mixamo's 1..3 are the phalanges), so the
/// phalanx roles map to segments 2/3/4.
/// </summary>
public static Profile SomaBvh { get; } = BuildSomaBvh();
/// <summary>
/// Classic BVH / Character-Studio-friendly naming (MotionBuilder "Export BVH to
/// Character Studio" convention, ACCAD-style mocap BVHs): <c>Hips</c>,
/// <c>Chest[2..4]</c> spine, arms <c>Collar→Shoulder→Elbow→Wrist</c> (the "Shoulder"
/// is the upper arm) and legs <c>Hip→Knee→Ankle→Toe</c> (the sided "Hip" is the
/// thigh). No fingers.
/// </summary>
public static Profile ClassicBvh { get; } = BuildClassicBvh();
/// <summary>
/// 3ds Max Character Studio Biped rigs: every bone is "<BipedName> <Part>"
/// where the biped name defaults to <c>Bip01</c> (3ds Max ≤2009) / <c>Bip001</c>
/// (2010+) per the Autodesk "Naming the Biped" documentation; some exporters mangle
/// the spaces to underscores (<c>Bip01_L_Thigh</c>), hence the <c>^Bip\d+[ _]</c>
/// namespace pattern (alias comparison is separator-insensitive, so "L UpperArm" and
/// "L_UpperArm" normalize identically). Sided bones use a bare mid-name <c>L/R</c>:
/// <c>L Clavicle→L UpperArm→L Forearm→L Hand</c> arms,
/// <c>L Thigh→L Calf→L Foot→L Toe0</c> legs. Fingers are numbered chains
/// <c>L Finger0..4</c> (0 = thumb) with phalanx segments <c>Finger01/Finger02</c>
/// etc. (MotionBuilder's "3ds Max Biped Template" characterization maps exactly these
/// names). The COM root <c>Bip01</c> itself, <c>Footsteps</c>, toe segments
/// <c>Toe01/Toe02</c> and <c>HorseLink</c> carry no aliases and are never mapped.
/// </summary>
public static Profile Biped { get; } = BuildBiped();
/// <summary>
/// DAZ/Poser classic naming (Poser 4 era figures, DAZ Generation-4 V4/M4, Genesis 1/2,
/// MakeHuman's "Poser/DAZ names" BVH export — verified against the local
/// <c>dev/corpus/unknown_rigs/makehuman_cmu_03_03_dazNames.bvh</c>): camel-case bones
/// with a lower-case <c>l</c>/<c>r</c> side prefix — <c>hip</c> (the translating
/// root), <c>abdomen[→abdomen2]→chest</c> spine, <c>neck</c>, <c>head</c>,
/// <c>lCollar→lShldr→lForeArm→lHand</c> arms, <c>lThigh→lShin→lFoot→lToe</c> legs and
/// <c>lThumb1..3/lIndex1..3/lMid1..3/lRing1..3/lPinky1..3</c> fingers. The
/// <c>l/rButtock</c> thigh helpers and eye bones carry no aliases and stay unmapped.
/// DAZ Genesis 3/8/9 renamed the skeleton (<c>abdomenLower</c>, <c>lShldrBend</c>, …)
/// and is NOT covered by this preset.
/// </summary>
public static Profile DazPoser { get; } = BuildDazPoser();
/// <summary>
/// Blender Rigify human rigs, per the metarig definition in the rigify add-on
/// (<c>rigify/metarigs/human.py</c>) and the Blender manual's basic.human reference:
/// the spine chain is <c>spine→spine.001..spine.006</c> where <c>spine</c> IS the
/// pelvis/hips bone (it sits at the pelvis and parents the thighs), spine.001–003 are
/// the torso, spine.004/005 the two neck bones (004 carries <see cref="BoneRole.Neck"/>,
/// 005 stays unmapped — same policy as ActorCore's NeckTwist02) and spine.006 is the
/// head. Limbs: <c>shoulder.L→upper_arm.L→forearm.L→hand.L</c>,
/// <c>thigh.L→shin.L→foot.L→toe.L</c>; fingers <c>thumb.01.L..03.L</c> and
/// <c>f_index/f_middle/f_ring/f_pinky.01.L..03.L</c>. The <c>^DEF-</c> namespace
/// pattern also matches rigify's generated deform skeleton (<c>DEF-spine.001</c>,
/// <c>DEF-upper_arm.L</c>, …); the segmented deform twins (<c>DEF-upper_arm.L.001</c>),
/// <c>palm.*</c>, <c>pelvis.L/R</c>, <c>heel.02.L</c>, face bones and the generated
/// ORG-/MCH-/control bones have no aliases and are never mapped.
/// </summary>
public static Profile Rigify { get; } = BuildRigify();
/// <summary>
/// VRoid Studio / VRM avatars (UniVRM exports): <c>J_Bip_<side>_<Part></c>
/// bones where side is <c>C</c> (center), <c>L</c> or <c>R</c> — the standard VRoid
/// skeleton behind the VRM humanoid spec (vrm-c/vrm-specification, humanoid bone map):
/// <c>J_Bip_C_Hips/Spine/Chest/UpperChest/Neck/Head</c>,
/// <c>J_Bip_L_Shoulder→UpperArm→LowerArm→Hand</c>,
/// <c>J_Bip_L_UpperLeg→LowerLeg→Foot→ToeBase</c>, fingers
/// <c>J_Bip_L_Thumb1..3/Index1..3/Middle1..3/Ring1..3/Little1..3</c> ("Little" is the
/// pinky, per the VRM littleProximal/Intermediate/Distal humanoid bones). Secondary
/// physics/adjust bones (<c>J_Sec_*</c>, <c>J_Adj_*</c>) and the <c>Root</c> bone have
/// no aliases and are never mapped.
/// </summary>
public static Profile Vrm { get; } = BuildVrm();
/// <summary>
/// Blender Auto-Rig Pro humanoid FBX exports — bone names verified empirically against
/// the local user repro <c>dev/corpus/todo/Defenses.fbx</c> (the PunchPerfect family):
/// <c>.x</c> suffix marks center bones, <c>.l/.r</c> the sides, and the exported limb
/// deform bones carry the <c>_stretch</c> twin name — <c>root.x</c> is the hips
/// (under a ground bone <c>root</c>), <c>spine_01.x→spine_02.x→spine_03.x</c>,
/// <c>neck.x</c>, <c>head.x</c>, arms <c>shoulder.l→arm_stretch.l→forearm_stretch.l→
/// hand.l</c> (plain "arm", NOT "upperarm"), legs <c>thigh_stretch.l→leg_stretch.l→
/// foot.l→toes_01.l</c> ("leg" is the calf). Fingers keep Auto-Rig Pro's <c>c_</c>
/// control prefix on the exported deform chain: <c>c_thumb1.l..3.l</c>,
/// <c>c_index/c_middle/c_ring/c_pinky1.l..3.l</c>. Leftover finger-tip markers
/// (<c>mixamorig:LeftHandIndex4</c> in the repro) and <c>root</c> have no aliases.
/// </summary>
public static Profile AutoRigPro { get; } = BuildAutoRigPro();
/// <summary>
/// Xsens MVN exports (23-segment MVN body model; MVN Animate/Analyze FBX and BVH):
/// anatomical vertebra names for the spine chain <c>Pelvis→L5→L3→T12→T8</c> (the four
/// exported lumbar/thoracic segments of the MVN model), <c>Neck→Head</c>, arms
/// <c>RightShoulder→RightUpperArm→RightForeArm→RightHand</c> and legs
/// <c>RightUpperLeg→RightLowerLeg→RightFoot→RightToe</c>. Body-suit capture only — no
/// finger segments (Xsens gloves ship as separate data), so the hands are chain tips.
/// </summary>
public static Profile XsensMvn { get; } = BuildXsensMvn();
/// <summary>
/// Perception Neuron / Axis Neuron BVH exports: mixamo-like limb and finger names
/// (<c>RightArm→RightForeArm→RightHand</c>, <c>RightUpLeg→RightLeg→RightFoot</c>,
/// <c>RightHandThumb1..3</c>) but a FOUR-bone spine (<c>Spine→Spine1..Spine3</c>), no
/// toe joints (the feet are chain tips), and per-finger <c>RightInHandIndex</c>-style
/// metacarpal helpers between the hand and the <c>RightHandIndex1..3</c> phalanges.
/// The InHand metacarpals carry no aliases (barely animated palm helpers; mapping them
/// as phalanges would shift every curl one joint outward — the SOMA finger bug class).
/// The extra Spine3 is what lets this preset outscore mixamo on Neuron rigs (and
/// mixamo's toes keep mixamo ahead on real Mixamo rigs).
/// </summary>
public static Profile PerceptionNeuron { get; } = BuildPerceptionNeuron();
/// <summary>
/// Source engine ValveBiped skeletons (HL2/GMod humanoids, playermodels):
/// <c>ValveBiped.Bip01_*</c> names — 3ds-Max-Biped-derived parts behind the fixed
/// namespace, with underscores and a spine chain that SKIPS Spine3
/// (<c>Spine→Spine1→Spine2→Spine4</c>), <c>Neck1</c>/<c>Head1</c>, arms
/// <c>L_Clavicle→L_UpperArm→L_Forearm→L_Hand</c>, legs
/// <c>L_Thigh→L_Calf→L_Foot→L_Toe0</c> and numbered finger chains
/// <c>L_Finger0/01/02</c> (0 = thumb) … <c>L_Finger4/41/42</c> (pinky). Evaluated
/// before <see cref="Biped"/> (same Bip01 ancestry; the plain-Biped preset must never
/// claim a ValveBiped rig). Attachment/weapon helpers (<c>ValveBiped.forward</c>,
/// <c>ValveBiped.Anim_Attachment_*</c>) have no aliases and are never mapped.
/// </summary>
public static Profile ValveBiped { get; } = BuildValveBiped();
/// <summary>
/// DAZ Genesis 3/8(.1) figures: renamed Genesis skeleton (NOT covered by
/// <see cref="DazPoser"/>) — <c>hip</c> is the translating root and the LCA of the
/// <c>pelvis</c> leg branch and the <c>abdomenLower→abdomenUpper→chestLower→chestUpper</c>
/// spine (so <c>hip</c> carries <see cref="BoneRole.Hips"/> and <c>pelvis</c> stays
/// unmapped, same policy as ActorCore's Hip/Pelvis pair). Neck chain
/// <c>neckLower→neckUpper→head</c> (neckUpper unmapped, NeckTwist02 policy). Limbs use
/// Bend/Twist pairs: the Bend bones (<c>lShldrBend</c>, <c>lForearmBend</c>,
/// <c>lThighBend</c>) are the primary limb bones; the co-linear Twist roll helpers
/// (<c>lShldrTwist</c>, <c>lForearmTwist</c>, <c>lThighTwist</c>) carry no aliases and
/// are never mapped. Legs <c>lThighBend→lShin→lFoot→lToe</c> (<c>lMetatarsals</c> is an
/// arch helper between foot and toe, unmapped). Fingers are the classic DAZ
/// <c>lThumb1..3/lIndex1..3/lMid1..3/lRing1..3/lPinky1..3</c>. Genesis 9 renamed the
/// skeleton again (<c>l_upperarm</c>, …) and is NOT covered by this preset.
/// </summary>
public static Profile DazGenesis { get; } = BuildDazGenesis();
/// <summary>
/// The s&box citizen-family skeleton itself (<c>citizen.vmdl</c>,
/// <c>citizen_human_*.vmdl</c> and every community model re-rigged on their skeleton):
/// <c>pelvis</c>, <c>spine_0..2</c>, <c>neck_0</c>, <c>head</c>, reversed-word limb
/// names <c>arm_upper_L→arm_lower_L→hand_L</c> / <c>leg_upper_L→leg_lower_L→
/// ankle_L→ball_L</c>, <c>clavicle_L/R</c> and fingers
/// <c>finger_<name>_{meta,0,1,2}_L</c> (meta = metacarpal, 0/1/2 =
/// proximal/middle/distal). The alias table mirrors
/// <see cref="Target.SboxBoneClassifier"/>'s curated role table — kept in sync by a
/// test. This is what lets a COMPILED s&box model picked as a custom conversion
/// target (or used as a source) be recognized: the reversed word order
/// (<c>arm_upper</c>, not <c>upper_arm</c>) defeats generic token matching, which
/// scored the citizen rig at 5% and made every custom-model pick fail with "not
/// recognized as humanoid". Twist/helper/IK/face bones (<c>*_twist*</c>,
/// <c>*_helper*</c>, <c>eye_*</c>, …) have no aliases and are never mapped.
/// </summary>
public static Profile Sbox { get; } = BuildSbox();
/// <summary>
/// AdvancedSkeleton (Maya auto-rigger, ubiquitous in game rips and mobile-game rigs):
/// <c>Root_M</c> hips, <c>Spine1_M(→Spine2_M)→Chest_M</c> spine, <c>Neck_M→Head_M</c>
/// (small rigs parent <c>Head_M</c> straight to the chest with no neck),
/// <c>Scapula→Shoulder→Elbow→Wrist</c> arms, <c>Hip→Knee→Ankle→Toes</c> legs and
/// <c><Name>Finger1..3</c> fingers, all sided <c>_L/_R</c> (center <c>_M</c>).
/// Twist helpers (<c>ShoulderPart1</c>, <c>HipPart1</c>, …), <c>Cup</c> palm bones and
/// the face rig carry no aliases. Real case: a Sonic mobile-game rip whose name stage
/// scored below threshold — the topology fallback then mapped ARMS AND LEGS ONTO THE
/// HEAD QUILLS (long symmetric chains), playing every clip as garbage.
/// </summary>
public static Profile AdvancedSkeleton { get; } = BuildAdvancedSkeleton();
/// <summary>All built-in presets, in detection order (first wins score ties — see
/// <see cref="SmplX"/> vs <see cref="Smpl"/>; <see cref="ValveBiped"/> is evaluated
/// before <see cref="Biped"/> and <see cref="DazGenesis"/> before
/// <see cref="DazPoser"/> within their families).</summary>
public static IReadOnlyList<Profile> All { get; } =
new[]
{
Sbox, Mixamo, ActorCoreCc, UeMannequin, XsensMvn, PerceptionNeuron, RokokoBvh,
SmplX, Smpl, SomaBvh, ClassicBvh, ValveBiped, Biped, DazGenesis, DazPoser,
Rigify, Vrm, AutoRigPro, AdvancedSkeleton,
};
// ---------------------------------------------------------------- advanced skeleton
private static Profile BuildAdvancedSkeleton()
{
var aliases = new Dictionary<BoneRole, string[]>
{
[BoneRole.Hips] = new[] { "Root_M" },
[BoneRole.Spine0] = new[] { "Spine1_M" },
[BoneRole.Spine1] = new[] { "Spine2_M" },
[BoneRole.Spine2] = new[] { "Chest_M" },
[BoneRole.Neck] = new[] { "Neck_M" },
[BoneRole.Head] = new[] { "Head_M" },
};
foreach (var side in new[] { "L", "R" })
{
aliases[Role("Clavicle", side)] = new[] { $"Scapula_{side}" };
aliases[Role("UpperArm", side)] = new[] { $"Shoulder_{side}" };
aliases[Role("LowerArm", side)] = new[] { $"Elbow_{side}" };
aliases[Role("Hand", side)] = new[] { $"Wrist_{side}" };
aliases[Role("UpperLeg", side)] = new[] { $"Hip_{side}" };
aliases[Role("LowerLeg", side)] = new[] { $"Knee_{side}" };
aliases[Role("Foot", side)] = new[] { $"Ankle_{side}" };
aliases[Role("Toe", side)] = new[] { $"Toes_{side}" };
foreach (var finger in new[] { "Thumb", "Index", "Middle", "Ring", "Pinky" })
{
aliases[Role($"{finger}Prox", side)] = new[] { $"{finger}Finger1_{side}" };
aliases[Role($"{finger}Mid", side)] = new[] { $"{finger}Finger2_{side}" };
aliases[Role($"{finger}Dist", side)] = new[] { $"{finger}Finger3_{side}" };
}
}
return new Profile("advanced_skeleton", new string[0], aliases);
}
// ---------------------------------------------------------------- sbox
private static Profile BuildSbox()
{
var aliases = new Dictionary<BoneRole, string[]>
{
[BoneRole.Hips] = new[] { "pelvis" },
[BoneRole.Spine0] = new[] { "spine_0" },
[BoneRole.Spine1] = new[] { "spine_1" },
[BoneRole.Spine2] = new[] { "spine_2" },
[BoneRole.Spine3] = new[] { "spine_3" },
[BoneRole.Neck] = new[] { "neck_0" },
[BoneRole.Head] = new[] { "head" },
};
foreach (var side in new[] { "L", "R" })
{
aliases[Role("Clavicle", side)] = new[] { $"clavicle_{side}" };
aliases[Role("UpperArm", side)] = new[] { $"arm_upper_{side}" };
aliases[Role("LowerArm", side)] = new[] { $"arm_lower_{side}" };
aliases[Role("Hand", side)] = new[] { $"hand_{side}" };
aliases[Role("UpperLeg", side)] = new[] { $"leg_upper_{side}" };
aliases[Role("LowerLeg", side)] = new[] { $"leg_lower_{side}" };
aliases[Role("Foot", side)] = new[] { $"ankle_{side}" };
aliases[Role("Toe", side)] = new[] { $"ball_{side}" };
foreach (var (finger, rolePrefix) in new[]
{
("thumb", "Thumb"), ("index", "Index"), ("middle", "Middle"),
("ring", "Ring"), ("pinky", "Pinky"),
})
{
aliases[Role($"{rolePrefix}Meta", side)] = new[] { $"finger_{finger}_meta_{side}" };
aliases[Role($"{rolePrefix}Prox", side)] = new[] { $"finger_{finger}_0_{side}" };
aliases[Role($"{rolePrefix}Mid", side)] = new[] { $"finger_{finger}_1_{side}" };
aliases[Role($"{rolePrefix}Dist", side)] = new[] { $"finger_{finger}_2_{side}" };
}
}
return new Profile("sbox", new string[0], aliases);
}
// ---------------------------------------------------------------- mixamo
private static Profile BuildMixamo()
{
var aliases = new Dictionary<BoneRole, string[]>
{
[BoneRole.Hips] = new[] { "Hips" },
[BoneRole.Spine0] = new[] { "Spine" },
[BoneRole.Spine1] = new[] { "Spine1" },
[BoneRole.Spine2] = new[] { "Spine2" },
[BoneRole.Neck] = new[] { "Neck" },
[BoneRole.Head] = new[] { "Head" },
};
foreach (var (roleSide, nameSide) in Sides())
{
aliases[Role("Clavicle", roleSide)] = new[] { $"{nameSide}Shoulder" };
aliases[Role("UpperArm", roleSide)] = new[] { $"{nameSide}Arm" };
aliases[Role("LowerArm", roleSide)] = new[] { $"{nameSide}ForeArm" };
aliases[Role("Hand", roleSide)] = new[] { $"{nameSide}Hand" };
aliases[Role("UpperLeg", roleSide)] = new[] { $"{nameSide}UpLeg" };
aliases[Role("LowerLeg", roleSide)] = new[] { $"{nameSide}Leg" };
aliases[Role("Foot", roleSide)] = new[] { $"{nameSide}Foot" };
aliases[Role("Toe", roleSide)] = new[] { $"{nameSide}ToeBase" };
foreach (var finger in new[] { "Thumb", "Index", "Middle", "Ring", "Pinky" })
{
aliases[Role($"{finger}Prox", roleSide)] = new[] { $"{nameSide}Hand{finger}1" };
aliases[Role($"{finger}Mid", roleSide)] = new[] { $"{nameSide}Hand{finger}2" };
aliases[Role($"{finger}Dist", roleSide)] = new[] { $"{nameSide}Hand{finger}3" };
}
}
// Both ':' (FBX namespace) and '_' (namespace mangled by some exporters) forms occur
// in the wild; some Mixamo downloads ship with no namespace at all, which still
// matches because the aliases are the bare names.
return new Profile("mixamo", new[] { "^mixamorig[0-9]*:", "^mixamorig[0-9]*_" }, aliases);
}
// ---------------------------------------------------------------- actorcore / cc
private static Profile BuildActorCoreCc()
{
var aliases = new Dictionary<BoneRole, string[]>
{
[BoneRole.Hips] = new[] { "Hip" },
[BoneRole.Spine0] = new[] { "Waist" },
[BoneRole.Spine1] = new[] { "Spine01" },
[BoneRole.Spine2] = new[] { "Spine02" },
[BoneRole.Neck] = new[] { "NeckTwist01" },
[BoneRole.Head] = new[] { "Head" },
};
foreach (var roleSide in new[] { "L", "R" })
{
var nameSide = roleSide; // CC bones use the bare side letter: CC_Base_L_Thigh.
aliases[Role("Clavicle", roleSide)] = new[] { $"{nameSide}_Clavicle" };
aliases[Role("UpperArm", roleSide)] = new[] { $"{nameSide}_Upperarm" };
aliases[Role("LowerArm", roleSide)] = new[] { $"{nameSide}_Forearm" };
aliases[Role("Hand", roleSide)] = new[] { $"{nameSide}_Hand" };
aliases[Role("UpperLeg", roleSide)] = new[] { $"{nameSide}_Thigh" };
aliases[Role("LowerLeg", roleSide)] = new[] { $"{nameSide}_Calf" };
aliases[Role("Foot", roleSide)] = new[] { $"{nameSide}_Foot" };
aliases[Role("Toe", roleSide)] = new[] { $"{nameSide}_ToeBase" };
foreach (var (role, cc) in new[]
{
("Thumb", "Thumb"), ("Index", "Index"), ("Middle", "Mid"), ("Ring", "Ring"), ("Pinky", "Pinky"),
})
{
aliases[Role($"{role}Prox", roleSide)] = new[] { $"{nameSide}_{cc}1" };
aliases[Role($"{role}Mid", roleSide)] = new[] { $"{nameSide}_{cc}2" };
aliases[Role($"{role}Dist", roleSide)] = new[] { $"{nameSide}_{cc}3" };
}
}
return new Profile("actorcore_cc", new[] { "^CC_Base_" }, aliases);
}
// ---------------------------------------------------------------- ue mannequin
private static Profile BuildUeMannequin()
{
var aliases = new Dictionary<BoneRole, string[]>
{
[BoneRole.Hips] = new[] { "pelvis" },
[BoneRole.Spine0] = new[] { "spine_01" },
[BoneRole.Spine1] = new[] { "spine_02" },
[BoneRole.Spine2] = new[] { "spine_03" },
[BoneRole.Spine3] = new[] { "spine_04" },
[BoneRole.Spine4] = new[] { "spine_05" },
[BoneRole.Neck] = new[] { "neck_01" },
[BoneRole.Head] = new[] { "head" },
};
foreach (var (roleSide, s) in new[] { ("L", "l"), ("R", "r") })
{
aliases[Role("Clavicle", roleSide)] = new[] { $"clavicle_{s}" };
aliases[Role("UpperArm", roleSide)] = new[] { $"upperarm_{s}" };
aliases[Role("LowerArm", roleSide)] = new[] { $"lowerarm_{s}" };
aliases[Role("Hand", roleSide)] = new[] { $"hand_{s}" };
aliases[Role("UpperLeg", roleSide)] = new[] { $"thigh_{s}" };
aliases[Role("LowerLeg", roleSide)] = new[] { $"calf_{s}" };
aliases[Role("Foot", roleSide)] = new[] { $"foot_{s}" };
aliases[Role("Toe", roleSide)] = new[] { $"ball_{s}" };
foreach (var (role, ue) in new[]
{
("Thumb", "thumb"), ("Index", "index"), ("Middle", "middle"), ("Ring", "ring"), ("Pinky", "pinky"),
})
{
// UE5 mannequin adds metacarpals for the four fingers (not the thumb).
if (role != "Thumb")
aliases[Role($"{role}Meta", roleSide)] = new[] { $"{ue}_metacarpal_{s}" };
aliases[Role($"{role}Prox", roleSide)] = new[] { $"{ue}_01_{s}" };
aliases[Role($"{role}Mid", roleSide)] = new[] { $"{ue}_02_{s}" };
aliases[Role($"{role}Dist", roleSide)] = new[] { $"{ue}_03_{s}" };
}
}
return new Profile("ue_mannequin", new string[0], aliases);
}
// ---------------------------------------------------------------- rokoko / xsens bvh
private static Profile BuildRokokoBvh()
{
var aliases = new Dictionary<BoneRole, string[]>
{
[BoneRole.Hips] = new[] { "Hips" },
// Spine naming varies (Spine, Spine1..Spine4); ordered alias preference plus the
// used-bone exclusion in the detector shifts the chain up when "Spine" is absent.
[BoneRole.Spine0] = new[] { "Spine", "Spine1" },
[BoneRole.Spine1] = new[] { "Spine1", "Spine2" },
[BoneRole.Spine2] = new[] { "Spine2", "Spine3" },
[BoneRole.Spine3] = new[] { "Spine3", "Spine4" },
[BoneRole.Spine4] = new[] { "Spine4" },
[BoneRole.Neck] = new[] { "Neck", "Neck1" },
[BoneRole.Head] = new[] { "Head" },
};
foreach (var (roleSide, nameSide) in Sides())
{
aliases[Role("Clavicle", roleSide)] = new[] { $"{nameSide}Shoulder", $"{nameSide}Collar" };
aliases[Role("UpperArm", roleSide)] = new[] { $"{nameSide}Arm", $"{nameSide}UpperArm" };
aliases[Role("LowerArm", roleSide)] = new[] { $"{nameSide}ForeArm", $"{nameSide}LowerArm" };
aliases[Role("Hand", roleSide)] = new[] { $"{nameSide}Hand" };
aliases[Role("UpperLeg", roleSide)] = new[] { $"{nameSide}UpLeg", $"{nameSide}Thigh", $"{nameSide}UpperLeg" };
aliases[Role("LowerLeg", roleSide)] = new[] { $"{nameSide}Leg", $"{nameSide}Shin", $"{nameSide}LowerLeg" };
aliases[Role("Foot", roleSide)] = new[] { $"{nameSide}Foot" };
aliases[Role("Toe", roleSide)] = new[] { $"{nameSide}Toe", $"{nameSide}ToeBase" };
}
return new Profile("rokoko_bvh", new string[0], aliases);
}
// ---------------------------------------------------------------- xsens mvn
private static Profile BuildXsensMvn()
{
var aliases = new Dictionary<BoneRole, string[]>
{
[BoneRole.Hips] = new[] { "Pelvis" },
// MVN's exported spine segments are the anatomical vertebra levels L5/L3/T12/T8.
[BoneRole.Spine0] = new[] { "L5" },
[BoneRole.Spine1] = new[] { "L3" },
[BoneRole.Spine2] = new[] { "T12" },
[BoneRole.Spine3] = new[] { "T8" },
[BoneRole.Neck] = new[] { "Neck" },
[BoneRole.Head] = new[] { "Head" },
};
foreach (var (roleSide, nameSide) in Sides())
{
aliases[Role("Clavicle", roleSide)] = new[] { $"{nameSide}Shoulder" };
aliases[Role("UpperArm", roleSide)] = new[] { $"{nameSide}UpperArm" };
aliases[Role("LowerArm", roleSide)] = new[] { $"{nameSide}ForeArm" };
aliases[Role("Hand", roleSide)] = new[] { $"{nameSide}Hand" };
aliases[Role("UpperLeg", roleSide)] = new[] { $"{nameSide}UpperLeg" };
aliases[Role("LowerLeg", roleSide)] = new[] { $"{nameSide}LowerLeg" };
aliases[Role("Foot", roleSide)] = new[] { $"{nameSide}Foot" };
aliases[Role("Toe", roleSide)] = new[] { $"{nameSide}Toe" };
}
// Body-suit capture: no finger segments (see the property remarks).
return new Profile("xsens_mvn", new string[0], aliases);
}
// ---------------------------------------------------------------- perception neuron
private static Profile BuildPerceptionNeuron()
{
var aliases = new Dictionary<BoneRole, string[]>
{
[BoneRole.Hips] = new[] { "Hips" },
[BoneRole.Spine0] = new[] { "Spine" },
[BoneRole.Spine1] = new[] { "Spine1" },
[BoneRole.Spine2] = new[] { "Spine2" },
[BoneRole.Spine3] = new[] { "Spine3" },
[BoneRole.Neck] = new[] { "Neck" },
[BoneRole.Head] = new[] { "Head" },
};
foreach (var (roleSide, nameSide) in Sides())
{
aliases[Role("Clavicle", roleSide)] = new[] { $"{nameSide}Shoulder" };
aliases[Role("UpperArm", roleSide)] = new[] { $"{nameSide}Arm" };
aliases[Role("LowerArm", roleSide)] = new[] { $"{nameSide}ForeArm" };
aliases[Role("Hand", roleSide)] = new[] { $"{nameSide}Hand" };
aliases[Role("UpperLeg", roleSide)] = new[] { $"{nameSide}UpLeg" };
aliases[Role("LowerLeg", roleSide)] = new[] { $"{nameSide}Leg" };
aliases[Role("Foot", roleSide)] = new[] { $"{nameSide}Foot" };
// No toe joints in Axis Neuron exports; the feet are chain tips.
// Phalanges only: the LeftInHandIndex-style metacarpal helpers between hand
// and phalanges carry no role (see the property remarks).
foreach (var finger in new[] { "Thumb", "Index", "Middle", "Ring", "Pinky" })
{
aliases[Role($"{finger}Prox", roleSide)] = new[] { $"{nameSide}Hand{finger}1" };
aliases[Role($"{finger}Mid", roleSide)] = new[] { $"{nameSide}Hand{finger}2" };
aliases[Role($"{finger}Dist", roleSide)] = new[] { $"{nameSide}Hand{finger}3" };
}
}
return new Profile("perception_neuron", new string[0], aliases);
}
// ---------------------------------------------------------------- valvebiped
private static Profile BuildValveBiped()
{
var aliases = new Dictionary<BoneRole, string[]>
{
[BoneRole.Hips] = new[] { "Pelvis" },
[BoneRole.Spine0] = new[] { "Spine" },
[BoneRole.Spine1] = new[] { "Spine1" },
[BoneRole.Spine2] = new[] { "Spine2" },
// The stock HL2 chain skips Spine3 (Spine2's child IS Spine4, the chest);
// ordered preference + used-bone exclusion also absorbs a variant that has
// both: Spine3→Spine3 and Spine4→Spine4.
[BoneRole.Spine3] = new[] { "Spine3", "Spine4" },
[BoneRole.Spine4] = new[] { "Spine4" },
[BoneRole.Neck] = new[] { "Neck1" },
[BoneRole.Head] = new[] { "Head1" },
};
foreach (var s in new[] { "L", "R" })
{
aliases[Role("Clavicle", s)] = new[] { $"{s}_Clavicle" };
aliases[Role("UpperArm", s)] = new[] { $"{s}_UpperArm" };
aliases[Role("LowerArm", s)] = new[] { $"{s}_Forearm" };
aliases[Role("Hand", s)] = new[] { $"{s}_Hand" };
aliases[Role("UpperLeg", s)] = new[] { $"{s}_Thigh" };
aliases[Role("LowerLeg", s)] = new[] { $"{s}_Calf" };
aliases[Role("Foot", s)] = new[] { $"{s}_Foot" };
aliases[Role("Toe", s)] = new[] { $"{s}_Toe0" };
// Biped-style numbered finger chains behind the ValveBiped namespace:
// Finger0 is the thumb; segments append the phalanx digit (Finger0 →
// Finger01 → Finger02, Finger1 → Finger11 → …).
foreach (var (finger, n) in new[]
{
("Thumb", 0), ("Index", 1), ("Middle", 2), ("Ring", 3), ("Pinky", 4),
})
{
aliases[Role($"{finger}Prox", s)] = new[] { $"{s}_Finger{n}" };
aliases[Role($"{finger}Mid", s)] = new[] { $"{s}_Finger{n}1" };
aliases[Role($"{finger}Dist", s)] = new[] { $"{s}_Finger{n}2" };
}
}
// The fixed "ValveBiped.Bip01_" namespace: anchored, so plain "Bip01 ..." Character
// Studio rigs never strip it (and the Biped preset's "^Bip\d+[ _]" never matches
// the ValveBiped prefix — the two families cannot cross-claim).
return new Profile("valvebiped", new[] { @"^ValveBiped\.Bip01_" }, aliases);
}
// ---------------------------------------------------------------- daz genesis 3/8
private static Profile BuildDazGenesis()
{
var aliases = new Dictionary<BoneRole, string[]>
{
// "hip" is the translating root and LCA of the pelvis (leg branch) and the
// abdomen (spine branch); "pelvis" is a leg-branch intermediate and stays
// unmapped — same policy as ActorCore's CC_Base_Hip/CC_Base_Pelvis pair.
[BoneRole.Hips] = new[] { "hip" },
[BoneRole.Spine0] = new[] { "abdomenLower" },
[BoneRole.Spine1] = new[] { "abdomenUpper" },
[BoneRole.Spine2] = new[] { "chestLower" },
[BoneRole.Spine3] = new[] { "chestUpper" },
// neckLower→neckUpper→head: neckLower IS the neck; neckUpper stays unmapped
// (same policy as ActorCore's NeckTwist02 / rigify's spine.005).
[BoneRole.Neck] = new[] { "neckLower" },
[BoneRole.Head] = new[] { "head" },
};
foreach (var s in new[] { "L", "R" })
{
var p = s == "L" ? "l" : "r"; // lower-case side prefix: lShldrBend, rThighBend
aliases[Role("Clavicle", s)] = new[] { $"{p}Collar" };
// Bend bones are the primary limb bones; the co-linear *Twist roll helpers
// have no aliases and are never mapped.
aliases[Role("UpperArm", s)] = new[] { $"{p}ShldrBend" };
aliases[Role("LowerArm", s)] = new[] { $"{p}ForearmBend" };
aliases[Role("Hand", s)] = new[] { $"{p}Hand" };
aliases[Role("UpperLeg", s)] = new[] { $"{p}ThighBend" };
aliases[Role("LowerLeg", s)] = new[] { $"{p}Shin" };
aliases[Role("Foot", s)] = new[] { $"{p}Foot" };
// lMetatarsals sits between foot and toe (arch helper, unmapped).
aliases[Role("Toe", s)] = new[] { $"{p}Toe" };
foreach (var (role, daz) in new[]
{
("Thumb", "Thumb"), ("Index", "Index"), ("Middle", "Mid"), ("Ring", "Ring"), ("Pinky", "Pinky"),
})
{
aliases[Role($"{role}Prox", s)] = new[] { $"{p}{daz}1" };
aliases[Role($"{role}Mid", s)] = new[] { $"{p}{daz}2" };
aliases[Role($"{role}Dist", s)] = new[] { $"{p}{daz}3" };
}
}
return new Profile("daz_genesis", new string[0], aliases);
}
// ---------------------------------------------------------------- smpl / smpl-x
private static Profile BuildSmpl(bool withFingers)
{
var aliases = new Dictionary<BoneRole, string[]>
{
[BoneRole.Hips] = new[] { "Pelvis" },
[BoneRole.Spine0] = new[] { "Spine1" },
[BoneRole.Spine1] = new[] { "Spine2" },
[BoneRole.Spine2] = new[] { "Spine3" },
[BoneRole.Neck] = new[] { "Neck" },
[BoneRole.Head] = new[] { "Head" },
};
foreach (var (roleSide, abbr, word) in new[] { ("L", "L", "left"), ("R", "R", "right") })
{
// Both documented spellings per role: abbreviated FBX-rig names ("L_Hip") and
// spelled model joint names ("left_hip"). Comparison is separator-insensitive.
aliases[Role("Clavicle", roleSide)] = new[] { $"{abbr}_Collar", $"{word}_collar" };
aliases[Role("UpperArm", roleSide)] = new[] { $"{abbr}_Shoulder", $"{word}_shoulder" };
aliases[Role("LowerArm", roleSide)] = new[] { $"{abbr}_Elbow", $"{word}_elbow" };
aliases[Role("Hand", roleSide)] = new[] { $"{abbr}_Wrist", $"{word}_wrist" };
aliases[Role("UpperLeg", roleSide)] = new[] { $"{abbr}_Hip", $"{word}_hip" };
aliases[Role("LowerLeg", roleSide)] = new[] { $"{abbr}_Knee", $"{word}_knee" };
aliases[Role("Foot", roleSide)] = new[] { $"{abbr}_Ankle", $"{word}_ankle" };
aliases[Role("Toe", roleSide)] = new[] { $"{abbr}_Foot", $"{word}_foot" };
if (!withFingers)
continue;
// SMPL-X finger joints (left_index1..3 etc., per vchoutas/smplx joint_names.py).
foreach (var finger in new[] { "thumb", "index", "middle", "ring", "pinky" })
{
var name = char.ToUpperInvariant(finger[0]) + finger[1..];
aliases[Role($"{name}Prox", roleSide)] = new[] { $"{word}_{finger}1" };
aliases[Role($"{name}Mid", roleSide)] = new[] { $"{word}_{finger}2" };
aliases[Role($"{name}Dist", roleSide)] = new[] { $"{word}_{finger}3" };
}
}
// Gendered SMPL FBX rigs prefix every bone (m_avg_L_Hip, f_avg_Pelvis).
return new Profile(withFingers ? "smpl_x" : "smpl", new[] { "^m_avg_", "^f_avg_" }, aliases);
}
// ---------------------------------------------------------------- nvidia soma bvh
private static Profile BuildSomaBvh()
{
var aliases = new Dictionary<BoneRole, string[]>
{
[BoneRole.Hips] = new[] { "Hips" },
[BoneRole.Spine0] = new[] { "Spine1" },
[BoneRole.Spine1] = new[] { "Spine2" },
[BoneRole.Spine2] = new[] { "Chest" },
[BoneRole.Neck] = new[] { "Neck1" },
[BoneRole.Head] = new[] { "Head" },
};
foreach (var (roleSide, nameSide) in Sides())
{
aliases[Role("Clavicle", roleSide)] = new[] { $"{nameSide}Shoulder" };
aliases[Role("UpperArm", roleSide)] = new[] { $"{nameSide}Arm" };
aliases[Role("LowerArm", roleSide)] = new[] { $"{nameSide}ForeArm" };
aliases[Role("Hand", roleSide)] = new[] { $"{nameSide}Hand" };
// SOMA's "Leg" is the thigh, "Shin" the calf — the decisive difference from
// mixamo, where "Leg" is the calf under "UpLeg".
aliases[Role("UpperLeg", roleSide)] = new[] { $"{nameSide}Leg" };
aliases[Role("LowerLeg", roleSide)] = new[] { $"{nameSide}Shin" };
aliases[Role("Foot", roleSide)] = new[] { $"{nameSide}Foot" };
aliases[Role("Toe", roleSide)] = new[] { $"{nameSide}ToeBase" };
// Mixamo-style finger NAMES but not mixamo segmentation: SOMA fingers have four
// segments where segment 1 is a metacarpal (measured on the repro BVH: Index1
// sits 3.2 cm from the wrist at the palm base, then a 6.4 cm metacarpal to the
// Index2 knuckle, then 3.7/2.3 cm phalanges to Index3/Index4) — so 2/3/4 are the
// phalanges. Mapping 1..3 as Prox/Mid/Dist (mixamo's segmentation) shifted every
// curl one joint outward and dropped the distal curl entirely (frozen fingers).
// The thumb is three segments plus *End, mapped 1..3 like mixamo's; *End tip
// markers carry no role.
aliases[Role("ThumbProx", roleSide)] = new[] { $"{nameSide}HandThumb1" };
aliases[Role("ThumbMid", roleSide)] = new[] { $"{nameSide}HandThumb2" };
aliases[Role("ThumbDist", roleSide)] = new[] { $"{nameSide}HandThumb3" };
foreach (var finger in new[] { "Index", "Middle", "Ring", "Pinky" })
{
aliases[Role($"{finger}Meta", roleSide)] = new[] { $"{nameSide}Hand{finger}1" };
aliases[Role($"{finger}Prox", roleSide)] = new[] { $"{nameSide}Hand{finger}2" };
aliases[Role($"{finger}Mid", roleSide)] = new[] { $"{nameSide}Hand{finger}3" };
aliases[Role($"{finger}Dist", roleSide)] = new[] { $"{nameSide}Hand{finger}4" };
}
}
return new Profile("soma_bvh", new string[0], aliases);
}
// ---------------------------------------------------------------- classic bvh
private static Profile BuildClassicBvh()
{
var aliases = new Dictionary<BoneRole, string[]>
{
[BoneRole.Hips] = new[] { "Hips" },
[BoneRole.Spine0] = new[] { "Chest" },
[BoneRole.Spine1] = new[] { "Chest2" },
[BoneRole.Spine2] = new[] { "Chest3" },
[BoneRole.Spine3] = new[] { "Chest4" },
[BoneRole.Neck] = new[] { "Neck" },
[BoneRole.Head] = new[] { "Head" },
};
foreach (var (roleSide, nameSide) in Sides())
{
aliases[Role("Clavicle", roleSide)] = new[] { $"{nameSide}Collar" };
aliases[Role("UpperArm", roleSide)] = new[] { $"{nameSide}Shoulder" };
aliases[Role("LowerArm", roleSide)] = new[] { $"{nameSide}Elbow" };
aliases[Role("Hand", roleSide)] = new[] { $"{nameSide}Wrist" };
aliases[Role("UpperLeg", roleSide)] = new[] { $"{nameSide}Hip" };
aliases[Role("LowerLeg", roleSide)] = new[] { $"{nameSide}Knee" };
aliases[Role("Foot", roleSide)] = new[] { $"{nameSide}Ankle" };
aliases[Role("Toe", roleSide)] = new[] { $"{nameSide}Toe" };
}
return new Profile("classic_bvh", new string[0], aliases);
}
// ---------------------------------------------------------------- 3ds max biped
private static Profile BuildBiped()
{
var aliases = new Dictionary<BoneRole, string[]>
{
[BoneRole.Hips] = new[] { "Pelvis" },
[BoneRole.Spine0] = new[] { "Spine" },
[BoneRole.Spine1] = new[] { "Spine1" },
[BoneRole.Spine2] = new[] { "Spine2" },
[BoneRole.Spine3] = new[] { "Spine3" },
[BoneRole.Neck] = new[] { "Neck" },
[BoneRole.Head] = new[] { "Head" },
};
foreach (var s in new[] { "L", "R" })
{
aliases[Role("Clavicle", s)] = new[] { $"{s} Clavicle" };
aliases[Role("UpperArm", s)] = new[] { $"{s} UpperArm" };
aliases[Role("LowerArm", s)] = new[] { $"{s} Forearm" };
aliases[Role("Hand", s)] = new[] { $"{s} Hand" };
aliases[Role("UpperLeg", s)] = new[] { $"{s} Thigh" };
aliases[Role("LowerLeg", s)] = new[] { $"{s} Calf" };
aliases[Role("Foot", s)] = new[] { $"{s} Foot" };
aliases[Role("Toe", s)] = new[] { $"{s} Toe0" };
// Numbered finger chains: Finger0 is the thumb; segment names append the
// phalanx digit (Finger0 → Finger01 → Finger02, Finger1 → Finger11 → ...).
foreach (var (finger, n) in new[]
{
("Thumb", 0), ("Index", 1), ("Middle", 2), ("Ring", 3), ("Pinky", 4),
})
{
aliases[Role($"{finger}Prox", s)] = new[] { $"{s} Finger{n}" };
aliases[Role($"{finger}Mid", s)] = new[] { $"{s} Finger{n}1" };
aliases[Role($"{finger}Dist", s)] = new[] { $"{s} Finger{n}2" };
}
}
// "Bip01 "/"Bip001 " biped-name prefix; underscore form covers exporters that
// mangle the spaces ("Bip01_L_Thigh"). The bare COM root "Bip01" is untouched by
// the pattern (no trailing separator) and has no alias.
return new Profile("biped", new[] { @"^Bip\d+[ _]" }, aliases);
}
// ---------------------------------------------------------------- daz / poser
private static Profile BuildDazPoser()
{
var aliases = new Dictionary<BoneRole, string[]>
{
[BoneRole.Hips] = new[] { "hip" },
[BoneRole.Spine0] = new[] { "abdomen" },
// Poser classic / DAZ Gen4 spine is abdomen→chest; DAZ Genesis 1/2 inserts
// abdomen2. Ordered preference + used-bone exclusion handles both: without
// abdomen2 the chest falls back to Spine1 and Spine2 stays unmapped.
[BoneRole.Spine1] = new[] { "abdomen2", "chest" },
[BoneRole.Spine2] = new[] { "chest" },
[BoneRole.Neck] = new[] { "neck" },
[BoneRole.Head] = new[] { "head" },
};
foreach (var s in new[] { "L", "R" })
{
var p = s == "L" ? "l" : "r"; // lower-case side prefix: lShldr, rThigh
aliases[Role("Clavicle", s)] = new[] { $"{p}Collar" };
aliases[Role("UpperArm", s)] = new[] { $"{p}Shldr" };
aliases[Role("LowerArm", s)] = new[] { $"{p}ForeArm" };
aliases[Role("Hand", s)] = new[] { $"{p}Hand" };
aliases[Role("UpperLeg", s)] = new[] { $"{p}Thigh" };
aliases[Role("LowerLeg", s)] = new[] { $"{p}Shin" };
aliases[Role("Foot", s)] = new[] { $"{p}Foot" };
aliases[Role("Toe", s)] = new[] { $"{p}Toe" };
foreach (var (role, daz) in new[]
{
("Thumb", "Thumb"), ("Index", "Index"), ("Middle", "Mid"), ("Ring", "Ring"), ("Pinky", "Pinky"),
})
{
aliases[Role($"{role}Prox", s)] = new[] { $"{p}{daz}1" };
aliases[Role($"{role}Mid", s)] = new[] { $"{p}{daz}2" };
aliases[Role($"{role}Dist", s)] = new[] { $"{p}{daz}3" };
}
}
return new Profile("daz_poser", new string[0], aliases);
}
// ---------------------------------------------------------------- blender rigify
private static Profile BuildRigify()
{
var aliases = new Dictionary<BoneRole, string[]>
{
// rigify's "spine" bone sits AT the pelvis and parents both thighs — it is
// the hips, not a spine link (rigify/metarigs/human.py).
[BoneRole.Hips] = new[] { "spine" },
[BoneRole.Spine0] = new[] { "spine.001" },
[BoneRole.Spine1] = new[] { "spine.002" },
[BoneRole.Spine2] = new[] { "spine.003" },
// spine.004 + spine.005 are the two neck bones, spine.006 the head;
// spine.005 stays unmapped (same policy as ActorCore's NeckTwist02).
[BoneRole.Neck] = new[] { "spine.004" },
[BoneRole.Head] = new[] { "spine.006" },
};
foreach (var s in new[] { "L", "R" })
{
aliases[Role("Clavicle", s)] = new[] { $"shoulder.{s}" };
aliases[Role("UpperArm", s)] = new[] { $"upper_arm.{s}" };
aliases[Role("LowerArm", s)] = new[] { $"forearm.{s}" };
aliases[Role("Hand", s)] = new[] { $"hand.{s}" };
aliases[Role("UpperLeg", s)] = new[] { $"thigh.{s}" };
aliases[Role("LowerLeg", s)] = new[] { $"shin.{s}" };
aliases[Role("Foot", s)] = new[] { $"foot.{s}" };
aliases[Role("Toe", s)] = new[] { $"toe.{s}" };
foreach (var (role, rigify) in new[]
{
("Thumb", "thumb"), ("Index", "f_index"), ("Middle", "f_middle"),
("Ring", "f_ring"), ("Pinky", "f_pinky"),
})
{
aliases[Role($"{role}Prox", s)] = new[] { $"{rigify}.01.{s}" };
aliases[Role($"{role}Mid", s)] = new[] { $"{rigify}.02.{s}" };
aliases[Role($"{role}Dist", s)] = new[] { $"{rigify}.03.{s}" };
}
}
// The generated deform skeleton prefixes every deform bone with "DEF-"; its
// segmented limb twins ("DEF-upper_arm.L.001") keep their numeric suffix after
// stripping and therefore never collide with the whole-bone aliases.
return new Profile("rigify", new[] { "^DEF-" }, aliases);
}
// ---------------------------------------------------------------- vroid / vrm
private static Profile BuildVrm()
{
var aliases = new Dictionary<BoneRole, string[]>
{
[BoneRole.Hips] = new[] { "J_Bip_C_Hips" },
[BoneRole.Spine0] = new[] { "J_Bip_C_Spine" },
[BoneRole.Spine1] = new[] { "J_Bip_C_Chest" },
[BoneRole.Spine2] = new[] { "J_Bip_C_UpperChest" },
[BoneRole.Neck] = new[] { "J_Bip_C_Neck" },
[BoneRole.Head] = new[] { "J_Bip_C_Head" },
};
foreach (var s in new[] { "L", "R" })
{
aliases[Role("Clavicle", s)] = new[] { $"J_Bip_{s}_Shoulder" };
aliases[Role("UpperArm", s)] = new[] { $"J_Bip_{s}_UpperArm" };
aliases[Role("LowerArm", s)] = new[] { $"J_Bip_{s}_LowerArm" };
aliases[Role("Hand", s)] = new[] { $"J_Bip_{s}_Hand" };
aliases[Role("UpperLeg", s)] = new[] { $"J_Bip_{s}_UpperLeg" };
aliases[Role("LowerLeg", s)] = new[] { $"J_Bip_{s}_LowerLeg" };
aliases[Role("Foot", s)] = new[] { $"J_Bip_{s}_Foot" };
aliases[Role("Toe", s)] = new[] { $"J_Bip_{s}_ToeBase" };
foreach (var (role, vrm) in new[]
{
("Thumb", "Thumb"), ("Index", "Index"), ("Middle", "Middle"),
("Ring", "Ring"), ("Pinky", "Little"),
})
{
aliases[Role($"{role}Prox", s)] = new[] { $"J_Bip_{s}_{vrm}1" };
aliases[Role($"{role}Mid", s)] = new[] { $"J_Bip_{s}_{vrm}2" };
aliases[Role($"{role}Dist", s)] = new[] { $"J_Bip_{s}_{vrm}3" };
}
}
return new Profile("vrm", new string[0], aliases);
}
// ---------------------------------------------------------------- auto-rig pro
private static Profile BuildAutoRigPro()
{
var aliases = new Dictionary<BoneRole, string[]>
{
[BoneRole.Hips] = new[] { "root.x" },
[BoneRole.Spine0] = new[] { "spine_01.x" },
[BoneRole.Spine1] = new[] { "spine_02.x" },
[BoneRole.Spine2] = new[] { "spine_03.x" },
[BoneRole.Neck] = new[] { "neck.x" },
[BoneRole.Head] = new[] { "head.x" },
};
foreach (var s in new[] { "L", "R" })
{
var p = s == "L" ? "l" : "r";
aliases[Role("Clavicle", s)] = new[] { $"shoulder.{p}" };
aliases[Role("UpperArm", s)] = new[] { $"arm_stretch.{p}" };
aliases[Role("LowerArm", s)] = new[] { $"forearm_stretch.{p}" };
aliases[Role("Hand", s)] = new[] { $"hand.{p}" };
aliases[Role("UpperLeg", s)] = new[] { $"thigh_stretch.{p}" };
aliases[Role("LowerLeg", s)] = new[] { $"leg_stretch.{p}" };
aliases[Role("Foot", s)] = new[] { $"foot.{p}" };
aliases[Role("Toe", s)] = new[] { $"toes_01.{p}" };
// Exported finger deform bones keep ARP's c_ control prefix (Defenses.fbx).
foreach (var finger in new[] { "thumb", "index", "middle", "ring", "pinky" })
{
var role = char.ToUpperInvariant(finger[0]) + finger[1..];
aliases[Role($"{role}Prox", s)] = new[] { $"c_{finger}1.{p}" };
aliases[Role($"{role}Mid", s)] = new[] { $"c_{finger}2.{p}" };
aliases[Role($"{role}Dist", s)] = new[] { $"c_{finger}3.{p}" };
}
}
return new Profile("auto_rig_pro", new string[0], aliases);
}
// ---------------------------------------------------------------- helpers
private static IEnumerable<(string RoleSide, string NameSide)> Sides()
{
yield return ("L", "Left");
yield return ("R", "Right");
}
private static BoneRole Role(string baseName, string side)
=> System.Enum.Parse<BoneRole>(baseName + side);
}