🔍 s&box Package Code Search
Search C# source code, UI razor templates, shaders, and configs across s&box packages.
🔗 Raw Facepunch API Link: https://public.facepunch.com/sbox/code/search/1/?ident=notpointless.chomnr_humanoid_retargeter&take=20
Showing code results for query:
*
(158 total matches found)
Game
library
#nullable enable annotations
using System;
using System.Collections.Generic;
using System.Numerics;
using HumanoidRetargeter.Maths;
using SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;
namespace HumanoidRetargeter.Cleanup;
using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)
/// <summary>Tunables for the grounded-foot stance recalibration pass.</summary>
public sealed class FootGroundAlignOptions
{
/// <summary>
/// Dead zone (degrees): measured stance offsets at or below this are genuine planted
/// articulation (heel-roll bias, natural lean — measured 2–4° on well-rested rigs and
/// on citizen clips) and are left untouched, keeping the transfer byte-faithful there.
/// Only offsets beyond it are clearly rest-pose artifacts (measured 12–25° on the
/// repro rig) and get recalibrated.
/// </summary>
public float MinCorrectionDeg { get; set; } = 8f;
/// <summary>
/// Maximum mean sole deviation (degrees) a plant may show and still count as a STANCE
/// for the offset measurement. Plants beyond this are not standing on the sole (crawls,
/// kneels, prone contact — measured 60–90° there) and are excluded; genuine rest-pose
/// stance artifacts measure well below it (largest seen: 27°).
/// </summary>
public float MaxStanceDeviationDeg { get; set; } = 35f;
}
/// <summary>Per-foot results of a <see cref="FootGroundAlign.Apply"/> run.</summary>
public sealed class FootGroundAlignFootReport
{
/// <summary>Plants that contributed to the stance measurement.</summary>
public int StancePlants { get; set; }
/// <summary>Plants excluded as non-stance (mean sole deviation beyond
/// <see cref="FootGroundAlignOptions.MaxStanceDeviationDeg"/>).</summary>
public int SkippedPlants { get; set; }
/// <summary>Measured planted sole offset from the ground plane, degrees (0 when no
/// stance plants exist).</summary>
public float MeasuredOffsetDeg { get; set; }
/// <summary>Foot correction applied to every frame, degrees (0 = inside the dead zone,
/// nothing changed).</summary>
public float AppliedFootDeg { get; set; }
/// <summary>Toe correction applied to every frame, degrees.</summary>
public float AppliedToeDeg { get; set; }
}
/// <summary>Results of a <see cref="FootGroundAlign.Apply"/> run.</summary>
public sealed class FootGroundAlignReport
{
/// <summary>Left-foot results.</summary>
public required FootGroundAlignFootReport Left { get; init; }
/// <summary>Right-foot results.</summary>
public required FootGroundAlignFootReport Right { get; init; }
}
/// <summary>
/// Grounded-foot stance recalibration: measures how far the foot's SOLE sits from the ground
/// plane while planted, and — when that offset is clearly a rest-pose artifact — rotates it
/// out with one constant per foot, applied to every frame of the clip.
/// </summary>
/// <remarks>
/// <para><b>Why a cleanup pass.</b> The solver transfers feet as rest-relative deltas
/// (<see cref="Solve.RoleTransferMode.CharacterDeltaFromRest"/>), so the target keeps its own
/// ankle anatomy — correct whenever the source's rest pose is a flat-footed stance (the delta
/// is then "deviation from standing"). Some rigs ship a NON-stance rest (measured: an
/// Auto-Rig-Pro export whose rest foot sits 12–25° from its planted stance), and that constant
/// offset rides into every frame of the replay — planted feet hover toe-down/heel-up. What a
/// stance actually looks like is animation evidence (planted phases), which a per-frame
/// solver cannot see, so the recalibration lives here.</para>
/// <para><b>Measurement.</b> Per foot: over every planted frame, the sole normal = rest up
/// carried by the foot's world delta from the target bind rest (whose feet stand on the
/// ground by construction); plants whose own mean normal sits beyond
/// <see cref="FootGroundAlignOptions.MaxStanceDeviationDeg"/> are excluded (crawl/kneel/prone
/// contact is not a stance). The pooled mean normal's deviation from up is the stance
/// offset.</para>
/// <para><b>Correction.</b> Offsets inside <see cref="FootGroundAlignOptions.MinCorrectionDeg"/>
/// are genuine articulation — nothing is changed (well-rested rigs and same-rig round trips
/// stay byte-identical through this pass). Beyond it, the shortest-arc rotation taking the
/// pooled normal back to up (pitch+roll only — yaw/toe-out is pose and follows the source)
/// premultiplies the foot's world rotation on EVERY frame: a rest artifact is constant, so
/// the fix is too — within-plant heel-roll, swing styling and frame-to-frame continuity are
/// preserved exactly, and no blending is needed. The toe then receives its own residual
/// constant measured on top of the corrected foot (it neither double-rotates with the foot
/// fix nor inherits the source toe's own rest artifact). Corrections rotate bones about
/// their own joints: ankle positions are untouched, so the pass composes freely with the
/// <see cref="FootPlant"/> position pinning (which preserves foot world rotations).</para>
/// <para><b>Plant intervals come from the caller</b> (the pipeline detects them on the
/// SOURCE clip via <see cref="FootPlant.DetectPlantIntervals"/> — ground truth, immune to
/// the hip-height rescaling that can push target-side trajectories outside the cm-tuned
/// Kovar thresholds). So does the decision to run at all: the pipeline invokes this pass
/// only when the source's normalized rest is implausible as a flat stance (toe at/above
/// ankle level or asymmetric feet — see <c>Retargeter.GroundAlignFeet</c>); on plausible
/// stance rests the solver's rest-relative transfer is already faithful and planted-sole
/// deviations are genuine articulation (boxing stances, heel rolls) that must not be
/// flattened.</para>
/// </remarks>
public static class FootGroundAlign
{
/// <summary>Measures planted stance offsets and recalibrates feet whose offset is a
/// rest-pose artifact; returns what was measured and done.</summary>
/// <param name="frames">Per-frame local transforms (skeleton bone order); modified in place.</param>
/// <param name="skeleton">Bone hierarchy the frames are expressed against; its bind rest
/// is the flat-stance reference.</param>
/// <param name="left">Left leg chain bone indices.</param>
/// <param name="right">Right leg chain bone indices.</param>
/// <param name="up">World up direction of the clip's space.</param>
/// <param name="leftPlants">Left-foot plant intervals (frame indices into
/// <paramref name="frames"/>; out-of-range parts are clamped/ignored).</param>
/// <param name="rightPlants">Right-foot plant intervals.</param>
/// <param name="options">Tunables; defaults used when null.</param>
public static FootGroundAlignReport Apply(
List<XForm[]> frames,
SkeletonModel skeleton,
FootChain left,
FootChain right,
Vector3 up,
IReadOnlyList<FrameRange> leftPlants,
IReadOnlyList<FrameRange> rightPlants,
FootGroundAlignOptions? options = null)
{
ArgumentNullException.ThrowIfNull(frames);
ArgumentNullException.ThrowIfNull(skeleton);
ArgumentNullException.ThrowIfNull(left);
ArgumentNullException.ThrowIfNull(right);
ArgumentNullException.ThrowIfNull(leftPlants);
ArgumentNullException.ThrowIfNull(rightPlants);
options ??= new FootGroundAlignOptions();
var report = new FootGroundAlignReport
{
Left = new FootGroundAlignFootReport(),
Right = new FootGroundAlignFootReport(),
};
if (frames.Count == 0 || up.LengthSquared() < 1e-12f)
return report;
up = Vector3.Normalize(up);
RecalibrateFoot(frames, skeleton, left, up, leftPlants, options, report.Left);
RecalibrateFoot(frames, skeleton, right, up, rightPlants, options, report.Right);
return report;
}
private static void RecalibrateFoot(
List<XForm[]> frames, SkeletonModel skeleton, FootChain chain, Vector3 up,
IReadOnlyList<FrameRange> plants, FootGroundAlignOptions options,
FootGroundAlignFootReport report)
{
int n = frames.Count;
var foot = chain.Ankle;
var restFootRotInv = Quaternion.Conjugate(skeleton.RestWorld[foot].Rot);
var maxStanceCos = MathF.Cos(options.MaxStanceDeviationDeg * MathF.PI / 180f);
// ---- measurement: pooled planted sole normal over the stance plants ----
var pooled = Vector3.Zero;
foreach (var plant in plants)
{
int start = Math.Max(plant.Start, 0);
int end = Math.Min(plant.End, n - 1);
if (start > end)
continue;
var plantSum = Vector3.Zero;
for (int f = start; f <= end; f++)
{
var footRot = FkUtil.BoneWorld(frames[f], skeleton, foot).Rot;
plantSum += Vector3.Transform(up, MathQ.Normalize(footRot * restFootRotInv));
}
if (plantSum.LengthSquared() < 1e-8f
|| Vector3.Dot(Vector3.Normalize(plantSum), up) < maxStanceCos)
{
report.SkippedPlants++; // not standing on the sole — crawl/kneel/toe contact
continue;
}
report.StancePlants++;
pooled += plantSum; // frame-count-weighted: longer stances dominate
}
if (pooled.LengthSquared() < 1e-8f)
return;
pooled = Vector3.Normalize(pooled);
var offsetDeg = MathQ.AngleBetween(pooled, up) * (180f / MathF.PI);
report.MeasuredOffsetDeg = offsetDeg;
if (offsetDeg <= options.MinCorrectionDeg)
return; // genuine planted articulation — leave the transfer byte-faithful
// ---- correction: one constant per foot, every frame ----
var footFix = MathQ.FromTo(pooled, up);
report.AppliedFootDeg = offsetDeg;
// Toe residual measured on top of the corrected foot, same dead zone.
var toeFix = Quaternion.Identity;
if (chain.Toe is { } toe && skeleton[toe].ParentIndex == foot)
{
var restToeRotInv = Quaternion.Conjugate(skeleton.RestWorld[toe].Rot);
var toePooled = Vector3.Zero;
foreach (var plant in plants)
{
int start = Math.Max(plant.Start, 0);
int end = Math.Min(plant.End, n - 1);
for (int f = start; f <= end && f >= 0; f++)
{
var toeRot = FkUtil.BoneWorld(frames[f], skeleton, toe).Rot;
toePooled += Vector3.Transform(
up, MathQ.Normalize(footFix * toeRot * restToeRotInv));
}
}
if (toePooled.LengthSquared() > 1e-8f)
{
toePooled = Vector3.Normalize(toePooled);
var toeDeg = MathQ.AngleBetween(toePooled, up) * (180f / MathF.PI);
if (toeDeg > options.MinCorrectionDeg && Vector3.Dot(toePooled, up) >= maxStanceCos)
{
toeFix = MathQ.FromTo(toePooled, up);
report.AppliedToeDeg = toeDeg;
}
}
}
for (int f = 0; f < n; f++)
CorrectFrame(frames[f], skeleton, chain, footFix, toeFix);
}
/// <summary>Premultiplies the foot's world rotation by the constant fix (the joint
/// position is untouched — the rotation pivots the foot about its own head), then gives
/// the toe its own residual on top of the corrected foot.</summary>
private static void CorrectFrame(
XForm[] locals, SkeletonModel skeleton, FootChain chain,
Quaternion footFix, Quaternion toeFix)
{
var foot = chain.Ankle;
var parent = skeleton[foot].ParentIndex;
var parentRot = parent < 0
? Quaternion.Identity
: FkUtil.BoneWorld(locals, skeleton, parent).Rot;
var footWorld = MathQ.Normalize(parentRot * locals[foot].Rot);
var newFootWorld = MathQ.Normalize(footFix * footWorld);
locals[foot] = new XForm(
locals[foot].Pos, MathQ.Normalize(Quaternion.Conjugate(parentRot) * newFootWorld));
if (chain.Toe is { } toe && skeleton[toe].ParentIndex == foot)
{
// Desired toe world = toeFix ∘ footFix ∘ original world; re-derive its local
// against the corrected foot so it does not double-rotate with the foot fix.
var toeWorldOld = MathQ.Normalize(footWorld * locals[toe].Rot);
var desired = MathQ.Normalize(toeFix * footFix * toeWorldOld);
locals[toe] = new XForm(
locals[toe].Pos, MathQ.Normalize(Quaternion.Conjugate(newFootWorld) * desired));
}
}
}
Game
library
#nullable enable annotations
using System;
using System.Collections.Generic;
using System.Numerics;
using HumanoidRetargeter.Mapping;
using HumanoidRetargeter.Maths;
using HumanoidRetargeter.Skeleton;
using HumanoidRetargeter.Solve;
using SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;
namespace HumanoidRetargeter.Dl;
using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)
/// <summary>The z-normalization statistics shipped with the SAME checkpoint
/// (<c>ms_dict</c>): per-feature mean/std applied to every input except contact.</summary>
public sealed class SameStats
{
internal float[] LoM, LoS, GoM, GoS, QM, QS, PM, PS, RM, RS, PvM, PvS, QvM, QvS, PprevM, PprevS;
/// <summary>Reads the 16 <c>ms.*</c> arrays from a parsed weight blob.</summary>
public SameStats(SameWeights weights)
{
ArgumentNullException.ThrowIfNull(weights);
LoM = weights.Stat("lo_m"); LoS = weights.Stat("lo_s");
GoM = weights.Stat("go_m"); GoS = weights.Stat("go_s");
QM = weights.Stat("q_m"); QS = weights.Stat("q_s");
PM = weights.Stat("p_m"); PS = weights.Stat("p_s");
RM = weights.Stat("r_m"); RS = weights.Stat("r_s");
PvM = weights.Stat("pv_m"); PvS = weights.Stat("pv_s");
QvM = weights.Stat("qv_m"); QvS = weights.Stat("qv_s");
PprevM = weights.Stat("pprev_m"); PprevS = weights.Stat("pprev_s");
}
}
/// <summary>A batched per-frame source graph ready for <see cref="SameModel.Encode"/>.</summary>
public sealed class SameSourceGraph
{
/// <summary>Normalized node features, flat [FrameCount·JointCount × 32].</summary>
public required float[] X { get; init; }
/// <summary>Edge sources (bidirectional + self-loops, all frames).</summary>
public required int[] EdgeSrc { get; init; }
/// <summary>Edge destinations.</summary>
public required int[] EdgeDst { get; init; }
/// <summary>Frame id per node.</summary>
public required int[] Batch { get; init; }
/// <summary>Number of feature frames (matches the clip's frame count in production
/// mode; native frames − 2 in golden-parity mode).</summary>
public required int FrameCount { get; init; }
/// <summary>Graph joints per frame (hips subtree + end joints).</summary>
public required int JointCount { get; init; }
/// <summary>Graph node names within one frame (bone names; synthesized leaf tips get
/// a <c>_end</c> suffix). For diagnostics and parity tests.</summary>
public required string[] JointNames { get; init; }
}
/// <summary>
/// Source-side feature pipeline of the SAME port (FEASIBILITY.md "C# port work list"
/// steps 1–5): skeleton normalization, cm/Y-up/+Z-facing alignment, per-frame
/// q/p/r/pv/qv/pprev/c features in the root-facing frame, z-normalization, and the
/// bidirectional+self-loop edge list.
/// </summary>
/// <remarks>
/// <para><b>Skeleton normalization without an intermediate skeleton.</b> SAME's
/// <c>motion_normalize</c> rebuilds the rig with identity rest-local rotations and
/// re-expresses every frame against it. Algebraically the normalized motion's world
/// rotations are exactly the world-space deltas from the T-pose,
/// <c>Ĝ(j,t) = G(j,t) · G_tpose(j)⁻¹</c>, its local rotations are
/// <c>Ĝ(parent)⁻¹ · Ĝ(j)</c>, and its world positions equal the original world positions
/// — so this port computes the features directly from FK world transforms, no rebuilt
/// skeleton needed (verified against the Python pipeline by the golden-vector tests).</para>
/// <para><b>T-pose reference.</b> SAME consumes the source clip's first frame as the
/// reference; production keeps that convention but emits one feature frame per clip frame
/// (the sequence is computed over [f0, f0…fN−1] with f0 doubling as the reference — see
/// <see cref="TposeReference"/> for why the rest-pose alternative measurably loses).
/// Golden-parity mode replicates Python's frame accounting exactly (frame 0 = reference,
/// frame 1 dropped).</para>
/// <para><b>Alignment.</b> Features assume cm (guaranteed by the importers), Y-up and
/// rest facing +Z with +X to the character's left. The source is rotated by a world
/// alignment derived from the rig's rest geometry (<see cref="CharacterFrame"/> via the
/// mapping when computable, else the file's axis metadata), snapped to the nearest whole
/// axis permutation (an exact-axis rig must map to the identity — the rest-geometry tilt
/// of a few degrees otherwise leaks into every feature), and shifted so the lowest joint
/// over the clip sits on the ground plane.</para>
/// <para><b>Graph.</b> Nodes are the hips subtree (hips = mapped Hips role, else the
/// shallowest branch bone) in skeleton order — hips is always node 0, which is where the
/// root feature row lives — plus one synthesized end joint per childless leaf (BVH End
/// Sites already import as <c>_end</c> bones and are used as-is; FBX leaves get a
/// half-length continuation of their parent segment).</para>
/// </remarks>
public static class SameFeatures
{
/// <summary>How the T-pose reference (skeleton normalization + lo/go features) is chosen.</summary>
public enum TposeReference
{
/// <summary>The clip's own first frame — SAME's native convention and the
/// production default. Empirically the pretrained checkpoint tracks arms FAR
/// better against the clip's first frame than against a synthesized true T-pose,
/// even though its training references are T-poses (measured on the fixture clip:
/// mean role cosine vs the geometric solver 0.94 first-frame vs 0.57 rest-pose,
/// hands flipping negative — reproduced identically in the Python reference
/// pipeline, so it is a property of the checkpoint, not of this port).</summary>
FirstFrame,
/// <summary>Synthesize the reference from the skeleton's rest pose (the
/// FEASIBILITY suggestion; kept for experiments — see above for why it lost).</summary>
RestPose,
}
/// <summary>Options for <see cref="BuildSourceGraph"/>; defaults are production mode.</summary>
public sealed class SourceOptions
{
/// <summary>T-pose reference choice (see <see cref="TposeReference"/>).</summary>
public TposeReference Reference { get; init; } = TposeReference.FirstFrame;
/// <summary>SAME's native frame accounting: the first frame is consumed as the
/// reference and the next dropped for its undefined velocity, so the output has
/// two frames fewer than the clip. Golden-parity tests only — production emits
/// one feature frame per clip frame (the first frame doubles as the reference
/// and gets zero velocity).</summary>
public bool NativeFrameDrop { get; init; }
/// <summary>Apply the rest-geometry world alignment (Y-up, +Z facing). Disabled
/// only by golden-parity tests (Python applies none).</summary>
public bool Align { get; init; } = true;
/// <summary>Ground both the T-pose reference and the animation: the T-pose is
/// shifted so its lowest joint sits at height 0 (a BVH rest pose has its root at
/// the origin and would otherwise put the hips on the floor), and the animation is
/// shifted by its own lowest joint height over the clip (no-op for the usual
/// authored-ground-at-0 data). Disabled only by golden-parity tests (the Python
/// reference consumes data as authored).</summary>
public bool GroundShift { get; init; } = true;
}
private const float ContactHeightCm = 5f;
private const float ContactSpeedMps = 0.4f;
private const float VelocityFps = 30f;
/// <summary>
/// Builds the batched source graph for one clip: graph selection, alignment, per-frame
/// features, normalization, edges.
/// </summary>
/// <param name="scene">Imported source (cm, native axes).</param>
/// <param name="clipIndex">Clip to encode.</param>
/// <param name="map">Source mapping; used only for hips identification and the
/// rest-geometry alignment (the model itself is skeleton-agnostic). May be sparse —
/// heuristics cover missing roles.</param>
/// <param name="stats">Normalization statistics.</param>
/// <param name="options">Null = production mode.</param>
public static SameSourceGraph BuildSourceGraph(
SourceScene scene, int clipIndex, MappingResult? map, SameStats stats, SourceOptions? options = null)
{
ArgumentNullException.ThrowIfNull(scene);
ArgumentNullException.ThrowIfNull(stats);
options ??= new SourceOptions();
if (clipIndex < 0 || clipIndex >= scene.Clips.Count)
throw new ArgumentOutOfRangeException(nameof(clipIndex));
var clip = scene.Clips[clipIndex];
if (clip.FrameCount < 1)
throw new ArgumentException("Clip has no frames.", nameof(clipIndex));
if (options.NativeFrameDrop && clip.FrameCount < 3)
throw new ArgumentException("Native frame accounting needs at least 3 frames.", nameof(options));
var skeleton = scene.Skeleton;
var hips = FindHips(skeleton, map);
var nodes = GraphNodes.Build(skeleton, hips);
var align = options.Align ? ComputeAlignment(skeleton, map, scene) : Quaternion.Identity;
// T-pose reference world transforms (aligned), grounded on its own lowest joint
// (a BVH rest pose has the root at the origin — ungrounded, its hips would sit on
// the floor and every height-bearing feature would be wrong).
var tposeLocals = options.Reference == TposeReference.RestPose
? Pose.Rest(skeleton).Locals
: clip.Frames[0];
var tposeWorld = AlignedWorld(skeleton, tposeLocals, align, nodes);
if (options.GroundShift)
ShiftToGround(tposeWorld.Pos);
// The pose sequence the features run over; features are emitted for seq[1..].
var seq = new List<XForm[]>();
if (options.NativeFrameDrop)
{
for (var f = 1; f < clip.FrameCount; f++)
seq.Add(clip.Frames[f]);
}
else
{
seq.Add(clip.Frames[0]); // duplicated: gives the real first frame zero velocity
for (var f = 0; f < clip.FrameCount; f++)
seq.Add(clip.Frames[f]);
}
var frames = seq.Count - 1;
var j = nodes.Count;
// Pass 0: aligned world transforms; ground the whole clip on its lowest joint.
var worlds = new AlignedFrame[seq.Count];
for (var t = 0; t < seq.Count; t++)
worlds[t] = AlignedWorld(skeleton, seq[t], align, nodes);
if (options.GroundShift)
{
var ground = float.PositiveInfinity;
foreach (var world in worlds)
{
foreach (var p in world.Pos)
ground = MathF.Min(ground, p.Y);
}
if (float.IsFinite(ground) && ground != 0f)
{
foreach (var world in worlds)
{
for (var i = 0; i < j; i++)
world.Pos[i].Y -= ground;
}
}
}
// Pass 1: normalized-skeleton local rotations + facing per frame.
var localRots = new Quaternion[seq.Count][]; // facing-adjusted at the root row
var facing = new (float Yaw, Vector3 Pos)[seq.Count];
for (var t = 0; t < seq.Count; t++)
{
var world = worlds[t];
// Normalized-skeleton world rotations: world delta from the T-pose.
var normWorld = new Quaternion[j];
for (var i = 0; i < j; i++)
normWorld[i] = MathQ.Normalize(world.Rot[i] * Quaternion.Conjugate(tposeWorld.Rot[i]));
// Root facing: yaw (about +Y) of the normalized root rotation, at the root's
// ground-plane position.
var yaw = YawAngle(normWorld[0]);
facing[t] = (yaw, new Vector3(world.Pos[0].X, 0f, world.Pos[0].Z));
// Normalized-skeleton local rotations; root premultiplied by the inverse facing.
var locals = new Quaternion[j];
locals[0] = MathQ.Normalize(Quaternion.CreateFromAxisAngle(Vector3.UnitY, -yaw) * normWorld[0]);
for (var i = 1; i < j; i++)
{
locals[i] = MathQ.Normalize(
Quaternion.Conjugate(normWorld[nodes.Parent[i]]) * normWorld[i]);
}
localRots[t] = locals;
}
// Pass 2: feature rows.
var x = new float[frames * j * SameModel.InputDim];
for (var t = 1; t < seq.Count; t++)
{
var f = t - 1;
var (yaw, fpos) = facing[t];
var invFacing = Quaternion.CreateFromAxisAngle(Vector3.UnitY, -yaw);
var (yawPrev, fposPrev) = facing[t - 1];
var invFacingPrev = Quaternion.CreateFromAxisAngle(Vector3.UnitY, -yawPrev);
// r: facing delta (dθ, dx, dz) + absolute root height.
var dTheta = WrapPi(yaw - yawPrev);
var dPlanar = Vector3.Transform(fpos - fposPrev, invFacingPrev);
var rootHeight = worlds[t].Pos[0].Y;
for (var i = 0; i < j; i++)
{
var row = (f * j + i) * SameModel.InputDim;
var col = 0;
// ---- skel: lo, go (tiled per frame) -------------------------------------
Vector3 lo, go;
if (i == 0)
{
lo = new Vector3(0f, tposeWorld.Pos[0].Y, 0f);
go = lo;
}
else
{
lo = tposeWorld.Pos[i] - tposeWorld.Pos[nodes.Parent[i]];
go = tposeWorld.Pos[i] - new Vector3(tposeWorld.Pos[0].X, 0f, tposeWorld.Pos[0].Z);
}
WriteNorm3(x, row, ref col, lo, stats.LoM, stats.LoS);
WriteNorm3(x, row, ref col, go, stats.GoM, stats.GoS);
// ---- q ------------------------------------------------------------------
WriteNorm6(x, row, ref col, SixD(localRots[t][i]), stats.QM, stats.QS);
// ---- p (facing-frame-relative global position) --------------------------
var p = Vector3.Transform(worlds[t].Pos[i] - fpos, invFacing);
WriteNorm3(x, row, ref col, p, stats.PM, stats.PS);
// ---- r (root row only; other rows are the mean → zeros after norm) ------
if (i == 0)
{
x[row + col++] = (dTheta - stats.RM[0]) / stats.RS[0];
x[row + col++] = (dPlanar.X - stats.RM[1]) / stats.RS[1];
x[row + col++] = (dPlanar.Z - stats.RM[2]) / stats.RS[2];
x[row + col++] = (rootHeight - stats.RM[3]) / stats.RS[3];
}
else
{
col += 4; // already zero
}
// ---- pv (facing-frame velocity, ×30 fps) ---------------------------------
var pv = Vector3.Transform(worlds[t].Pos[i] - worlds[t - 1].Pos[i], invFacing) * VelocityFps;
WriteNorm3(x, row, ref col, pv, stats.PvM, stats.PvS);
// ---- qv (local rotation delta) -------------------------------------------
var qv = MathQ.Normalize(Quaternion.Conjugate(localRots[t - 1][i]) * localRots[t][i]);
WriteNorm6(x, row, ref col, SixD(qv), stats.QvM, stats.QvS);
// ---- pprev (previous position in the CURRENT facing frame) ---------------
var pprev = Vector3.Transform(worlds[t - 1].Pos[i] - fpos, invFacing);
WriteNorm3(x, row, ref col, pprev, stats.PprevM, stats.PprevS);
// ---- c (ground contact; not normalized) -----------------------------------
var speedMps = (worlds[t].Pos[i] - worlds[t - 1].Pos[i]).Length() * VelocityFps / 100f;
x[row + col] = worlds[t].Pos[i].Y < ContactHeightCm && speedMps < ContactSpeedMps ? 1f : 0f;
}
}
var (edgeSrc, edgeDst) = BuildEdges(nodes.Parent, frames);
var batch = new int[frames * j];
for (var f = 0; f < frames; f++)
{
for (var i = 0; i < j; i++)
batch[f * j + i] = f;
}
AssertFinite(x, "SAME source features");
return new SameSourceGraph
{
X = x,
EdgeSrc = edgeSrc,
EdgeDst = edgeDst,
Batch = batch,
FrameCount = frames,
JointCount = j,
JointNames = nodes.Names,
};
}
// ================================================================ graph topology
/// <summary>The per-frame graph node set: hips-subtree bones in skeleton order
/// (hips first) plus synthesized end joints for childless leaves.</summary>
internal sealed class GraphNodes
{
/// <summary>Skeleton bone index per node; -1 for synthesized end joints.</summary>
public required int[] Bone { get; init; }
/// <summary>Graph-parent node index; -1 for the root (node 0).</summary>
public required int[] Parent { get; init; }
/// <summary>For synthesized end joints: the rest-local offset from the leaf bone
/// (zero vector for real bones).</summary>
public required Vector3[] EndOffset { get; init; }
public required string[] Names { get; init; }
public int Count => Bone.Length;
public static GraphNodes Build(SkeletonModel skeleton, int hips)
{
// Hips subtree, skeleton order (parents precede children, hips first).
var inSubtree = new bool[skeleton.Count];
inSubtree[hips] = true;
var bones = new List<int> { hips };
for (var i = hips + 1; i < skeleton.Count; i++)
{
var parent = skeleton[i].ParentIndex;
if (parent >= 0 && inSubtree[parent])
{
inSubtree[i] = true;
bones.Add(i);
}
}
var nodeOfBone = new Dictionary<int, int>(bones.Count);
for (var n = 0; n < bones.Count; n++)
nodeOfBone[bones[n]] = n;
var hasChild = new bool[skeleton.Count];
foreach (var b in bones)
{
var parent = skeleton[b].ParentIndex;
if (parent >= 0 && inSubtree[parent])
hasChild[parent] = true;
}
var bone = new List<int>(bones);
var parentNode = new List<int>(bones.Count);
var endOffset = new List<Vector3>(bones.Count);
var names = new List<string>(bones.Count);
foreach (var b in bones)
{
var p = skeleton[b].ParentIndex;
parentNode.Add(b == hips ? -1 : nodeOfBone[p]);
endOffset.Add(Vector3.Zero);
names.Add(skeleton[b].Name);
}
// Synthesized end joints: leaves with no children anywhere in the skeleton.
// BVH End Sites already import as real `_end`/`_End` bones and ARE the end
// joints — no tip on a tip. The tip continues the parent→leaf segment at half
// length — a neutral stand-in for the unknown bone tail (FBX carries none).
foreach (var b in bones)
{
if (hasChild[b]
|| skeleton[b].Name.EndsWith("_end", StringComparison.OrdinalIgnoreCase))
continue;
var p = skeleton[b].ParentIndex;
var segment = p >= 0
? skeleton.RestWorld[b].Pos - skeleton.RestWorld[p].Pos
: Vector3.Zero;
var tip = segment.Length() > 1e-4f ? segment * 0.5f : new Vector3(0f, 2f, 0f);
// Express in the leaf's rest-local frame (applied via the leaf's world rot).
var local = Vector3.Transform(tip, Quaternion.Conjugate(skeleton.RestWorld[b].Rot));
bone.Add(-1);
parentNode.Add(nodeOfBone[b]);
endOffset.Add(local);
names.Add(skeleton[b].Name + "_end");
}
return new GraphNodes
{
Bone = bone.ToArray(),
Parent = parentNode.ToArray(),
EndOffset = endOffset.ToArray(),
Names = names.ToArray(),
};
}
}
/// <summary>Aligned world transforms of the graph nodes for one pose.</summary>
internal readonly struct AlignedFrame
{
public required Vector3[] Pos { get; init; }
public required Quaternion[] Rot { get; init; }
}
private static AlignedFrame AlignedWorld(
SkeletonModel skeleton, XForm[] locals, Quaternion align, GraphNodes nodes)
{
var world = new Pose(locals).ToWorld(skeleton);
var pos = new Vector3[nodes.Count];
var rot = new Quaternion[nodes.Count];
for (var n = 0; n < nodes.Count; n++)
{
XForm w;
if (nodes.Bone[n] >= 0)
{
w = world[nodes.Bone[n]];
}
else
{
// Synthesized end joint: rides its leaf bone (identity local rotation).
var leaf = world[nodes.Bone[nodes.Parent[n]]];
w = new XForm(leaf.TransformPoint(nodes.EndOffset[n]), leaf.Rot);
}
pos[n] = Vector3.Transform(w.Pos, align);
rot[n] = MathQ.Normalize(align * w.Rot);
}
return new AlignedFrame { Pos = pos, Rot = rot };
}
/// <summary>Bidirectional parent↔child pairs plus one self-loop per node, replicated
/// per frame with node indices offset.</summary>
internal static (int[] Src, int[] Dst) BuildEdges(int[] parent, int frames)
{
var j = parent.Length;
var nonRoot = 0;
for (var i = 0; i < j; i++)
{
if (parent[i] >= 0)
nonRoot++;
}
var perFrame = nonRoot * 2 + j;
var src = new int[perFrame * frames];
var dst = new int[perFrame * frames];
var e = 0;
for (var f = 0; f < frames; f++)
{
var offset = f * j;
for (var i = 0; i < j; i++)
{
if (parent[i] < 0)
continue;
src[e] = offset + parent[i];
dst[e] = offset + i;
e++;
src[e] = offset + i;
dst[e] = offset + parent[i];
e++;
}
for (var i = 0; i < j; i++)
{
src[e] = offset + i;
dst[e] = offset + i;
e++;
}
}
return (src, dst);
}
// ================================================================ alignment + hips
/// <summary>Mapped Hips role when available, else the shallowest bone with two or more
/// children (the hips of any humanoid: the legs/spine branch point).</summary>
internal static int FindHips(SkeletonModel skeleton, MappingResult? map)
{
if (map is not null && map.RoleToBone.TryGetValue(BoneRole.Hips, out var mapped)
&& mapped >= 0 && mapped < skeleton.Count)
return mapped;
var childCount = new int[skeleton.Count];
for (var i = 0; i < skeleton.Count; i++)
{
if (skeleton[i].ParentIndex >= 0)
childCount[skeleton[i].ParentIndex]++;
}
var best = -1;
var bestDepth = int.MaxValue;
for (var i = 0; i < skeleton.Count; i++)
{
if (childCount[i] < 2)
continue;
var depth = 0;
for (var a = skeleton[i].ParentIndex; a >= 0; a = skeleton[a].ParentIndex)
depth++;
if (depth < bestDepth)
{
best = i;
bestDepth = depth;
}
}
return best >= 0 ? best : 0;
}
/// <summary>
/// World rotation taking the rig into the canonical SAME frame (X = character left,
/// Y = up, Z = facing): rest-geometry character frame when computable from the mapping,
/// else the file's recorded axis conventions.
/// </summary>
internal static Quaternion ComputeAlignment(SkeletonModel skeleton, MappingResult? map, SourceScene? scene)
{
if (map is not null)
{
try
{
var frame = CharacterFrame.Compute(skeleton, map, skeleton.RestWorld);
return AlignFromBasis(frame.Lateral, frame.Up, frame.Forward);
}
catch (ArgumentException)
{
// fall through to axis metadata
}
}
if (scene is not null)
{
var up = AxisVector(scene.UpAxis, scene.UpAxisSign);
var forward = AxisVector(scene.FrontAxis, scene.FrontAxisSign);
if (MathF.Abs(Vector3.Dot(up, forward)) < 0.5f)
return AlignFromBasis(Vector3.Cross(up, forward), up, forward);
}
return Quaternion.Identity;
}
/// <summary>
/// Rotation mapping the given (left, up, forward) world directions onto (+X, +Y, +Z),
/// snapped to the nearest whole axis permutation when one is unambiguous: rigs authored
/// on exact axes (BVH Y-up/+Z, the s&box rig, Z-up FBX) must map by an exact
/// quarter-turn — the few degrees of rest-geometry tilt (shoulders not exactly above
/// hips) otherwise leak into every feature and measurably cost accuracy.
/// </summary>
internal static Quaternion AlignFromBasis(Vector3 left, Vector3 up, Vector3 forward)
{
var l = SnapAxis(left);
var u = SnapAxis(up);
var f = SnapAxis(forward);
if (MathF.Abs(Vector3.Dot(l, u)) > 0.5f || MathF.Abs(Vector3.Dot(l, f)) > 0.5f
|| MathF.Abs(Vector3.Dot(u, f)) > 0.5f)
{
// Genuinely oblique rig: keep the exact (orthonormalized) directions.
l = Vector3.Normalize(left);
u = Vector3.Normalize(up - l * Vector3.Dot(up, l));
f = Vector3.Cross(l, u);
}
// Row-major with rows = basis images maps +X→left, +Y→up, +Z→forward
// (System.Numerics row-vector convention); the alignment is its inverse.
var m = new Matrix4x4(
l.X, l.Y, l.Z, 0f,
u.X, u.Y, u.Z, 0f,
f.X, f.Y, f.Z, 0f,
0f, 0f, 0f, 1f);
return Quaternion.Conjugate(MathQ.Normalize(Quaternion.CreateFromRotationMatrix(m)));
}
private static Vector3 SnapAxis(Vector3 v)
{
var ax = MathF.Abs(v.X);
var ay = MathF.Abs(v.Y);
var az = MathF.Abs(v.Z);
if (ax >= ay && ax >= az)
return new Vector3(MathF.Sign(v.X), 0f, 0f);
if (ay >= az)
return new Vector3(0f, MathF.Sign(v.Y), 0f);
return new Vector3(0f, 0f, MathF.Sign(v.Z));
}
private static Vector3 AxisVector(int axis, int sign) => axis switch
{
0 => new Vector3(sign, 0f, 0f),
2 => new Vector3(0f, 0f, sign),
_ => new Vector3(0f, sign, 0f),
};
// ================================================================ small math
/// <summary>The yaw (rotation about +Y) closest to <paramref name="q"/> — fairmotion's
/// <c>Q_closest(q, identity, +Y)</c>, reproduced exactly for parity.</summary>
internal static float YawAngle(Quaternion q)
{
var alpha = Math.Atan2(q.W, q.Y);
var theta1 = -2.0 * alpha + Math.PI;
var theta2 = -2.0 * alpha - Math.PI;
var d1 = q.Y * Math.Sin(theta1 * 0.5) + q.W * Math.Cos(theta1 * 0.5);
var d2 = q.Y * Math.Sin(theta2 * 0.5) + q.W * Math.Cos(theta2 * 0.5);
return (float)(d1 > d2 ? theta1 : theta2);
}
private static void ShiftToGround(Vector3[] positions)
{
var ground = float.PositiveInfinity;
foreach (var p in positions)
ground = MathF.Min(ground, p.Y);
if (!float.IsFinite(ground) || ground == 0f)
return;
for (var i = 0; i < positions.Length; i++)
positions[i].Y -= ground;
}
internal static float WrapPi(float angle)
{
while (angle > MathF.PI)
angle -= 2f * MathF.PI;
while (angle < -MathF.PI)
angle += 2f * MathF.PI;
return angle;
}
/// <summary>6D rotation representation: the first two columns of the rotation matrix
/// (<c>R·e_x</c> then <c>R·e_y</c>).</summary>
internal static (Vector3 C0, Vector3 C1) SixD(Quaternion q)
=> (Vector3.Transform(Vector3.UnitX, q), Vector3.Transform(Vector3.UnitY, q));
private static void WriteNorm3(float[] x, int row, ref int col, Vector3 v, float[] m, float[] s)
{
x[row + col++] = (v.X - m[0]) / s[0];
x[row + col++] = (v.Y - m[1]) / s[1];
x[row + col++] = (v.Z - m[2]) / s[2];
}
private static void WriteNorm6(float[] x, int row, ref int col, (Vector3 C0, Vector3 C1) sixD, float[] m, float[] s)
{
x[row + col++] = (sixD.C0.X - m[0]) / s[0];
x[row + col++] = (sixD.C0.Y - m[1]) / s[1];
x[row + col++] = (sixD.C0.Z - m[2]) / s[2];
x[row + col++] = (sixD.C1.X - m[3]) / s[3];
x[row + col++] = (sixD.C1.Y - m[4]) / s[4];
x[row + col++] = (sixD.C1.Z - m[5]) / s[5];
}
internal static void AssertFinite(float[] values, string what)
{
foreach (var v in values)
{
if (!float.IsFinite(v))
throw new InvalidOperationException($"{what} contain non-finite values.");
}
}
}
Game
library
#nullable enable annotations
using System;
using System.Buffers.Binary;
using System.IO;
using System.Text;
namespace HumanoidRetargeter.Formats.Fbx;
/// <summary>
/// Serializes an <see cref="FbxNode"/> tree back to binary FBX (version 7400 layout —
/// u32 header fields, universally readable). The inverse of
/// <see cref="FbxTokenizer.Parse"/>: a tree parsed from a 7.x binary file and written
/// here re-parses to an identical tree (arrays are written uncompressed; zlib-encoded
/// inputs therefore round-trip by VALUE, not byte-for-byte).
/// </summary>
/// <remarks>
/// Used by <see cref="FbxBindPoseFixer"/> to persist repaired node transforms. The footer
/// is written the way Blender's exporter does: a fixed 16-byte watermark (importers treat
/// it as opaque), zero padding to a 16-byte boundary, the version echo, 120 zero bytes and
/// the closing magic. The FBX SDK computes a content hash here, but every consumer we
/// target (s&box, Blender, assimp) ignores it.
/// </remarks>
public static class FbxBinaryWriter
{
private const uint Version = 7400;
private static readonly byte[] HeaderMagic =
"Kaydara FBX Binary \0\x1a\0"u8.ToArray();
// Blender's fbx_binary.py FOOT_ID + closing magic bytes.
private static readonly byte[] FooterWatermark =
{
0xfa, 0xbc, 0xab, 0x09, 0xd0, 0xc8, 0xd4, 0x66, 0xb1, 0x76, 0xfb, 0x83, 0x1c, 0xf7, 0x26, 0x7e,
};
private static readonly byte[] FooterMagic =
{
0xf8, 0x5a, 0x8c, 0x6a, 0xde, 0xf5, 0xd9, 0x7e, 0xec, 0xe9, 0x0c, 0x6e, 0x0c, 0xc0, 0x00, 0x00,
};
/// <summary>
/// Serializes <paramref name="root"/> (a virtual root whose children are the top-level
/// document nodes, as produced by <see cref="FbxTokenizer.Parse"/>).
/// </summary>
public static byte[] Write(FbxNode root)
{
ArgumentNullException.ThrowIfNull(root);
using var ms = new MemoryStream();
ms.Write(HeaderMagic);
WriteU32(ms, Version);
foreach (var child in root.Children)
WriteNode(ms, child);
WriteNullRecord(ms);
WriteFooter(ms);
return ms.ToArray();
}
// ------------------------------------------------------------------ nodes
private static void WriteNode(MemoryStream ms, FbxNode node)
{
long headerAt = ms.Position;
// Placeholder header: endOffset, numProps, propListLen (patched after the body).
WriteU32(ms, 0);
WriteU32(ms, (uint)node.Properties.Count);
WriteU32(ms, 0);
var nameBytes = Encoding.ASCII.GetBytes(node.Name);
if (nameBytes.Length > byte.MaxValue)
throw new FormatException($"FBX write: node name too long ({node.Name.Length} chars).");
ms.WriteByte((byte)nameBytes.Length);
ms.Write(nameBytes);
long propsAt = ms.Position;
foreach (var p in node.Properties)
WriteProperty(ms, p, node.Name);
long propsLen = ms.Position - propsAt;
if (node.Children.Count > 0)
{
foreach (var child in node.Children)
WriteNode(ms, child);
WriteNullRecord(ms);
}
long endAt = ms.Position;
ms.Position = headerAt;
WriteU32(ms, checked((uint)endAt));
WriteU32(ms, (uint)node.Properties.Count);
WriteU32(ms, checked((uint)propsLen));
ms.Position = endAt;
}
private static void WriteNullRecord(MemoryStream ms)
{
Span<byte> zeros = stackalloc byte[13];
zeros.Clear();
ms.Write(zeros);
}
// ------------------------------------------------------------------ properties
private static void WriteProperty(MemoryStream ms, object value, string owner)
{
switch (value)
{
case short y:
ms.WriteByte((byte)'Y');
WriteI16(ms, y);
break;
case bool c:
ms.WriteByte((byte)'C');
ms.WriteByte(c ? (byte)1 : (byte)0);
break;
case int i:
ms.WriteByte((byte)'I');
WriteI32(ms, i);
break;
case float f:
ms.WriteByte((byte)'F');
WriteF32(ms, f);
break;
case double d:
ms.WriteByte((byte)'D');
WriteF64(ms, d);
break;
case long l:
ms.WriteByte((byte)'L');
WriteI64(ms, l);
break;
case float[] fa:
WriteArrayHeader(ms, 'f', fa.Length, 4);
foreach (var x in fa)
WriteF32(ms, x);
break;
case double[] da:
WriteArrayHeader(ms, 'd', da.Length, 8);
foreach (var x in da)
WriteF64(ms, x);
break;
case long[] la:
WriteArrayHeader(ms, 'l', la.Length, 8);
foreach (var x in la)
WriteI64(ms, x);
break;
case int[] ia:
WriteArrayHeader(ms, 'i', ia.Length, 4);
foreach (var x in ia)
WriteI32(ms, x);
break;
case bool[] ba:
WriteArrayHeader(ms, 'b', ba.Length, 1);
foreach (var x in ba)
ms.WriteByte(x ? (byte)1 : (byte)0);
break;
case string s:
{
ms.WriteByte((byte)'S');
var bytes = Encoding.UTF8.GetBytes(s);
WriteU32(ms, (uint)bytes.Length);
ms.Write(bytes);
break;
}
case byte[] r:
ms.WriteByte((byte)'R');
WriteU32(ms, (uint)r.Length);
ms.Write(r);
break;
default:
throw new FormatException(
$"FBX write: node '{owner}': unsupported property CLR type {value?.GetType().Name ?? "null"}.");
}
}
private static void WriteArrayHeader(MemoryStream ms, char code, int count, int elemSize)
{
ms.WriteByte((byte)code);
WriteU32(ms, (uint)count);
WriteU32(ms, 0); // encoding 0 = uncompressed
WriteU32(ms, checked((uint)(count * elemSize)));
}
// ------------------------------------------------------------------ footer
private static void WriteFooter(MemoryStream ms)
{
ms.Write(FooterWatermark);
// Zero-pad so the version echo starts 16-aligned (Blender pads at least 1 byte).
int pad = (int)(16 - ms.Position % 16);
for (int i = 0; i < pad; i++)
ms.WriteByte(0);
WriteU32(ms, Version);
Span<byte> zeros = stackalloc byte[120];
zeros.Clear();
ms.Write(zeros);
ms.Write(FooterMagic);
}
// ------------------------------------------------------------------ primitives
private static void WriteU32(MemoryStream ms, uint v)
{
Span<byte> b = stackalloc byte[4];
BinaryPrimitives.WriteUInt32LittleEndian(b, v);
ms.Write(b);
}
private static void WriteI16(MemoryStream ms, short v)
{
Span<byte> b = stackalloc byte[2];
BinaryPrimitives.WriteInt16LittleEndian(b, v);
ms.Write(b);
}
private static void WriteI32(MemoryStream ms, int v)
{
Span<byte> b = stackalloc byte[4];
BinaryPrimitives.WriteInt32LittleEndian(b, v);
ms.Write(b);
}
private static void WriteI64(MemoryStream ms, long v)
{
Span<byte> b = stackalloc byte[8];
BinaryPrimitives.WriteInt64LittleEndian(b, v);
ms.Write(b);
}
private static void WriteF32(MemoryStream ms, float v)
{
Span<byte> b = stackalloc byte[4];
BinaryPrimitives.WriteSingleLittleEndian(b, v);
ms.Write(b);
}
private static void WriteF64(MemoryStream ms, double v)
{
Span<byte> b = stackalloc byte[8];
BinaryPrimitives.WriteDoubleLittleEndian(b, v);
ms.Write(b);
}
}
Game
library
#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);
}
Game
library
#nullable enable annotations
using System;
using System.Collections.Generic;
using System.Numerics;
using HumanoidRetargeter.Mapping;
using HumanoidRetargeter.Maths;
using HumanoidRetargeter.Target;
namespace HumanoidRetargeter.Solve;
using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)
/// <summary>
/// Mirrors a solved TARGET-space clip across the target character's sagittal plane,
/// producing the left/right-swapped twin of an animation (e.g. a right-foot-lead walk from a
/// left-foot-lead one).
/// </summary>
/// <remarks>
/// <para><b>Mirror plane.</b> The plane through the rig-space origin spanned by the target
/// character's up and forward directions; its normal is the character's LATERAL axis,
/// computed from the target rig's rest geometry via <see cref="CharacterFrame"/> (never
/// hardcoded — an arbitrary target may be authored in any axis convention). When the
/// computed lateral lies on a coordinate axis up to float dirt (< 1e-3 on the other two
/// components — true for every axis-aligned authored rig, including the s&box citizen
/// rigs), it is snapped to that exact axis, which makes every reflection below an EXACT
/// sign-flip in IEEE arithmetic and therefore the whole mirror a bit-exact involution
/// (mirror ∘ mirror == identity, verified by test).</para>
/// <para><b>Math.</b> Let M = I − 2n̂n̂ᵀ be the reflection across the plane with unit normal
/// n̂. A world transform W = (R, t) maps to its mirror image by conjugation:
/// W′ = M̂ ∘ W ∘ M̂ (M̂ is its own inverse), giving rotation R′ = M·R·M and translation
/// t′ = M·t. For a quaternion q = (v, w), M·R·M is the rotation by the SAME angle about the
/// REFLECTED axis with REVERSED sense (a reflection flips orientation), i.e.
/// q′ = (2(n̂·v)n̂ − v, w); with n̂ = +X that is exactly q′ = (x, −y, −z, w), and positions
/// reflect as p′ = p − 2(n̂·p)n̂ = (−pₓ, p_y, p_z).</para>
/// <para><b>Locals, not worlds.</b> Because conjugation is a homomorphism
/// (M̂(AB)M̂ = (M̂AM̂)(M̂BM̂)) and world transforms are products of locals down the
/// hierarchy, mirroring every LOCAL transform and permuting bones by their L↔R partner is
/// exactly equivalent to mirroring the FK worlds — provided the partner permutation is
/// hierarchy-consistent (the partner's parent is the parent's partner), which is validated
/// and holds on structurally symmetric humanoid rigs. This avoids FK→inverse-FK float drift
/// entirely, which is what makes the double-mirror identity bit-exact.</para>
/// <para><b>Pairing.</b> Left/right bones are paired by the rig's canonical role annotations
/// first (UpperArmL ↔ UpperArmR, …); role-less bones (twist helpers, IK bones) fall back to
/// <c>_L</c>/<c>_R</c> name-token pairing (<c>arm_upper_L_twist0</c> ↔
/// <c>arm_upper_R_twist0</c>, <c>foot_L_IK_target</c> ↔ <c>foot_R_IK_target</c>); anything
/// unpaired (center bones: pelvis, spine, neck, head) mirrors in place, which reflects its
/// rotation across the sagittal plane and negates its lateral translation. IK-baked helper
/// bones are NOT re-baked after mirroring: conjugation is a homomorphism, so the mirrored
/// copies of the primary clip's final helper channels already hang in exactly the mirrored
/// relationship over the mirrored body (re-baking encoded a divergent convention, the Gate 3
/// review's mechanism 2 of the _M render defect; southpaw project, gate3_review.md 3.4).
/// Channel exclusions on mirrored clips go through <see cref="MirrorSafeExclusions"/>.</para>
/// </remarks>
public static class ClipMirror
{
/// <summary>Maximum off-axis component magnitude below which the computed lateral axis is
/// snapped to the exact coordinate axis (authored rigs are axis-aligned; the tiny rest
/// asymmetries of a real mesh stay far below this).</summary>
private const float AxisSnapTolerance = 1e-3f;
/// <summary>
/// Returns the mirrored copy of <paramref name="frames"/> (one new list, inputs
/// untouched): per frame, bone i takes the conjugated local transform of its L↔R partner
/// σ(i). See the class remarks for the math and pairing rules.
/// </summary>
/// <param name="frames">Solved per-frame local transforms (target skeleton bone order).</param>
/// <param name="rig">The target rig (skeleton + roles) the frames belong to.</param>
/// <exception cref="ArgumentException">Thrown when the rig maps a sided role without its
/// counterpart, the pairing is not hierarchy-consistent, or the character frame is not
/// computable — mirroring would silently produce garbage in those cases.</exception>
public static List<XForm[]> Mirror(List<XForm[]> frames, TargetRig rig)
{
ArgumentNullException.ThrowIfNull(frames);
ArgumentNullException.ThrowIfNull(rig);
var skeleton = rig.Skeleton;
var lateral = LateralAxis(rig);
var pair = BuildPairing(rig);
var fkFix = HierarchyInconsistentBones(rig, pair);
var result = new List<XForm[]>(frames.Count);
var baseWorld = fkFix.Count > 0 ? new XForm[skeleton.Count] : null;
var mirrorWorld = fkFix.Count > 0 ? new XForm[skeleton.Count] : null;
foreach (var locals in frames)
{
if (locals.Length != skeleton.Count)
throw new ArgumentException(
$"Frame has {locals.Length} bones but the target skeleton has {skeleton.Count}.",
nameof(frames));
var mirrored = new XForm[locals.Length];
for (var i = 0; i < locals.Length; i++)
{
var source = locals[pair[i]];
mirrored[i] = new XForm(
ReflectPoint(source.Pos, lateral),
ReflectRotation(source.Rot, lateral));
}
// Hierarchy-inconsistent pairs (the citizen parents arm_elbow_helper_R under
// arm_lower_R_twist0 while _L hangs under arm_lower_L, the W3a-documented
// rig quirk): the partner's conjugated LOCAL under a non-mirrored parent
// chain misplaces the bone by the parent-chain difference (measured ~1 in on
// the elbow/knee helpers). Solve their locals by FK so the mirrored WORLD is
// the exact reflection of the partner's world (southpaw G8 mirror fix).
if (fkFix.Count > 0)
{
for (var i = 0; i < skeleton.Count; i++)
{
var parent = skeleton[i].ParentIndex;
baseWorld![i] = parent < 0
? locals[i]
: XForm.Compose(baseWorld[parent], locals[i]);
}
for (var i = 0; i < skeleton.Count; i++)
{
var parent = skeleton[i].ParentIndex;
if (fkFix.Contains(i))
{
var desired = new XForm(
ReflectPoint(baseWorld![pair[i]].Pos, lateral),
ReflectRotation(baseWorld[pair[i]].Rot, lateral));
mirrored[i] = parent < 0
? desired
: XForm.ToLocal(mirrorWorld![parent], desired);
}
mirrorWorld![i] = parent < 0
? mirrored[i]
: XForm.Compose(mirrorWorld[parent], mirrored[i]);
}
}
result.Add(mirrored);
}
return result;
}
/// <summary>
/// Bones whose L/R pairing is NOT hierarchy-consistent (the partner hangs under a
/// non-mirrored parent). Only constraint-driven helpers can reach this state
/// (<see cref="BuildPairing"/> fails hard for any other bone); their mirrored locals
/// need the FK solve in <see cref="Mirror"/> and their DMX channels must be written
/// (<see cref="MirrorSafeExclusions"/>).
/// </summary>
private static HashSet<int> HierarchyInconsistentBones(TargetRig rig, int[] pair)
{
var skeleton = rig.Skeleton;
var result = new HashSet<int>();
for (var i = 0; i < pair.Length; i++)
{
var parent = skeleton[i].ParentIndex;
var partnerParent = skeleton[pair[i]].ParentIndex;
var consistent = parent < 0
? partnerParent < 0
: partnerParent == pair[parent];
if (!consistent)
result.Add(i);
}
return result;
}
/// <summary>
/// Filters a channel-exclusion set for a MIRRORED clip: returns the subset of
/// <paramref name="excluded"/> that is still safe to leave channel-less after mirroring.
/// A channel-less bone is rendered/baked at its own REST local under its (mirrored)
/// parent; the mirrored frames instead carry the conjugated rest local of the bone's
/// L/R partner. Those agree only when the rig's rest locals are mirror conjugates
/// (restLocal(i) == conjugate(restLocal(partner(i)))). Bones breaking that symmetry
/// (measured on the citizen rig: leg/arm *_twist1 chains and neck_clothing, 19 to 37 cm
/// off) MUST keep explicit mirrored channels or every data consumer (model compiler
/// sequence bake, render-side helper evaluation) places them wrong: the Gate 3 review's
/// MECHANISM 1 of the _M render defect (southpaw project, gate3_review.md 3.4).
/// Truly symmetric helpers stay excluded exactly as on primary clips.
/// </summary>
/// <param name="rig">The target rig the exclusion set belongs to.</param>
/// <param name="excluded">The primary-clip exclusion set (constraint-driven bones).</param>
/// <returns>The mirror-safe subset, or null when nothing remains excluded.</returns>
public static IReadOnlySet<int>? MirrorSafeExclusions(TargetRig rig, IReadOnlySet<int>? excluded)
{
if (excluded is null || excluded.Count == 0)
return excluded;
var skeleton = rig.Skeleton;
var lateral = LateralAxis(rig);
var pair = BuildPairing(rig);
var inconsistent = HierarchyInconsistentBones(rig, pair);
const float posTolCm = 0.1f;
const float rotTolDeg = 0.5f;
var cosTol = MathF.Cos(rotTolDeg * MathF.PI / 360f); // half-angle for quat dot
var safe = new HashSet<int>();
foreach (var i in excluded)
{
// Hierarchy-inconsistent pairs always need explicit channels: their mirrored
// locals are FK-solved (see Mirror) and no rest local can stand in for them.
if (inconsistent.Contains(i))
continue;
var own = skeleton[i].RestLocal;
var partnerRest = skeleton[pair[i]].RestLocal;
var needed = new XForm(
ReflectPoint(partnerRest.Pos, lateral),
ReflectRotation(partnerRest.Rot, lateral));
var posOk = (own.Pos - needed.Pos).Length() <= posTolCm;
var dot = MathF.Abs(
own.Rot.X * needed.Rot.X + own.Rot.Y * needed.Rot.Y
+ own.Rot.Z * needed.Rot.Z + own.Rot.W * needed.Rot.W);
var rotOk = dot >= cosTol;
if (posOk && rotOk)
safe.Add(i);
}
return safe.Count > 0 ? safe : null;
}
// ================================================================ mirror plane
/// <summary>The unit mirror normal: the target character's lateral axis from rest
/// geometry, snapped to an exact coordinate axis when within tolerance (bit-exact
/// reflections, see class remarks).</summary>
private static Vector3 LateralAxis(TargetRig rig)
{
Vector3 lateral;
try
{
lateral = CharacterFrame.Compute(
rig.Skeleton, rig.ToMappingResult(), rig.Skeleton.RestWorld).Lateral;
}
catch (ArgumentException e)
{
throw new ArgumentException(
$"Cannot mirror: target character frame not computable ({e.Message}).", e);
}
var a = Vector3.Abs(lateral);
if (a.Y <= AxisSnapTolerance && a.Z <= AxisSnapTolerance)
return Vector3.UnitX;
if (a.X <= AxisSnapTolerance && a.Z <= AxisSnapTolerance)
return Vector3.UnitY;
if (a.X <= AxisSnapTolerance && a.Y <= AxisSnapTolerance)
return Vector3.UnitZ;
return lateral; // general (non-axis-aligned) rig: exact involution is lost, math is not
}
/// <summary>p′ = p − 2(n̂·p)n̂. With a snapped axis this is an exact sign flip of one
/// component (IEEE subtraction of representable values is exact).</summary>
private static Vector3 ReflectPoint(Vector3 p, Vector3 n)
=> p - 2f * Vector3.Dot(p, n) * n;
/// <summary>q′ = (2(n̂·v)n̂ − v, w): the conjugated rotation M·R·M — same angle, axis
/// reflected, sense reversed. With n̂ = +X this is (x, −y, −z, w). Components are
/// preserved exactly (no renormalization), keeping the double mirror bit-exact.</summary>
private static Quaternion ReflectRotation(Quaternion q, Vector3 n)
{
var v = new Vector3(q.X, q.Y, q.Z);
var reflected = 2f * Vector3.Dot(v, n) * n - v;
return new Quaternion(reflected.X, reflected.Y, reflected.Z, q.W);
}
// ================================================================ L↔R pairing
/// <summary>
/// σ: bone → mirror partner (identity for center/unpaired bones). Roles pair first;
/// role-less bones pair by <c>_L</c>/<c>_R</c> name tokens. Validated to be an involution
/// consistent with the hierarchy (σ(parent(i)) == parent(σ(i))).
/// </summary>
private static int[] BuildPairing(TargetRig rig)
{
var skeleton = rig.Skeleton;
var pair = new int[skeleton.Count];
for (var i = 0; i < pair.Length; i++)
pair[i] = i;
for (var i = 0; i < skeleton.Count; i++)
{
if (rig.RoleOf(i) is { } role)
{
if (MirrorRole(role) is not { } mirroredRole)
continue; // center role: mirrors in place
pair[i] = rig.BoneForRole(mirroredRole)
?? throw new ArgumentException(
$"Cannot mirror: target rig maps role {role} ('{skeleton[i].Name}') "
+ $"but not its counterpart {mirroredRole}.");
}
else
{
var partnerName = SwapSideTokens(skeleton[i].Name);
if (partnerName is null)
continue; // no side token: center bone
var partner = skeleton.IndexOf(partnerName);
if (partner >= 0)
pair[i] = partner;
// No partner bone: leave in place (e.g. an asymmetric prop bone) — its
// rotation still mirrors across the sagittal plane.
}
}
for (var i = 0; i < pair.Length; i++)
{
// Constraint-driven helper bones are excluded from the output DMX (the model's
// AnimConstraintList re-drives them at runtime, see Retargeter.EmitClip
// ChannelExcludedBones), so their mirrored channels are never written. The shipped
// s&box citizen rig parents these asymmetrically (arm_elbow_helper_R hangs under
// arm_lower_R_twist0 while arm_elbow_helper_L hangs under arm_lower_L), a benign
// data quirk that must not fail the whole mirror. Skip the strict L/R
// hierarchy-consistency requirement for them: their pairing does not affect any
// written channel. (W3a fix, southpaw project.)
if (rig.HelpersAreConstraintDriven && rig.ClassOf(i) == BoneClass.ConstraintDriven)
continue;
if (pair[pair[i]] != i)
throw new ArgumentException(
$"Cannot mirror: bone pairing is not symmetric ('{skeleton[i].Name}' → "
+ $"'{skeleton[pair[i]].Name}' → '{skeleton[pair[pair[i]]].Name}').");
var parent = skeleton[i].ParentIndex;
var partnerParent = skeleton[pair[i]].ParentIndex;
var consistent = parent < 0
? partnerParent < 0
: partnerParent == pair[parent];
if (!consistent)
throw new ArgumentException(
$"Cannot mirror: left/right pairing is not hierarchy-consistent — "
+ $"'{skeleton[i].Name}' and partner '{skeleton[pair[i]].Name}' hang under "
+ "non-mirrored parents.");
}
return pair;
}
/// <summary>UpperArmL → UpperArmR (and back); null for center roles. Every sided
/// <see cref="BoneRole"/> ends in <c>L</c>/<c>R</c>; no center role does.</summary>
private static BoneRole? MirrorRole(BoneRole role)
{
var name = role.ToString();
var mirroredName = name[^1] switch
{
'L' => name[..^1] + "R",
'R' => name[..^1] + "L",
_ => null,
};
return mirroredName is not null && Enum.TryParse<BoneRole>(mirroredName, out var mirrored)
? mirrored
: null;
}
/// <summary>Swaps <c>L</c>/<c>R</c> underscore-delimited name tokens
/// (<c>foot_L_IK_target</c> → <c>foot_R_IK_target</c>); null when the name carries no
/// side token.</summary>
private static string? SwapSideTokens(string name)
{
var tokens = name.Split('_');
for (var i = 0; i < tokens.Length; i++)
{
tokens[i] = tokens[i] switch
{
"L" => "R",
"R" => "L",
"l" => "r",
"r" => "l",
_ => tokens[i],
};
}
var result = string.Join('_', tokens);
return string.Equals(result, name, StringComparison.Ordinal) ? null : result;
}
}
Game
library
#nullable enable annotations
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using HumanoidRetargeter.Mapping;
using HumanoidRetargeter.Maths;
namespace HumanoidRetargeter.Solve;
using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)
/// <summary>
/// Finger retargeting. Picks one of three strategies per finger chain:
/// <list type="number">
/// <item><b>1:1 absolute copy</b> (via the <c>transferOneToOne</c> callback into
/// <see cref="GeometricSolver"/>'s body path) when the source and target chains are
/// <i>geometrically identical</i> — same mapped role set, same canonical frames, same
/// normalized rest rotations. This is the same-rig round-trip case and is lossless (exact
/// identity, twist included).</item>
/// <item><b>Direction matching</b> when the phalanx counts match ordinally but the rigs
/// differ (the common cross-rig case, e.g. Mixamo Prox/Mid/Dist onto the s&box finger
/// with its extra metacarpal — which keeps its rest local; a source metacarpal's rotation is
/// implicit in the proximal's absolute direction). Each target phalanx is swung — shortest
/// arc, rotation axis ⊥ the finger axis, hence <b>zero twist by construction</b> — so that its
/// segment direction matches the source phalanx's direction in character-frame coordinates
/// exactly. Curl and splay are both captured by the direction; the source's axial twist is
/// dropped (hinge-joint noise; copying it absolutely would read as roll through the
/// inter-phalanx canonical mismatch between rigs, measured up to ~12° on thumbs).</item>
/// <item><b>Proportional redistribution</b> when phalanx counts differ (e.g. a two-phalanx
/// source finger): per-phalanx local curls — swing-twist about the canonical hinge Y of
/// <c>λ_b = C_b⁻¹·(ΔR_prev⁻¹·ΔR_b)·C_b</c> — are summed over the source chain (metacarpal
/// included) and redistributed over the target phalanges proportional to rest segment
/// lengths; splay (metacarpal + proximal, swing-twist about canonical Z) goes 100% to the
/// target proximal; the X-twist residual is dropped.</item>
/// </list>
/// In every mode target world deltas rebuild hierarchically from the solved target hand:
/// <c>ΔR_i = ΔR_{i-1} · (C_i · λ_i · C_i⁻¹)</c>, then <c>W_i = ΔR_i · R_tgtNormRest,i</c>.
/// Instances are per-solve and not thread-safe.
/// </summary>
internal sealed class FingerSolver
{
/// <summary>Two canonical frames / rest rotations within this angle count as identical
/// (same-rig detection for the lossless 1:1 path); cross-rig differences are degrees.</summary>
private const float SameRigToleranceRad = 1e-3f;
private enum ChainMode
{
DirectionMatch,
Proportional,
}
private readonly struct SourcePhalanx
{
public required int Slot { get; init; }
public required Quaternion C { get; init; }
public required Quaternion CInv { get; init; }
public required bool TakesSplay { get; init; }
}
private readonly struct Recipient
{
public required int TgtBone { get; init; }
public required Quaternion C { get; init; }
public required Quaternion CInv { get; init; }
public required Quaternion RestRot { get; init; }
public required float Weight { get; init; }
public required bool Splay { get; init; }
}
private sealed class Chain
{
public required ChainMode Mode { get; init; }
public required int SrcHandSlot { get; init; }
public required int TgtHandBone { get; init; }
public required Quaternion TgtHandNormRestRotInv { get; init; }
public required SourcePhalanx[] Sources { get; init; }
public required Recipient[] Recipients { get; init; }
}
private readonly List<Chain> _chains;
private readonly Quaternion _chrSrcInv;
private readonly Quaternion _chrTgt;
private FingerSolver(List<Chain> chains, Quaternion chrSrcInv, Quaternion chrTgt)
{
_chains = chains;
_chrSrcInv = chrSrcInv;
_chrTgt = chrTgt;
}
// ---------------------------------------------------------------- role tables
private static readonly BoneRole[][] ChainRoles = BuildChainRoles();
private static readonly HashSet<BoneRole> FingerRoleSet = ChainRoles.SelectMany(c => c.Skip(1)).ToHashSet();
private static BoneRole[][] BuildChainRoles()
{
var chains = new List<BoneRole[]>();
foreach (var side in new[] { "L", "R" })
{
foreach (var finger in new[] { "Thumb", "Index", "Middle", "Ring", "Pinky" })
{
// Element 0 is the hand the chain hangs off; 1.. are Meta/Prox/Mid/Dist.
chains.Add(new[]
{
Enum.Parse<BoneRole>("Hand" + side),
Enum.Parse<BoneRole>(finger + "Meta" + side),
Enum.Parse<BoneRole>(finger + "Prox" + side),
Enum.Parse<BoneRole>(finger + "Mid" + side),
Enum.Parse<BoneRole>(finger + "Dist" + side),
});
}
}
return chains.ToArray();
}
/// <summary>True for the 40 per-finger segment roles (Meta/Prox/Mid/Dist × finger × side).</summary>
public static bool IsFingerRole(BoneRole role) => FingerRoleSet.Contains(role);
// ---------------------------------------------------------------- build
/// <summary>
/// Builds the per-chain plans. Geometrically identical chains are reported through
/// <paramref name="transferOneToOne"/> instead of being planned here. Returns null when
/// every mapped chain took that path (or none is mapped).
/// </summary>
public static FingerSolver? Build(
MappingResult sourceMap,
CanonicalFrames srcCanon,
IReadOnlyList<XForm> srcNormRest,
Func<BoneRole, int?> tgtBoneForRole,
CanonicalFrames tgtCanon,
IReadOnlyList<XForm> tgtNormRest,
Quaternion chrSrcInv,
Quaternion chrTgt,
Func<int, int> registerSlot,
Action<BoneRole> transferOneToOne)
{
var chains = new List<Chain>();
foreach (var chainRoles in ChainRoles)
{
var handRole = chainRoles[0];
var metaRole = chainRoles[1];
var proxRole = chainRoles[2];
var segments = chainRoles.Skip(1).ToArray();
var srcRoles = segments
.Where(r => sourceMap.RoleToBone.ContainsKey(r) && srcCanon.Has(r))
.ToArray();
var tgtRoles = segments
.Where(r => tgtBoneForRole(r) is not null && tgtCanon.Has(r))
.ToArray();
if (srcRoles.Length == 0 || tgtRoles.Length == 0)
continue;
if (srcRoles.SequenceEqual(tgtRoles) && ChainsCoincide(
srcRoles, sourceMap, srcCanon, srcNormRest, tgtBoneForRole, tgtCanon, tgtNormRest))
{
foreach (var role in srcRoles)
transferOneToOne(role);
continue;
}
var srcPhalanges = srcRoles.Where(r => r != metaRole).ToArray();
var tgtPhalanges = tgtRoles.Where(r => r != metaRole).ToArray();
var recipientRoles = tgtPhalanges.Length > 0 ? tgtPhalanges : tgtRoles;
var mode = srcPhalanges.Length == recipientRoles.Length && srcPhalanges.Length > 0
? ChainMode.DirectionMatch
: ChainMode.Proportional;
// Direction matching consumes only the non-meta phalanges (the metacarpal's
// motion is implicit in the proximal's absolute direction); redistribution
// decomposes every mapped source segment including the metacarpal.
var sourceRolesUsed = mode == ChainMode.DirectionMatch ? srcPhalanges : srcRoles;
var sources = sourceRolesUsed.Select(r =>
{
var c = srcCanon.WorldFrameOf(r);
return new SourcePhalanx
{
Slot = registerSlot(sourceMap.RoleToBone[r]),
C = c,
CInv = Quaternion.Conjugate(c),
TakesSplay = r == metaRole || r == proxRole,
};
}).ToArray();
var weights = SegmentWeights(tgtRoles, recipientRoles, tgtBoneForRole, tgtNormRest);
var recipients = recipientRoles.Select((r, i) =>
{
var bone = tgtBoneForRole(r)!.Value;
var c = tgtCanon.WorldFrameOf(r);
return new Recipient
{
TgtBone = bone,
C = c,
CInv = Quaternion.Conjugate(c),
RestRot = tgtNormRest[bone].Rot,
Weight = weights[i],
Splay = i == 0,
};
}).ToArray();
var tgtHand = tgtBoneForRole(handRole);
chains.Add(new Chain
{
Mode = mode,
SrcHandSlot = sourceMap.RoleToBone.TryGetValue(handRole, out var srcHand)
? registerSlot(srcHand)
: -1,
TgtHandBone = tgtHand ?? -1,
TgtHandNormRestRotInv = tgtHand is int h
? Quaternion.Conjugate(tgtNormRest[h].Rot)
: Quaternion.Identity,
Sources = sources,
Recipients = recipients,
});
}
return chains.Count > 0 ? new FingerSolver(chains, chrSrcInv, chrTgt) : null;
}
/// <summary>Same-rig detection: every chain member's canonical frame and normalized rest
/// rotation agree between source and target (within float noise). Only then is the 1:1
/// absolute copy lossless.</summary>
private static bool ChainsCoincide(
BoneRole[] roles, MappingResult sourceMap, CanonicalFrames srcCanon,
IReadOnlyList<XForm> srcNormRest, Func<BoneRole, int?> tgtBoneForRole,
CanonicalFrames tgtCanon, IReadOnlyList<XForm> tgtNormRest)
{
foreach (var role in roles)
{
var srcBone = sourceMap.RoleToBone[role];
var tgtBone = tgtBoneForRole(role)!.Value;
if (MathQ.AngleBetween(srcCanon.WorldFrameOf(role), tgtCanon.WorldFrameOf(role)) > SameRigToleranceRad
|| MathQ.AngleBetween(srcNormRest[srcBone].Rot, tgtNormRest[tgtBone].Rot) > SameRigToleranceRad)
{
return false;
}
}
return true;
}
/// <summary>Normalized rest segment lengths of the recipient phalanges (the proportional
/// curl weights). The distal segment, having no chain child, is estimated as 0.8× its
/// preceding segment.</summary>
private static float[] SegmentWeights(
BoneRole[] tgtRoles, BoneRole[] recipientRoles,
Func<BoneRole, int?> tgtBoneForRole, IReadOnlyList<XForm> tgtNormRest)
{
var positions = tgtRoles.Select(r => tgtNormRest[tgtBoneForRole(r)!.Value].Pos).ToArray();
var weights = new float[recipientRoles.Length];
for (var i = 0; i < recipientRoles.Length; i++)
{
var j = Array.IndexOf(tgtRoles, recipientRoles[i]);
weights[i] = j + 1 < positions.Length
? (positions[j + 1] - positions[j]).Length()
: j > 0 ? 0.8f * (positions[j] - positions[j - 1]).Length() : 1f;
}
var sum = weights.Sum();
if (sum <= 1e-6f)
return Enumerable.Repeat(1f / weights.Length, weights.Length).ToArray();
for (var i = 0; i < weights.Length; i++)
weights[i] /= sum;
return weights;
}
// ---------------------------------------------------------------- per frame
/// <summary>
/// Solves the planned chains for one frame. <paramref name="srcDeltas"/> holds the
/// registered source world rotation deltas (from normalized rest); solved target world
/// rotations are written into <paramref name="rot"/>/<paramref name="solved"/>. The target
/// hands must already be solved (body pass runs first).
/// </summary>
public void Apply(Quaternion[] srcDeltas, bool[] solved, Quaternion[] rot)
{
foreach (var chain in _chains)
{
var acc = chain.TgtHandBone >= 0 && solved[chain.TgtHandBone]
? MathQ.Normalize(rot[chain.TgtHandBone] * chain.TgtHandNormRestRotInv)
: Quaternion.Identity;
if (chain.Mode == ChainMode.DirectionMatch)
ApplyDirectionMatch(chain, srcDeltas, acc, solved, rot);
else
ApplyProportional(chain, srcDeltas, acc, solved, rot);
}
}
private void ApplyDirectionMatch(
Chain chain, Quaternion[] srcDeltas, Quaternion acc, bool[] solved, Quaternion[] rot)
{
for (var i = 0; i < chain.Recipients.Length; i++)
{
var sp = chain.Sources[i];
var rc = chain.Recipients[i];
// Source phalanx direction in character coords; re-expressed in the target world,
// then relative to the already-reconstructed parent delta, then in the phalanx's
// canonical frame — where the rest direction is unit X.
var srcAbs = MathQ.Normalize(_chrSrcInv * srcDeltas[sp.Slot] * sp.C);
var dirChr = Vector3.Transform(Vector3.UnitX, srcAbs);
var dirTgtWorld = Vector3.Transform(dirChr, _chrTgt);
var dirLocal = Vector3.Transform(dirTgtWorld, Quaternion.Conjugate(acc));
var dirCanon = Vector3.Transform(dirLocal, rc.CInv);
// Shortest-arc swing X -> dir: rotation axis ⊥ X, so it carries zero finger-axis
// twist by construction.
var swing = MathQ.FromTo(Vector3.UnitX, dirCanon);
acc = MathQ.Normalize(acc * (rc.C * swing * rc.CInv));
rot[rc.TgtBone] = MathQ.Normalize(acc * rc.RestRot);
solved[rc.TgtBone] = true;
}
}
private static void ApplyProportional(
Chain chain, Quaternion[] srcDeltas, Quaternion acc, bool[] solved, Quaternion[] rot)
{
// Decompose: total local curl over the chain, splay from metacarpal + proximal.
var prev = chain.SrcHandSlot >= 0 ? srcDeltas[chain.SrcHandSlot] : Quaternion.Identity;
float totalCurl = 0f, splay = 0f;
foreach (var sp in chain.Sources)
{
var dr = srcDeltas[sp.Slot];
var local = MathQ.Normalize(Quaternion.Conjugate(prev) * dr);
var canon = MathQ.Normalize(sp.CInv * local * sp.C);
MathQ.SwingTwist(canon, Vector3.UnitY, out var swing, out var curlQ);
totalCurl += SignedAngle(curlQ, Vector3.UnitY);
if (sp.TakesSplay)
{
MathQ.SwingTwist(swing, Vector3.UnitZ, out _, out var splayQ);
splay += SignedAngle(splayQ, Vector3.UnitZ);
}
prev = dr;
}
foreach (var rc in chain.Recipients)
{
var mu = Quaternion.CreateFromAxisAngle(Vector3.UnitY, totalCurl * rc.Weight);
if (rc.Splay)
mu = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, splay) * mu;
acc = MathQ.Normalize(acc * (rc.C * mu * rc.CInv));
rot[rc.TgtBone] = MathQ.Normalize(acc * rc.RestRot);
solved[rc.TgtBone] = true;
}
}
/// <summary>Signed rotation angle of an axis-aligned twist quaternion about
/// <paramref name="axis"/>, wrapped to (−π, π].</summary>
private static float SignedAngle(Quaternion twist, Vector3 axis)
{
var s = twist.X * axis.X + twist.Y * axis.Y + twist.Z * axis.Z;
var angle = 2f * MathF.Atan2(s, twist.W);
if (angle > MathF.PI)
angle -= 2f * MathF.PI;
else if (angle < -MathF.PI)
angle += 2f * MathF.PI;
return angle;
}
}
Game
library
#nullable enable annotations
using System.Collections.Generic;
using HumanoidRetargeter.Mapping;
namespace HumanoidRetargeter.Solve;
/// <summary>How a mapped role's rotation is transferred by the <see cref="GeometricSolver"/>.</summary>
public enum RoleTransferMode
{
/// <summary>
/// Absolute canonical-orientation matching: the target's animated chain direction is
/// driven to <b>equal</b> the source's (in character-frame coordinates). Right for limbs
/// and the spine — the pose IS the direction — but it also imposes the source rig's rest
/// proportions/posture on roles whose rest directions legitimately differ between rigs.
/// </summary>
AbsoluteDirection,
/// <summary>
/// Rest-relative delta: the source's canonical-space rotation <i>delta from its own
/// normalized rest</i> is replayed onto the <b>target's</b> normalized rest
/// (<c>W_t(f) = C_t·ΔC(f)·C_t⁻¹·R_tgtNormRest</c> with
/// <c>ΔC(f) = C_s⁻¹·ΔR(f)·C_s</c>). The target keeps its own rest carriage (shoulder
/// line height, neck-base angle) and moves with the source. Identical to
/// <see cref="AbsoluteDirection"/> when source and target rigs coincide.
/// </summary>
DeltaFromRest,
/// <summary>
/// Character-space delta: the source's world-rotation delta from its normalized rest is
/// re-expressed in character coordinates and applied to the <b>target's</b> normalized
/// rest (<c>W_t(f) = M·ΔR(f)·M⁻¹·R_tgtNormRest</c> with <c>M = Q_tgt·Q_src⁻¹</c>, the
/// same character basis change <see cref="AbsoluteDirection"/> premultiplies). Like
/// <see cref="DeltaFromRest"/> the target keeps its own rest carriage, but the delta
/// keeps its <i>world</i> rotation axes instead of being remapped through the per-role
/// canonical frames — the faithful replay when the rigs' rest chain directions diverge
/// so far that canonical-axis remapping would tilt every rotation axis by that
/// divergence (measured 23–44° on feet: CMU/ARP ankle anatomy vs the s&box rig's
/// steep ankle, where canonical remapping mis-pitched planted feet by up to 47°).
/// Identical to the other modes when source and target rigs coincide.
/// </summary>
CharacterDeltaFromRest,
}
/// <summary>Options controlling a single retarget solve (one clip → one output clip).</summary>
public sealed class SolveOptions
{
/// <summary>
/// Default per-role transfer modes: shoulder girdle and neck carriage are
/// <see cref="RoleTransferMode.DeltaFromRest"/> (each rig's clavicle line / neck-base
/// direction is rig anatomy, not pose — absolute matching was measured to drag the
/// s&box shoulders 6–28° toward the source's flatter/lower clavicle line and is the
/// "low shoulders, hunched neck" artifact), and feet are
/// <see cref="RoleTransferMode.CharacterDeltaFromRest"/> (a rest foot→toe direction is
/// ankle anatomy too — rigs diverge 11–44° from the s&box rig's steep ankle, so
/// absolute matching pitched planted feet up to 25° off flat, the "feet bent
/// upward/inward" artifact; the character-space delta keeps the rotation's world axes,
/// which canonical-frame remapping would tilt by that same divergence). The head is
/// <see cref="RoleTransferMode.CharacterDeltaFromRest"/> for the same reason: the rest
/// neck→head direction is head-joint-placement anatomy (measured 0–27° forward lean
/// across neutral-rest rigs vs the s&box rig's 25.5°), so the target keeps its own
/// neutral skull attitude and replays the source's attitude <i>changes</i> — for the
/// head this computes exactly what the previous virtual-frame absolute matching did.
/// Two solver fallbacks adjust these defaults per rig pair: on a toe-less source the
/// foot entries become <see cref="RoleTransferMode.DeltaFromRest"/> (virtual-foot
/// fallback), and a source whose normalized rest head attitude is implausible as a
/// neutral carriage (a posed bind — e.g. a chin-down/tilted fighting-stance rest,
/// measured 40.7° forward / 16.9° lateral on such a rig where the delta replay read
/// ~12° "looking up at an angle") switches the head to
/// <see cref="RoleTransferMode.AbsoluteDirection"/> so the gaze follows the source
/// absolutely instead of replaying deltas from a posed reference (see the
/// <see cref="GeometricSolver"/> remarks for both). Everything else (limbs, spine,
/// toes, fingers) stays absolute: there the worldspace direction IS the pose.
/// </summary>
public static IReadOnlyDictionary<BoneRole, RoleTransferMode> DefaultTransferModes { get; } =
new Dictionary<BoneRole, RoleTransferMode>
{
[BoneRole.ClavicleL] = RoleTransferMode.DeltaFromRest,
[BoneRole.ClavicleR] = RoleTransferMode.DeltaFromRest,
[BoneRole.Neck] = RoleTransferMode.DeltaFromRest,
[BoneRole.Head] = RoleTransferMode.CharacterDeltaFromRest,
[BoneRole.FootL] = RoleTransferMode.CharacterDeltaFromRest,
[BoneRole.FootR] = RoleTransferMode.CharacterDeltaFromRest,
};
/// <summary>
/// Per-role transfer modes. Null (default) = <see cref="DefaultTransferModes"/> plus the
/// solver's fallback heuristics (a toe-less source's virtual foot direction overrides
/// the foot default to <see cref="RoleTransferMode.DeltaFromRest"/>, and a posed-rest
/// source head overrides the head default to
/// <see cref="RoleTransferMode.AbsoluteDirection"/> — see the
/// <see cref="GeometricSolver"/> remarks). A non-null map REPLACES the defaults entirely
/// and disables every fallback heuristic: each role uses exactly the mode in the map, and
/// roles absent from it are <see cref="RoleTransferMode.AbsoluteDirection"/>. Pass an
/// empty dictionary for fully absolute (legacy) behavior — API callers supplying a map
/// opt out of all heuristics.
/// </summary>
public IReadOnlyDictionary<BoneRole, RoleTransferMode>? TransferModes { get; init; }
/// <summary>
/// Scale applied to the pelvis translation components perpendicular to the character up
/// direction. Null (default) = automatic: target hip height / source hip height, both
/// measured on the normalized rests.
/// </summary>
public float? HipScaleHorizontal { get; init; }
/// <summary>
/// Scale applied to the pelvis translation component along the character up direction.
/// Null (default) = the same automatic hip-height ratio as <see cref="HipScaleHorizontal"/>.
/// </summary>
public float? HipScaleVertical { get; init; }
/// <summary>Whether finger roles are transferred; when false, target finger bones keep
/// their rest locals.</summary>
public bool TransferFingers { get; init; } = true;
/// <summary>Output clip name; null = the source clip's name.</summary>
public string? ClipName { get; init; }
/// <summary>Index of the source clip to retarget (<c>SourceScene.Clips</c>).</summary>
public int ClipIndex { get; init; }
}
Game
library
#nullable enable annotations
using System;
using System.Collections.Generic;
using System.Numerics;
using HumanoidRetargeter.Maths;
using SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;
namespace HumanoidRetargeter.Cleanup;
using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)
/// <summary>Tunables for the grounded-foot stance recalibration pass.</summary>
public sealed class FootGroundAlignOptions
{
/// <summary>
/// Dead zone (degrees): measured stance offsets at or below this are genuine planted
/// articulation (heel-roll bias, natural lean — measured 2–4° on well-rested rigs and
/// on citizen clips) and are left untouched, keeping the transfer byte-faithful there.
/// Only offsets beyond it are clearly rest-pose artifacts (measured 12–25° on the
/// repro rig) and get recalibrated.
/// </summary>
public float MinCorrectionDeg { get; set; } = 8f;
/// <summary>
/// Maximum mean sole deviation (degrees) a plant may show and still count as a STANCE
/// for the offset measurement. Plants beyond this are not standing on the sole (crawls,
/// kneels, prone contact — measured 60–90° there) and are excluded; genuine rest-pose
/// stance artifacts measure well below it (largest seen: 27°).
/// </summary>
public float MaxStanceDeviationDeg { get; set; } = 35f;
}
/// <summary>Per-foot results of a <see cref="FootGroundAlign.Apply"/> run.</summary>
public sealed class FootGroundAlignFootReport
{
/// <summary>Plants that contributed to the stance measurement.</summary>
public int StancePlants { get; set; }
/// <summary>Plants excluded as non-stance (mean sole deviation beyond
/// <see cref="FootGroundAlignOptions.MaxStanceDeviationDeg"/>).</summary>
public int SkippedPlants { get; set; }
/// <summary>Measured planted sole offset from the ground plane, degrees (0 when no
/// stance plants exist).</summary>
public float MeasuredOffsetDeg { get; set; }
/// <summary>Foot correction applied to every frame, degrees (0 = inside the dead zone,
/// nothing changed).</summary>
public float AppliedFootDeg { get; set; }
/// <summary>Toe correction applied to every frame, degrees.</summary>
public float AppliedToeDeg { get; set; }
}
/// <summary>Results of a <see cref="FootGroundAlign.Apply"/> run.</summary>
public sealed class FootGroundAlignReport
{
/// <summary>Left-foot results.</summary>
public required FootGroundAlignFootReport Left { get; init; }
/// <summary>Right-foot results.</summary>
public required FootGroundAlignFootReport Right { get; init; }
}
/// <summary>
/// Grounded-foot stance recalibration: measures how far the foot's SOLE sits from the ground
/// plane while planted, and — when that offset is clearly a rest-pose artifact — rotates it
/// out with one constant per foot, applied to every frame of the clip.
/// </summary>
/// <remarks>
/// <para><b>Why a cleanup pass.</b> The solver transfers feet as rest-relative deltas
/// (<see cref="Solve.RoleTransferMode.CharacterDeltaFromRest"/>), so the target keeps its own
/// ankle anatomy — correct whenever the source's rest pose is a flat-footed stance (the delta
/// is then "deviation from standing"). Some rigs ship a NON-stance rest (measured: an
/// Auto-Rig-Pro export whose rest foot sits 12–25° from its planted stance), and that constant
/// offset rides into every frame of the replay — planted feet hover toe-down/heel-up. What a
/// stance actually looks like is animation evidence (planted phases), which a per-frame
/// solver cannot see, so the recalibration lives here.</para>
/// <para><b>Measurement.</b> Per foot: over every planted frame, the sole normal = rest up
/// carried by the foot's world delta from the target bind rest (whose feet stand on the
/// ground by construction); plants whose own mean normal sits beyond
/// <see cref="FootGroundAlignOptions.MaxStanceDeviationDeg"/> are excluded (crawl/kneel/prone
/// contact is not a stance). The pooled mean normal's deviation from up is the stance
/// offset.</para>
/// <para><b>Correction.</b> Offsets inside <see cref="FootGroundAlignOptions.MinCorrectionDeg"/>
/// are genuine articulation — nothing is changed (well-rested rigs and same-rig round trips
/// stay byte-identical through this pass). Beyond it, the shortest-arc rotation taking the
/// pooled normal back to up (pitch+roll only — yaw/toe-out is pose and follows the source)
/// premultiplies the foot's world rotation on EVERY frame: a rest artifact is constant, so
/// the fix is too — within-plant heel-roll, swing styling and frame-to-frame continuity are
/// preserved exactly, and no blending is needed. The toe then receives its own residual
/// constant measured on top of the corrected foot (it neither double-rotates with the foot
/// fix nor inherits the source toe's own rest artifact). Corrections rotate bones about
/// their own joints: ankle positions are untouched, so the pass composes freely with the
/// <see cref="FootPlant"/> position pinning (which preserves foot world rotations).</para>
/// <para><b>Plant intervals come from the caller</b> (the pipeline detects them on the
/// SOURCE clip via <see cref="FootPlant.DetectPlantIntervals"/> — ground truth, immune to
/// the hip-height rescaling that can push target-side trajectories outside the cm-tuned
/// Kovar thresholds). So does the decision to run at all: the pipeline invokes this pass
/// only when the source's normalized rest is implausible as a flat stance (toe at/above
/// ankle level or asymmetric feet — see <c>Retargeter.GroundAlignFeet</c>); on plausible
/// stance rests the solver's rest-relative transfer is already faithful and planted-sole
/// deviations are genuine articulation (boxing stances, heel rolls) that must not be
/// flattened.</para>
/// </remarks>
public static class FootGroundAlign
{
/// <summary>Measures planted stance offsets and recalibrates feet whose offset is a
/// rest-pose artifact; returns what was measured and done.</summary>
/// <param name="frames">Per-frame local transforms (skeleton bone order); modified in place.</param>
/// <param name="skeleton">Bone hierarchy the frames are expressed against; its bind rest
/// is the flat-stance reference.</param>
/// <param name="left">Left leg chain bone indices.</param>
/// <param name="right">Right leg chain bone indices.</param>
/// <param name="up">World up direction of the clip's space.</param>
/// <param name="leftPlants">Left-foot plant intervals (frame indices into
/// <paramref name="frames"/>; out-of-range parts are clamped/ignored).</param>
/// <param name="rightPlants">Right-foot plant intervals.</param>
/// <param name="options">Tunables; defaults used when null.</param>
public static FootGroundAlignReport Apply(
List<XForm[]> frames,
SkeletonModel skeleton,
FootChain left,
FootChain right,
Vector3 up,
IReadOnlyList<FrameRange> leftPlants,
IReadOnlyList<FrameRange> rightPlants,
FootGroundAlignOptions? options = null)
{
ArgumentNullException.ThrowIfNull(frames);
ArgumentNullException.ThrowIfNull(skeleton);
ArgumentNullException.ThrowIfNull(left);
ArgumentNullException.ThrowIfNull(right);
ArgumentNullException.ThrowIfNull(leftPlants);
ArgumentNullException.ThrowIfNull(rightPlants);
options ??= new FootGroundAlignOptions();
var report = new FootGroundAlignReport
{
Left = new FootGroundAlignFootReport(),
Right = new FootGroundAlignFootReport(),
};
if (frames.Count == 0 || up.LengthSquared() < 1e-12f)
return report;
up = Vector3.Normalize(up);
RecalibrateFoot(frames, skeleton, left, up, leftPlants, options, report.Left);
RecalibrateFoot(frames, skeleton, right, up, rightPlants, options, report.Right);
return report;
}
private static void RecalibrateFoot(
List<XForm[]> frames, SkeletonModel skeleton, FootChain chain, Vector3 up,
IReadOnlyList<FrameRange> plants, FootGroundAlignOptions options,
FootGroundAlignFootReport report)
{
int n = frames.Count;
var foot = chain.Ankle;
var restFootRotInv = Quaternion.Conjugate(skeleton.RestWorld[foot].Rot);
var maxStanceCos = MathF.Cos(options.MaxStanceDeviationDeg * MathF.PI / 180f);
// ---- measurement: pooled planted sole normal over the stance plants ----
var pooled = Vector3.Zero;
foreach (var plant in plants)
{
int start = Math.Max(plant.Start, 0);
int end = Math.Min(plant.End, n - 1);
if (start > end)
continue;
var plantSum = Vector3.Zero;
for (int f = start; f <= end; f++)
{
var footRot = FkUtil.BoneWorld(frames[f], skeleton, foot).Rot;
plantSum += Vector3.Transform(up, MathQ.Normalize(footRot * restFootRotInv));
}
if (plantSum.LengthSquared() < 1e-8f
|| Vector3.Dot(Vector3.Normalize(plantSum), up) < maxStanceCos)
{
report.SkippedPlants++; // not standing on the sole — crawl/kneel/toe contact
continue;
}
report.StancePlants++;
pooled += plantSum; // frame-count-weighted: longer stances dominate
}
if (pooled.LengthSquared() < 1e-8f)
return;
pooled = Vector3.Normalize(pooled);
var offsetDeg = MathQ.AngleBetween(pooled, up) * (180f / MathF.PI);
report.MeasuredOffsetDeg = offsetDeg;
if (offsetDeg <= options.MinCorrectionDeg)
return; // genuine planted articulation — leave the transfer byte-faithful
// ---- correction: one constant per foot, every frame ----
var footFix = MathQ.FromTo(pooled, up);
report.AppliedFootDeg = offsetDeg;
// Toe residual measured on top of the corrected foot, same dead zone.
var toeFix = Quaternion.Identity;
if (chain.Toe is { } toe && skeleton[toe].ParentIndex == foot)
{
var restToeRotInv = Quaternion.Conjugate(skeleton.RestWorld[toe].Rot);
var toePooled = Vector3.Zero;
foreach (var plant in plants)
{
int start = Math.Max(plant.Start, 0);
int end = Math.Min(plant.End, n - 1);
for (int f = start; f <= end && f >= 0; f++)
{
var toeRot = FkUtil.BoneWorld(frames[f], skeleton, toe).Rot;
toePooled += Vector3.Transform(
up, MathQ.Normalize(footFix * toeRot * restToeRotInv));
}
}
if (toePooled.LengthSquared() > 1e-8f)
{
toePooled = Vector3.Normalize(toePooled);
var toeDeg = MathQ.AngleBetween(toePooled, up) * (180f / MathF.PI);
if (toeDeg > options.MinCorrectionDeg && Vector3.Dot(toePooled, up) >= maxStanceCos)
{
toeFix = MathQ.FromTo(toePooled, up);
report.AppliedToeDeg = toeDeg;
}
}
}
for (int f = 0; f < n; f++)
CorrectFrame(frames[f], skeleton, chain, footFix, toeFix);
}
/// <summary>Premultiplies the foot's world rotation by the constant fix (the joint
/// position is untouched — the rotation pivots the foot about its own head), then gives
/// the toe its own residual on top of the corrected foot.</summary>
private static void CorrectFrame(
XForm[] locals, SkeletonModel skeleton, FootChain chain,
Quaternion footFix, Quaternion toeFix)
{
var foot = chain.Ankle;
var parent = skeleton[foot].ParentIndex;
var parentRot = parent < 0
? Quaternion.Identity
: FkUtil.BoneWorld(locals, skeleton, parent).Rot;
var footWorld = MathQ.Normalize(parentRot * locals[foot].Rot);
var newFootWorld = MathQ.Normalize(footFix * footWorld);
locals[foot] = new XForm(
locals[foot].Pos, MathQ.Normalize(Quaternion.Conjugate(parentRot) * newFootWorld));
if (chain.Toe is { } toe && skeleton[toe].ParentIndex == foot)
{
// Desired toe world = toeFix ∘ footFix ∘ original world; re-derive its local
// against the corrected foot so it does not double-rotate with the foot fix.
var toeWorldOld = MathQ.Normalize(footWorld * locals[toe].Rot);
var desired = MathQ.Normalize(toeFix * footFix * toeWorldOld);
locals[toe] = new XForm(
locals[toe].Pos, MathQ.Normalize(Quaternion.Conjugate(newFootWorld) * desired));
}
}
}
Game
library
#nullable enable annotations
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Numerics;
using System.Text;
using HumanoidRetargeter.Maths;
using HumanoidRetargeter.Skeleton;
namespace HumanoidRetargeter.Formats.Bvh;
using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)
/// <summary>Options for <see cref="BvhImporter.Import"/>.</summary>
public sealed class BvhImportOptions
{
/// <summary>Fixed resampling rate for the motion data, frames per second.</summary>
public float SampleFps { get; init; } = 30f;
}
/// <summary>
/// BVH (Biovision Hierarchy) → <see cref="SourceScene"/> importer.
/// </summary>
/// <remarks>
/// <para><b>Format conventions implemented</b> (verified against Blender's
/// <c>io_anim_bvh</c> importer, which is the project's ground-truth extractor):</para>
/// <list type="bullet">
/// <item><b>Rest pose:</b> each joint's rest local translation is its <c>OFFSET</c>; rest
/// rotation is identity (BVH stores no rest orientation).</item>
/// <item><b>Rotation channels:</b> the channel list order IS the rotation order. The listed
/// rotations apply left-to-right as intrinsic rotations, which in this library's
/// column-vector convention (<c>a * b</c> applies <c>b</c> first) is the product
/// <c>R = R_chan1 * R_chan2 * R_chan3</c> — e.g. <c>Zrotation Yrotation Xrotation</c> gives
/// <c>R = Rz * Ry * Rx</c>. This matches Blender, which builds
/// <c>Euler((x,y,z), reversed(channelOrder))</c> for the same matrix. Angles are degrees.</item>
/// <item><b>Position channels:</b> when a joint has any position channel, the channel values
/// REPLACE the joint's local translation (missing components are 0) — they are not added to
/// the <c>OFFSET</c>. This is Blender's behavior; in practice roots have OFFSET 0 so the two
/// readings only diverge on non-root position channels (e.g. Bandai-Namco exports).</item>
/// <item><b>End Sites:</b> synthesized as a channel-less leaf bone named
/// <c>"<parent>_end"</c> so chain tips keep their direction information (Blender instead
/// folds them into the parent bone's tail).</item>
/// </list>
/// <para><b>Units</b>: BVH files carry no unit declaration. Heuristic: compute the rest
/// skeleton height (max−min world Y over all joints); if it is < 10 the file is assumed
/// to be in meters and all translations (offsets AND position channels, root included) are
/// scaled ×100 to centimeters, otherwise it is assumed to already be centimeters (×1).
/// Millimeter-scale files (height > 400) are not special-cased — they are rare and
/// ambiguous against cm mocap of long ranges; <see cref="SourceScene.UnitScaleCm"/> records
/// whichever factor was applied for diagnostics.</para>
/// <para><b>Calibration rest-frame trim</b>: mocap exports (CMU asf/amc conversions among
/// them) often prepend a skeleton-calibration segment — the rest pose itself (all rotation
/// channels ≈ 0), hard-cut (or blend-ramped over 2–3 frames) into the real motion. Played
/// back it reads as a T-pose flash at t = 0. The importer drops such a segment from either
/// clip end when ALL of (measured margins in parentheses, over the corpus + repro files):
/// every segment frame is rest-like (max joint rotation vs the identity rest ≤ 40°;
/// calibration frames/ramps measure ≤ 24°, real clip edges ≥ 80°); the segment is short
/// (≤ 4 rest-like frames — hard cuts measure 1, blend ramps 2; longer rest-like leads are
/// content); the clip beyond it is NOT rest-like; and the discontinuity where the segment
/// exits into the motion is both large in absolute terms (≥ 20°; measured 25–177°) and
/// large versus the clip's own typical inter-frame delta (≥ 4× the median; real clip edges
/// measure ≤ 8° at ≤ ~1× the median). A qualifying segment that exits through a multi-frame
/// blend RAMP (measured 22–25°/frame for 3 frames on a makehuman-retarget export) has the
/// ramp trimmed too, until the motion settles — 8 frames total per end at most. A clip that
/// legitimately starts near rest (an idle) is continuous into the motion and never trips
/// the discontinuity gates. Note the trim can never remove
/// the reference frame a non-anatomical stick bind needs for its rest rebuild (see
/// <c>RestNormalizer</c>): it only removes frames that MATCH the identity-rotation bind,
/// and a frame matching a stick bind carries no rest information the bind itself lacks —
/// the next (real) frame is then strictly the better reference.</para>
/// <para><b>Resampling</b>: motion frames are resampled from the file's <c>Frame Time</c>
/// grid onto <see cref="BvhImportOptions.SampleFps"/>. Each native frame's euler channels are
/// converted to a quaternion FIRST and bracketing frames are then slerped (positions lerped).
/// Interpolating raw euler angles across frames would mostly work at mocap densities
/// (30–120 fps, small per-frame deltas) but breaks down when an angle wraps ±180° between
/// frames; per-frame quaternion + slerp has no such failure mode, so that is what we do.</para>
/// <para><b>Axes</b>: BVH is conventionally Y-up / Z-forward / X-right. Native axes are
/// preserved (no conversion), matching the FBX importer's policy; the conventional axes are
/// recorded on the <see cref="SourceScene"/> (up = Y, front = Z, coord = X).</para>
/// </remarks>
public static class BvhImporter
{
private const float MeterHeightThreshold = 10f;
/// <summary>Parses BVH bytes and builds the source scene.</summary>
/// <exception cref="FormatException">Malformed or truncated BVH.</exception>
public static SourceScene Import(byte[] data, BvhImportOptions? options = null)
{
ArgumentNullException.ThrowIfNull(data);
options ??= new BvhImportOptions();
if (!(options.SampleFps > 0f) || !float.IsFinite(options.SampleFps))
throw new ArgumentOutOfRangeException(nameof(options), "SampleFps must be positive.");
var cursor = new TokenCursor(Encoding.UTF8.GetString(data));
// ---- HIERARCHY -----------------------------------------------------------------
cursor.ExpectKeyword("HIERARCHY");
var joints = new List<Joint>();
int channelCount = 0;
if (!cursor.PeekIs("ROOT"))
throw new FormatException("BVH: expected ROOT after HIERARCHY.");
while (cursor.PeekIs("ROOT")) // multiple roots are out of spec but harmless to accept
{
cursor.Next();
ParseJoint(cursor, joints, parent: -1, ref channelCount);
}
// ---- MOTION ---------------------------------------------------------------------
cursor.ExpectKeyword("MOTION");
cursor.ExpectKeyword("FRAMES:");
int frameCount = cursor.NextInt();
if (frameCount < 0)
throw new FormatException($"BVH: negative frame count {frameCount}.");
cursor.ExpectKeyword("FRAME");
cursor.ExpectKeyword("TIME:");
float frameTime = cursor.NextFloat();
if (!(frameTime > 0f) || !float.IsFinite(frameTime))
throw new FormatException($"BVH: invalid Frame Time {frameTime}.");
var motion = new float[frameCount][];
for (int f = 0; f < frameCount; f++)
{
var row = new float[channelCount];
for (int c = 0; c < channelCount; c++)
row[c] = cursor.NextFloat();
motion[f] = row;
}
// ---- units heuristic --------------------------------------------------------------
float unitScale = HeuristicUnitScale(joints);
// ---- skeleton ----------------------------------------------------------------------
var defs = new List<BoneDefinition>(joints.Count);
foreach (var j in joints)
{
defs.Add(new BoneDefinition(
j.Name,
j.Parent < 0 ? null : joints[j.Parent].Name,
new XForm(j.Offset * unitScale, Quaternion.Identity)));
}
var skeleton = Skeleton.Skeleton.Create(defs);
// ---- clip ----------------------------------------------------------------------------
var clips = new List<Clip>();
if (frameCount > 0)
clips.Add(ResampleClip(joints, skeleton, motion, frameTime, unitScale, options.SampleFps));
// BVH conventional axes: Y-up (1), Z-front (2), X-coord (0) — recorded, not converted.
// RestPlacementAuthored = false: the BVH rest skeleton is OFFSETs only (root at the
// file origin, no authored world placement), while MOTION root positions live in
// absolute capture-volume coordinates — the two share no common ground/origin, so
// the solver must normalize clip placement against the rest skeleton
// (see SourceScene.RestPlacementAuthored and GeometricSolver remarks).
return new SourceScene(
skeleton, clips, unitScale,
upAxis: 1, upAxisSign: 1,
frontAxis: 2, frontAxisSign: 1,
coordAxis: 0, coordAxisSign: 1,
originalUpAxis: -1)
{
RestPlacementAuthored = false,
};
}
// =====================================================================================
// hierarchy parsing
// =====================================================================================
private sealed class Joint
{
public required string Name;
public required int Parent; // index into the joint list, -1 for roots
public Vector3 Offset; // raw file units
public int PosX = -1, PosY = -1, PosZ = -1; // motion column per position axis
public List<(int Axis, int Column)> Rot = new(); // rotation channels in file order
public bool HasPos => PosX >= 0 || PosY >= 0 || PosZ >= 0;
}
private static void ParseJoint(TokenCursor cursor, List<Joint> joints, int parent, ref int channelCount)
{
// Joint name: tokens up to '{', joined with '_' (mirrors Blender's handling of
// names containing spaces).
var nameParts = new List<string>();
while (!cursor.PeekIs("{"))
{
if (cursor.AtEnd)
throw new FormatException("BVH: unexpected end of file in joint name.");
nameParts.Add(cursor.Next());
}
if (nameParts.Count == 0)
throw new FormatException("BVH: joint with no name.");
string name = UniqueName(string.Join('_', nameParts), joints);
cursor.ExpectKeyword("{");
cursor.ExpectKeyword("OFFSET");
var joint = new Joint { Name = name, Parent = parent };
joint.Offset = new Vector3(cursor.NextFloat(), cursor.NextFloat(), cursor.NextFloat());
int index = joints.Count;
joints.Add(joint);
if (cursor.PeekIs("CHANNELS"))
{
cursor.Next();
int n = cursor.NextInt();
if (n < 0 || n > 6)
throw new FormatException($"BVH: joint '{name}' has invalid channel count {n}.");
for (int i = 0; i < n; i++)
{
string channel = cursor.Next();
int column = channelCount++;
switch (channel.ToUpperInvariant())
{
case "XPOSITION": joint.PosX = column; break;
case "YPOSITION": joint.PosY = column; break;
case "ZPOSITION": joint.PosZ = column; break;
case "XROTATION": joint.Rot.Add((0, column)); break;
case "YROTATION": joint.Rot.Add((1, column)); break;
case "ZROTATION": joint.Rot.Add((2, column)); break;
default:
throw new FormatException($"BVH: unknown channel '{channel}' on joint '{name}'.");
}
}
}
while (!cursor.PeekIs("}"))
{
if (cursor.AtEnd)
throw new FormatException($"BVH: unexpected end of file inside joint '{name}'.");
if (cursor.PeekIs("JOINT"))
{
cursor.Next();
ParseJoint(cursor, joints, index, ref channelCount);
}
else if (cursor.PeekIs("END"))
{
cursor.Next();
cursor.ExpectKeyword("SITE");
while (!cursor.PeekIs("{")) // a name after "End Site" is out of spec; skip it
{
if (cursor.AtEnd)
throw new FormatException("BVH: unexpected end of file in End Site.");
cursor.Next();
}
cursor.ExpectKeyword("{");
cursor.ExpectKeyword("OFFSET");
var endOffset = new Vector3(cursor.NextFloat(), cursor.NextFloat(), cursor.NextFloat());
cursor.ExpectKeyword("}");
// Synthesize a channel-less leaf so the chain tip's direction is kept.
joints.Add(new Joint
{
Name = UniqueName(name + "_end", joints),
Parent = index,
Offset = endOffset,
});
}
else
{
throw new FormatException(
$"BVH: unexpected token '{cursor.Next()}' inside joint '{name}'.");
}
}
cursor.ExpectKeyword("}");
}
private static string UniqueName(string name, List<Joint> joints)
{
bool Taken(string candidate)
{
foreach (var j in joints)
if (string.Equals(j.Name, candidate, StringComparison.Ordinal))
return true;
return false;
}
if (!Taken(name))
return name;
for (int i = 1; ; i++)
{
string candidate = $"{name}#{i}";
if (!Taken(candidate))
return candidate;
}
}
// =====================================================================================
// units
// =====================================================================================
/// <summary>
/// Meters-vs-centimeters heuristic: rest skeleton height (max−min world Y over all
/// joints, end sites included) < 10 → meters → ×100; otherwise centimeters → ×1.
/// </summary>
private static float HeuristicUnitScale(List<Joint> joints)
{
Span<float> worldY = joints.Count <= 256 ? stackalloc float[joints.Count] : new float[joints.Count];
float min = float.MaxValue, max = float.MinValue;
for (int i = 0; i < joints.Count; i++)
{
worldY[i] = (joints[i].Parent < 0 ? 0f : worldY[joints[i].Parent]) + joints[i].Offset.Y;
min = MathF.Min(min, worldY[i]);
max = MathF.Max(max, worldY[i]);
}
float height = max - min;
return height > 0f && height < MeterHeightThreshold ? 100f : 1f;
}
// =====================================================================================
// motion sampling
// =====================================================================================
/// <summary>
/// Decodes every native frame to per-joint local transforms (quaternions built per frame
/// from the joint's channel order), drops leading/trailing calibration rest frames (see
/// class remarks), then resamples onto the <paramref name="fps"/> grid — positions
/// lerped, rotations slerped between the bracketing native frames.
/// </summary>
private static Clip ResampleClip(
List<Joint> joints, Skeleton.Skeleton skeleton, float[][] motion,
float frameTime, float unitScale, float fps)
{
int jointCount = joints.Count;
int nativeCount = motion.Length;
// Joint order may differ from skeleton bone order (topological sort) — map.
var toSkeleton = new int[jointCount];
for (int i = 0; i < jointCount; i++)
toSkeleton[i] = skeleton.IndexOf(joints[i].Name);
// Native-frame locals.
var native = new XForm[nativeCount][];
for (int f = 0; f < nativeCount; f++)
{
var row = motion[f];
var locals = new XForm[jointCount];
for (int i = 0; i < jointCount; i++)
locals[i] = EvaluateLocal(joints[i], row, unitScale);
native[f] = locals;
}
// Calibration rest-frame trim: a short rest-like segment per clip end (class remarks).
int first = 0;
int last = nativeCount - 1;
if (nativeCount >= 3)
{
float typicalDeltaDeg = TypicalNeighborRotDeltaDeg(native);
first += CalibrationSegmentLength(native, first, last, step: +1, typicalDeltaDeg);
last -= CalibrationSegmentLength(native, last, first, step: -1, typicalDeltaDeg);
}
int trimmedCount = last - first + 1;
double duration = (trimmedCount - 1) * (double)frameTime;
int outCount = Math.Max(1, (int)Math.Round(duration * fps) + 1);
var frames = new List<XForm[]>(outCount);
for (int f = 0; f < outCount; f++)
{
double s = f / (double)fps / frameTime; // position on the trimmed native frame grid
int i0 = first + Math.Clamp((int)Math.Floor(s), 0, trimmedCount - 1);
int i1 = Math.Min(i0 + 1, last);
float u = Math.Clamp((float)(s - (i0 - first)), 0f, 1f);
var frame = new XForm[skeleton.Count];
var a = native[i0];
var b = native[i1];
for (int i = 0; i < jointCount; i++)
{
frame[toSkeleton[i]] = new XForm(
Vector3.Lerp(a[i].Pos, b[i].Pos, u),
MathQ.Normalize(Quaternion.Slerp(a[i].Rot, b[i].Rot, u)));
}
frames.Add(frame);
}
// NativeFps records the file's authored frame rate (1 / FrameTime): external frame
// ranges (Unity .meta clipAnimations) are expressed in it.
float nativeFps = frameTime > 0f ? (float)(1.0 / frameTime) : fps;
return new Clip("motion", fps, looping: false, frames, nativeFps);
}
// ---------------------------------------------------------------- calibration trim
/// <summary>A frame counts as rest-like only below this max-joint rotation angle vs the
/// identity-rotation bind (measured: calibration frames/ramps ≤ 24°, real edges ≥ 80°).</summary>
private const float CalibrationRestMaxDeg = 40f;
/// <summary>Absolute floor on the discontinuity out of the rest-like segment (measured:
/// calibration exits 25–177°, continuous real clip edges ≤ 8°).</summary>
private const float CalibrationJumpMinDeg = 20f;
/// <summary>The segment-exit discontinuity must also exceed this multiple of the clip's
/// median inter-frame delta — a clip idling near rest never trips this.</summary>
private const float CalibrationJumpTypicalRatio = 4f;
/// <summary>Longest rest-like calibration segment trimmed per clip end. Hard cuts are
/// 1 frame (CMU asf/amc exports); rest→motion blend ramps measure 2 rest-like frames
/// (a makehuman-retarget export). Longer rest-like leads are content, left alone.</summary>
private const int CalibrationMaxSegmentFrames = 4;
/// <summary>Absolute floor on a blend-ramp frame's delta for the ramp extension
/// (measured ramp deltas 15–25°/frame; settled motion ≤ 6°).</summary>
private const float CalibrationRampMinDeg = 10f;
/// <summary>Hard cap on the total trim per clip end (rest-like segment + blend ramp;
/// measured worst case 5 frames on the makehuman-retarget export).</summary>
private const int CalibrationMaxTrimFrames = 8;
/// <summary>
/// Length of the prepended (<paramref name="step"/> = +1, scanning from
/// <paramref name="edge"/> toward <paramref name="stop"/>) or appended (−1)
/// skeleton-calibration segment, 0 when there is none. The segment is a short run of
/// rest-like frames (≤ <see cref="CalibrationMaxSegmentFrames"/>) that exits into
/// NON-rest-like motion (≥ 2 frames of which must remain) through a discontinuity that
/// is large both absolutely and against the clip's typical inter-frame delta. When the
/// exit is a multi-frame blend RAMP rather than a hard cut (measured: 2 rest-like frames
/// then 22–25°/frame for 3 more), the ramp frames are consumed too — until the motion
/// settles to ordinary deltas — bounded by <see cref="CalibrationMaxTrimFrames"/> total.
/// See class remarks for the measured margins.
/// </summary>
private static int CalibrationSegmentLength(
XForm[][] native, int edge, int stop, int step, float typicalDeltaDeg)
{
int length = 0;
int f = edge;
while (f != stop && length <= CalibrationMaxSegmentFrames
&& MaxRotDeltaDeg(native[f], null) <= CalibrationRestMaxDeg)
{
length++;
f += step;
}
if (length is 0 or > CalibrationMaxSegmentFrames)
return 0;
// f = first frame past the rest-like segment; require a real (non-rest-like) clip
// of ≥ 2 frames beyond it and a calibration-grade cut between segment and motion.
if (f == stop || MaxRotDeltaDeg(native[f], null) <= CalibrationRestMaxDeg)
return 0;
float jump = MaxRotDeltaDeg(native[f - step], native[f]);
if (jump < CalibrationJumpMinDeg || jump < CalibrationJumpTypicalRatio * typicalDeltaDeg)
return 0;
// Blend-ramp extension: consume frames still moving at calibration-ramp speed until
// the motion settles (leaving ≥ 2 frames past the trim).
float rampFloor = MathF.Max(
CalibrationRampMinDeg, CalibrationJumpTypicalRatio * typicalDeltaDeg);
while (length < CalibrationMaxTrimFrames
&& f != stop && f + step != stop
&& MaxRotDeltaDeg(native[f], native[f + step]) >= rampFloor)
{
length++;
f += step;
}
return length;
}
/// <summary>Max joint rotation angle (degrees) between two decoded frames, or — when
/// <paramref name="b"/> is null — against the identity-rotation BVH bind rest.</summary>
private static float MaxRotDeltaDeg(XForm[] a, XForm[]? b)
{
float max = 0f;
for (int i = 0; i < a.Length; i++)
{
float angle = MathQ.AngleBetween(a[i].Rot, b is null ? Quaternion.Identity : b[i].Rot);
max = MathF.Max(max, angle);
}
return max * (180f / MathF.PI);
}
/// <summary>Median of the per-pair max joint rotation deltas over the clip's INTERIOR
/// consecutive frame pairs (both edge pairs excluded — they are the trim candidates).</summary>
private static float TypicalNeighborRotDeltaDeg(XForm[][] native)
{
int pairCount = native.Length - 3; // pairs (1,2) … (n-3, n-2)
if (pairCount <= 0)
return 0f;
var deltas = new float[pairCount];
for (int f = 0; f < pairCount; f++)
deltas[f] = MaxRotDeltaDeg(native[f + 1], native[f + 2]);
Array.Sort(deltas);
return deltas[pairCount / 2];
}
/// <summary>One joint's local transform from one motion row (see class remarks).</summary>
private static XForm EvaluateLocal(Joint joint, float[] row, float unitScale)
{
// Position channels replace the OFFSET; absent channels (or no position channels at
// all) fall back per Blender's semantics described in the class remarks.
Vector3 pos = joint.HasPos
? new Vector3(
joint.PosX >= 0 ? row[joint.PosX] : 0f,
joint.PosY >= 0 ? row[joint.PosY] : 0f,
joint.PosZ >= 0 ? row[joint.PosZ] : 0f)
: joint.Offset;
// R = R_chan1 * R_chan2 * R_chan3 (column-vector convention; degrees in the file).
var rot = Quaternion.Identity;
foreach (var (axis, column) in joint.Rot)
{
float radians = row[column] * (MathF.PI / 180f);
var axisVector = axis switch
{
0 => Vector3.UnitX,
1 => Vector3.UnitY,
_ => Vector3.UnitZ,
};
rot *= Quaternion.CreateFromAxisAngle(axisVector, radians);
}
return new XForm(pos * unitScale, MathQ.Normalize(rot));
}
// =====================================================================================
// tokenizer
// =====================================================================================
/// <summary>Whitespace token stream over the BVH text (BVH is line-format agnostic).</summary>
private sealed class TokenCursor
{
private readonly string[] _tokens;
private int _pos;
public TokenCursor(string text)
=> _tokens = text.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
public bool AtEnd => _pos >= _tokens.Length;
public bool PeekIs(string keywordUpper)
=> _pos < _tokens.Length &&
string.Equals(_tokens[_pos], keywordUpper, StringComparison.OrdinalIgnoreCase);
public string Next()
{
if (AtEnd)
throw new FormatException("BVH: unexpected end of file.");
return _tokens[_pos++];
}
public void ExpectKeyword(string keywordUpper)
{
string token = Next();
if (!string.Equals(token, keywordUpper, StringComparison.OrdinalIgnoreCase))
throw new FormatException($"BVH: expected '{keywordUpper}', found '{token}'.");
}
public int NextInt()
{
string token = Next();
if (!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value))
throw new FormatException($"BVH: expected an integer, found '{token}'.");
return value;
}
public float NextFloat()
{
string token = Next();
if (!float.TryParse(token, NumberStyles.Float, CultureInfo.InvariantCulture, out float value) ||
!float.IsFinite(value))
throw new FormatException($"BVH: expected a number, found '{token}'.");
return value;
}
}
}
Game
library
#nullable enable annotations
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using HumanoidRetargeter.Skeleton;
using SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;
namespace HumanoidRetargeter.Formats.Dmx;
/// <summary>Options for <see cref="DmxWriter.Write"/>.</summary>
public sealed class DmxWriteOptions
{
/// <summary>Model/clip name written into the DmeModel element (e.g. the sequence name).</summary>
public string Name { get; set; } = "";
/// <summary>Free-form provenance note written as the DmeDCCMakefile source name
/// (fbx2dmx writes the source .fbx path here).</summary>
public string SourceNote { get; set; } = "";
/// <summary>When true (default, matching fbx2dmx output) the file declares a Y-up axis
/// system; when false it declares Z-up. Data is written as-is either way.</summary>
public bool UpAxisY { get; set; } = true;
/// <summary>
/// Skeleton bone indices that get NO DmeChannel pair: the bones keep their DmeJoint and
/// bind (rest) transform, but no animation channels are written for them — the engine then
/// drives them itself (e.g. ConstraintDriven twist/helper bones, design §3). Null (default)
/// writes channels for every bone.
/// </summary>
public IReadOnlySet<int>? ChannelExcludedBones { get; set; }
}
/// <summary>
/// Writes an animation DMX in <c>keyvalues2_noids</c> text encoding, replicating the exact
/// element/attribute shape of fbx2dmx output (authoritative reference:
/// <c>dev/m0/ref_idlepose.dmx</c>): a root DmElement holding an inline DmeModel (joint GUID
/// refs + bind base state), a top-level DmeAnimationList with one DmeChannelsClip carrying a
/// position and an orientation channel per bone, and top-level DmeTransform/DmeJoint elements
/// the channels and joint lists reference by GUID. Output is fully deterministic: GUIDs are
/// MD5-derived from the options name and an element path, and export tags use fixed
/// placeholder strings.
/// </summary>
public static class DmxWriter
{
private const string Header = "<!-- dmx encoding keyvalues2_noids 4 format model 22 -->";
/// <summary>
/// Serializes <paramref name="clip"/> on <paramref name="skeleton"/> to DMX text.
/// Frames must contain one local transform per bone in skeleton order.
/// </summary>
/// <exception cref="ArgumentException">Thrown when the clip is empty or a frame's bone
/// count does not match the skeleton.</exception>
public static string Write(SkeletonModel skeleton, Clip clip, DmxWriteOptions options)
{
ArgumentNullException.ThrowIfNull(skeleton);
ArgumentNullException.ThrowIfNull(clip);
ArgumentNullException.ThrowIfNull(options);
if (clip.FrameCount == 0)
throw new ArgumentException("Clip has no frames.", nameof(clip));
for (var f = 0; f < clip.FrameCount; f++)
{
if (clip.Frames[f].Length != skeleton.Count)
throw new ArgumentException(
$"Frame {f} has {clip.Frames[f].Length} bone transforms, skeleton has {skeleton.Count}.",
nameof(clip));
}
var w = new Emitter();
var animListGuid = GuidString(options.Name, "animationList");
var jointGuids = new string[skeleton.Count];
var transformGuids = new string[skeleton.Count];
for (var i = 0; i < skeleton.Count; i++)
{
jointGuids[i] = GuidString(options.Name, "joint:" + skeleton[i].Name);
transformGuids[i] = GuidString(options.Name, "transform:" + skeleton[i].Name);
}
w.Raw(Header);
// ---- root DmElement -------------------------------------------------
w.BeginTopLevel("DmElement");
w.Attr("name", "string", "root");
w.BeginInlineAttr("skeleton", "DmeModel");
w.Attr("name", "string", options.Name);
w.BeginInlineAttr("transform", "DmeTransform");
w.Attr("position", "vector3", "0 0 0");
w.Attr("orientation", "quaternion", "0 0 0 1");
w.EndInlineAttr();
w.Attr("shape", "element", "");
w.Attr("visible", "bool", "1");
w.BeginArray("children");
var roots = new List<int>();
for (var i = 0; i < skeleton.Count; i++)
{
if (skeleton[i].ParentIndex < 0)
roots.Add(i);
}
for (var r = 0; r < roots.Count; r++)
w.ElementRef(jointGuids[roots[r]], last: r == roots.Count - 1);
w.EndArray();
w.BeginArray("jointList");
for (var i = 0; i < skeleton.Count; i++)
w.ElementRef(jointGuids[i], last: i == skeleton.Count - 1);
w.EndArray();
w.BeginArray("baseStates");
w.BeginArrayElement("DmeTransformList");
w.Attr("name", "string", "bind");
w.BeginArray("transforms");
for (var i = 0; i < skeleton.Count; i++)
{
w.BeginArrayElement("DmeTransform");
w.Attr("name", "string", skeleton[i].Name);
w.Attr("position", "vector3", Vec(skeleton[i].RestLocal));
w.Attr("orientation", "quaternion", Quat(skeleton[i].RestLocal));
w.EndArrayElement(last: i == skeleton.Count - 1);
}
w.EndArray();
w.EndArrayElement(last: true);
w.EndArray();
w.Attr("upAxis", "string", options.UpAxisY ? "Y" : "Z");
w.BeginInlineAttr("axisSystem", "DmeAxisSystem");
w.Attr("upAxis", "int", options.UpAxisY ? "2" : "3");
w.Attr("forwardParity", "int", "2");
w.Attr("coordSys", "int", "0");
w.EndInlineAttr();
w.Attr("animationList", "element", animListGuid);
w.EndInlineAttr(); // skeleton DmeModel
w.BeginInlineAttr("makefile", "DmeDCCMakefile");
w.Attr("name", "string", "makefile");
w.BeginArray("sources");
w.BeginArrayElement("DmeSource");
w.Attr("name", "string", options.SourceNote);
w.EndArrayElement(last: true);
w.EndArray();
w.EndInlineAttr();
// Deterministic placeholders — never wall-clock/user data, so output is reproducible.
w.BeginInlineAttr("exportTags", "DmeExportTags");
w.Attr("name", "string", "exportTags");
w.Attr("date", "string", "2026/01/01");
w.Attr("time", "string", "12:00:00 am");
w.Attr("user", "string", "retargeter");
w.Attr("machine", "string", "retargeter");
w.Attr("app", "string", "humanoid-retargeter");
w.Attr("appVersion", "string", "1.0");
w.Attr("cmdLine", "string", "humanoid-retargeter");
w.Attr("pwd", "string", "");
w.EndInlineAttr();
w.Attr("animationList", "element", animListGuid);
w.EndTopLevel();
// ---- DmeAnimationList ----------------------------------------------
w.BeginTopLevel("DmeAnimationList");
w.Attr("id", "elementid", animListGuid);
w.Attr("name", "string", "anim");
w.BeginArray("animations");
w.BeginArrayElement("DmeChannelsClip");
w.Attr("name", "string", "anim");
w.BeginInlineAttr("timeFrame", "DmeTimeFrame");
w.Attr("start", "time", Time(0.0));
w.Attr("duration", "time", Time((clip.FrameCount - 1) / (double)clip.Fps));
w.Attr("offset", "time", Time(0.0));
w.Attr("scale", "float", "1");
w.EndInlineAttr();
w.Attr("color", "color", "0 0 0 0");
w.Attr("text", "string", "");
w.Attr("mute", "bool", "0");
w.BeginArray("trackGroups");
w.EndArray();
w.Attr("displayScale", "float", "1");
var channelBones = new List<int>(skeleton.Count);
for (var i = 0; i < skeleton.Count; i++)
{
if (options.ChannelExcludedBones is null || !options.ChannelExcludedBones.Contains(i))
channelBones.Add(i);
}
w.BeginArray("channels");
for (var n = 0; n < channelBones.Count; n++)
{
var i = channelBones[n];
WriteChannel(w, skeleton, clip, i, transformGuids[i], position: true, last: false);
WriteChannel(w, skeleton, clip, i, transformGuids[i], position: false,
last: n == channelBones.Count - 1);
}
w.EndArray();
w.Attr("frameRate", "int",
((int)MathF.Round(clip.Fps)).ToString(CultureInfo.InvariantCulture));
w.EndArrayElement(last: true);
w.EndArray();
w.EndTopLevel();
// ---- top-level channel-target DmeTransforms (rest values) -----------
for (var i = 0; i < skeleton.Count; i++)
{
w.BeginTopLevel("DmeTransform");
w.Attr("id", "elementid", transformGuids[i]);
w.Attr("name", "string", skeleton[i].Name);
w.Attr("position", "vector3", Vec(skeleton[i].RestLocal));
w.Attr("orientation", "quaternion", Quat(skeleton[i].RestLocal));
w.EndTopLevel();
}
// ---- top-level DmeJoints --------------------------------------------
for (var i = 0; i < skeleton.Count; i++)
{
w.BeginTopLevel("DmeJoint");
w.Attr("id", "elementid", jointGuids[i]);
w.Attr("name", "string", skeleton[i].Name);
w.Attr("transform", "element", transformGuids[i]);
w.Attr("shape", "element", "");
w.Attr("visible", "bool", "1");
w.BeginArray("children");
var children = new List<int>();
for (var c = 0; c < skeleton.Count; c++)
{
if (skeleton[c].ParentIndex == i)
children.Add(c);
}
for (var c = 0; c < children.Count; c++)
w.ElementRef(jointGuids[children[c]], last: c == children.Count - 1);
w.EndArray();
w.EndTopLevel();
}
return w.ToString();
}
/// <summary>
/// Deterministic element GUID: MD5 over <c>"<name>\n<path>"</c> (UTF-8)
/// interpreted as <see cref="Guid"/> bytes. Exposed so tests can verify the scheme.
/// </summary>
public static Guid ElementGuid(string name, string path)
=> new(MD5.HashData(Encoding.UTF8.GetBytes(name + "\n" + path)));
private static string GuidString(string name, string path)
=> ElementGuid(name, path).ToString("D", CultureInfo.InvariantCulture);
// ---------------------------------------------------------------- channels
private static void WriteChannel(Emitter w, SkeletonModel skeleton, Clip clip, int bone,
string transformGuid, bool position, bool last)
{
var logClass = position ? "DmeVector3Log" : "DmeQuaternionLog";
var layerClass = position ? "DmeVector3LogLayer" : "DmeQuaternionLogLayer";
var logName = position ? "vector3 log" : "quaternion log";
w.BeginArrayElement("DmeChannel");
w.Attr("name", "string", skeleton[bone].Name + (position ? "_p" : "_o"));
w.Attr("fromElement", "element", "");
w.Attr("fromAttribute", "string", "");
w.Attr("fromIndex", "int", "0");
w.Attr("toElement", "element", transformGuid);
w.Attr("toAttribute", "string", position ? "position" : "orientation");
w.Attr("toIndex", "int", "0");
w.Attr("mode", "int", "3");
w.BeginInlineAttr("log", logClass);
w.Attr("name", "string", logName);
w.BeginArray("layers");
w.BeginArrayElement(layerClass);
w.Attr("name", "string", logName);
w.BeginArray("times", "time_array");
for (var f = 0; f < clip.FrameCount; f++)
w.ArrayValue(Time(f / (double)clip.Fps), last: f == clip.FrameCount - 1);
w.EndArray();
w.BeginArray("curvetypes", "int_array");
w.EndArray();
w.BeginArray("values", position ? "vector3_array" : "quaternion_array");
// Orientation values are hemisphere-aligned on the fly (q and -q are the same
// rotation, but the engine interpolates between DMX samples numerically — see
// QuaternionContinuity). The clip itself is never mutated.
var prev = System.Numerics.Quaternion.Identity;
for (var f = 0; f < clip.FrameCount; f++)
{
var x = clip.Frames[f][bone];
string value;
if (position)
{
value = Vec(x);
}
else
{
var q = x.Rot;
if (f > 0 && System.Numerics.Quaternion.Dot(prev, q) < 0f)
q = System.Numerics.Quaternion.Negate(q);
prev = q;
value = Quat(q);
}
w.ArrayValue(value, last: f == clip.FrameCount - 1);
}
w.EndArray();
w.EmptyBinaryAttr("compressed");
w.EndArrayElement(last: true);
w.EndArray(); // layers
w.Attr("curveinfo", "element", "");
w.Attr("usedefaultvalue", "bool", "0");
w.Attr("defaultvalue", position ? "vector3" : "quaternion", position ? "0 0 0" : "0 0 0 1");
w.BeginArray("bookmarksX", "time_array");
w.EndArray();
w.BeginArray("bookmarksY", "time_array");
w.EndArray();
w.BeginArray("bookmarksZ", "time_array");
w.EndArray();
w.EndInlineAttr(); // log
w.EndArrayElement(last);
}
// ---------------------------------------------------------------- formatting
/// <summary>fbx2dmx float style: up to 10 decimal places, trailing zeros stripped,
/// invariant culture, negative zero normalized.</summary>
private static string F(float value)
{
if (value == 0f)
return "0";
return ((double)value).ToString("0.##########", CultureInfo.InvariantCulture);
}
private static string Time(double seconds)
=> seconds.ToString("0.0000", CultureInfo.InvariantCulture);
private static string Vec(in Maths.XForm x)
=> $"{F(x.Pos.X)} {F(x.Pos.Y)} {F(x.Pos.Z)}";
private static string Quat(in Maths.XForm x) => Quat(x.Rot);
private static string Quat(in System.Numerics.Quaternion q)
=> $"{F(q.X)} {F(q.Y)} {F(q.Z)} {F(q.W)}";
// ---------------------------------------------------------------- emitter
/// <summary>
/// Low-level keyvalues2 text emitter reproducing fbx2dmx layout quirks: CRLF endings,
/// tab indentation, a trailing space after array-typed attribute names, and an
/// indentation-only line after every inline element attribute closes.
/// </summary>
private sealed class Emitter
{
private readonly StringBuilder _sb = new();
private int _indent;
public void Raw(string text)
{
_sb.Append(text).Append("\r\n");
}
private void Line(string text)
{
_sb.Append('\t', _indent).Append(text).Append("\r\n");
}
public void Attr(string name, string type, string value)
=> Line($"\"{name}\" \"{type}\" \"{value}\"");
public void BeginTopLevel(string className)
{
Line($"\"{className}\"");
Line("{");
_indent++;
}
public void EndTopLevel()
{
_indent--;
Line("}");
_sb.Append("\r\n"); // blank separator after every top-level element (incl. the last)
}
public void BeginInlineAttr(string name, string className)
{
Line($"\"{name}\" \"{className}\"");
Line("{");
_indent++;
}
public void EndInlineAttr()
{
_indent--;
Line("}");
Line(""); // indentation-only line, as fbx2dmx emits
}
public void BeginArrayElement(string className)
{
Line($"\"{className}\"");
Line("{");
_indent++;
}
public void EndArrayElement(bool last)
{
_indent--;
Line(last ? "}" : "},");
}
public void BeginArray(string name, string type = "element_array")
{
Line($"\"{name}\" \"{type}\" ");
Line("[");
_indent++;
}
public void EndArray()
{
_indent--;
Line("]");
}
public void ElementRef(string guid, bool last)
=> Line($"\"element\" \"{guid}\"" + (last ? "" : ","));
public void ArrayValue(string value, bool last)
=> Line($"\"{value}\"" + (last ? "" : ","));
public void EmptyBinaryAttr(string name)
{
Line($"\"{name}\" \"binary\" ");
Line("\"");
Line("\"");
}
public override string ToString() => _sb.ToString();
}
}
Game
library
#nullable enable annotations
using System;
using System.Collections.Generic;
using System.Numerics;
using HumanoidRetargeter.Maths;
namespace HumanoidRetargeter.Formats.Fbx;
using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)
/// <summary>
/// Repairs FBX files that were exported MID-POSE: their node transforms (Lcl
/// Translation/Rotation) hold an animation snapshot while the true skeleton bind lives in
/// the file's Pose/BindPose section. Engines that build the skeleton from node transforms
/// (s&box does; the FBX SDK's own samples do) then import a posed "bind" — the skin
/// stays self-consistent so the model LOOKS fine at rest, but every anatomical assumption
/// about the skeleton (leg chains point down, hands mirror) is silently wrong and
/// retargeted motion comes out mangled on exactly the posed bones. Found in the wild on
/// Auto-Rig Pro exports whose IK'd hands/feet were left posed (one leg at hip height).
/// </summary>
public static class FbxBindPoseFixer
{
/// <summary>Bones whose node transform is further than this (native units, as a
/// fraction of skeleton height) from their BindPose matrix count as posed.</summary>
private const float PositionToleranceOfHeight = 0.01f;
/// <summary>Rotation disagreement (degrees) that counts as posed.</summary>
private const float RotationToleranceDeg = 2.0f;
/// <summary>
/// Detects the mid-pose condition and rewrites node transforms to the BindPose. Returns
/// null when the file needs no repair (no BindPose section, or node transforms already
/// agree with it); otherwise the repaired file bytes.
/// <paramref name="report"/> always describes what was found.
/// </summary>
public static byte[]? TryFix(byte[] fbx, out string report)
{
ArgumentNullException.ThrowIfNull(fbx);
FbxNode root;
FbxScene scene;
try
{
root = FbxTokenizer.Parse(fbx);
scene = FbxScene.Build(root);
}
catch (FormatException e)
{
report = $"not parseable ({e.Message})";
return null;
}
if (scene.BindPose.Count == 0)
{
report = "no BindPose section";
return null;
}
// Evaluate the ORIGINAL node-transform FK, roots first.
var originalWorld = new Dictionary<long, Matrix4x4>();
var order = new List<FbxObject>();
foreach (var model in scene.Models)
VisitModel(model, scene, originalWorld, order);
// Skeleton height (native units) for the position tolerance.
float minUp = float.MaxValue, maxUp = float.MinValue;
foreach (var w in originalWorld.Values)
{
var t = w.Translation;
float up = MathF.Max(MathF.Abs(t.Y), MathF.Abs(t.Z));
minUp = MathF.Min(minUp, up);
maxUp = MathF.Max(maxUp, up);
}
float posTolerance = MathF.Max(0.0001f, (maxUp - minUp) * PositionToleranceOfHeight);
// Any bone posed away from its bind?
int posedCount = 0;
foreach (var model in order)
{
if (!scene.BindPose.TryGetValue(model.Id, out var bind))
continue;
var fk = FbxTransform.ToRigid(originalWorld[model.Id]);
var target = FbxTransform.ToRigid(bind);
if ((fk.Pos - target.Pos).Length() > posTolerance
|| MathQ.AngleBetween(fk.Rot, target.Rot) > RotationToleranceDeg * MathF.PI / 180f)
{
posedCount++;
}
}
if (posedCount == 0)
{
report = $"node transforms match the BindPose ({scene.BindPose.Count} entries)";
return null;
}
// Rewrite every BindPose-backed model's local transform so FK lands on the bind.
// correctedWorld carries the repair down the hierarchy for bones WITHOUT a
// BindPose entry (helpers keep their original locals under corrected parents).
var correctedWorld = new Dictionary<long, Matrix4x4>();
int patched = 0, skipped = 0;
foreach (var model in order)
{
var parent = model.ModelParent;
Matrix4x4 parentWorld = parent is not null && correctedWorld.TryGetValue(parent.Id, out var pw)
? pw
: Matrix4x4.Identity;
if (!scene.BindPose.TryGetValue(model.Id, out var bindWorld))
{
// No bind info: keep the original local under the (possibly corrected) parent.
var transform = FbxTransform.FromModel(scene, model);
correctedWorld[model.Id] = transform.LocalMatrixDefault() * parentWorld;
continue;
}
if (!Matrix4x4.Invert(parentWorld, out var invParent))
{
correctedWorld[model.Id] = bindWorld;
skipped++;
continue;
}
// Row-vector: World = Local · ParentWorld ⇒ Local = World · ParentWorld⁻¹.
var desiredLocal = FbxTransform.ToRigid(bindWorld * invParent);
if (PatchModelLocal(scene, model, desiredLocal))
patched++;
else
skipped++;
// Children FK from the ACTUAL bind either way (unpatchable bones are rare and
// their children still deserve correct parent frames).
correctedWorld[model.Id] = bindWorld;
}
report = $"{posedCount} bones were exported mid-pose; repaired {patched}"
+ (skipped > 0 ? $", {skipped} left as-is (pivots/scale beyond the safe rewrite)" : "");
if (patched == 0)
return null;
return FbxBinaryWriter.Write(root);
}
private static void VisitModel(
FbxObject model, FbxScene scene,
Dictionary<long, Matrix4x4> world, List<FbxObject> order)
{
if (world.ContainsKey(model.Id))
return;
Matrix4x4 parentWorld = Matrix4x4.Identity;
if (model.ModelParent is { } parent)
{
VisitModel(parent, scene, world, order);
parentWorld = world[parent.Id];
}
var transform = FbxTransform.FromModel(scene, model);
world[model.Id] = transform.LocalMatrixDefault() * parentWorld;
order.Add(model);
}
// ------------------------------------------------------------------ patching
/// <summary>
/// Rewrites one Model's Lcl Translation/Rotation so its local evaluates to
/// <paramref name="desiredLocal"/>. Verified by re-evaluating through the full FBX
/// transform formula — models using pivots/offsets/scale that the rewrite cannot
/// express are left untouched (returns false).
/// </summary>
private static bool PatchModelLocal(FbxScene scene, FbxObject model, XForm desiredLocal)
{
var transform = FbxTransform.FromModel(scene, model);
// R_total = Pre · R · Post⁻¹ ⇒ R = Pre⁻¹ · R_total · Post
var r = MathQ.Normalize(
Quaternion.Conjugate(transform.PreRotation)
* desiredLocal.Rot
* transform.PostRotation);
var eulerDeg = QuaternionToEulerDegrees(r, transform.RotationOrder);
// Full-formula verification (catches pivots, scale, decomposition branches).
var check = new FbxTransform
{
LclTranslation = desiredLocal.Pos,
LclRotationDeg = eulerDeg,
LclScaling = transform.LclScaling,
PreRotation = transform.PreRotation,
PostRotation = transform.PostRotation,
RotationOffset = transform.RotationOffset,
RotationPivot = transform.RotationPivot,
ScalingOffset = transform.ScalingOffset,
ScalingPivot = transform.ScalingPivot,
RotationOrder = transform.RotationOrder,
};
var evaluated = FbxTransform.ToRigid(check.LocalMatrixDefault());
float posScale = MathF.Max(1f, desiredLocal.Pos.Length());
if ((evaluated.Pos - desiredLocal.Pos).Length() > 0.001f * posScale
|| MathQ.AngleBetween(evaluated.Rot, desiredLocal.Rot) > 0.1f * MathF.PI / 180f)
{
return false;
}
SetProperty70(model.Node, "Lcl Translation", "Lcl Translation", "A",
desiredLocal.Pos.X, desiredLocal.Pos.Y, desiredLocal.Pos.Z);
SetProperty70(model.Node, "Lcl Rotation", "Lcl Rotation", "A",
eulerDeg.X, eulerDeg.Y, eulerDeg.Z);
return true;
}
/// <summary>Sets (or adds) a 3-double P entry in the node's Properties70 block.</summary>
private static void SetProperty70(
FbxNode modelNode, string name, string type, string flags,
double x, double y, double z)
{
var block = modelNode.Child("Properties70");
if (block is null)
{
block = new FbxNode("Properties70");
modelNode.Children.Insert(0, block);
}
foreach (var p in block.ChildrenNamed("P"))
{
if (p.Properties.Count >= 1 && p.Properties[0] is string n && n == name)
{
// Values live at indices 4.. — replace, extending if the entry was short.
while (p.Properties.Count < 7)
p.Properties.Add(0.0);
p.Properties[4] = x;
p.Properties[5] = y;
p.Properties[6] = z;
return;
}
}
var entry = new FbxNode("P");
entry.Properties.Add(name);
entry.Properties.Add(type);
entry.Properties.Add("");
entry.Properties.Add(flags);
entry.Properties.Add(x);
entry.Properties.Add(y);
entry.Properties.Add(z);
block.Children.Add(entry);
}
// ------------------------------------------------------------------ euler decomposition
/// <summary>
/// Decomposes a quaternion into FBX euler degrees for the given RotationOrder, the
/// exact inverse of <see cref="FbxTransform.EulerDegreesToQuaternion"/>. Tait-Bryan
/// extraction on the column-convention rotation matrix.
/// </summary>
public static Vector3 QuaternionToEulerDegrees(Quaternion q, int order)
{
// Column-convention matrix C (v' = C·v): C = transpose of System.Numerics' row form.
var m = Matrix4x4.CreateFromQuaternion(q);
// C[r,c]: row r, column c.
float c00 = m.M11, c01 = m.M21, c02 = m.M31;
float c10 = m.M12, c11 = m.M22, c12 = m.M32;
float c20 = m.M13, c21 = m.M23, c22 = m.M33;
const float radToDeg = 180f / MathF.PI;
float a, b, c;
switch (order)
{
case 0: // XYZ: C = Rz·Ry·Rx
b = MathF.Asin(Math.Clamp(-c20, -1f, 1f));
a = MathF.Atan2(c21, c22);
c = MathF.Atan2(c10, c00);
return new Vector3(a * radToDeg, b * radToDeg, c * radToDeg);
case 1: // XZY: C = Ry·Rz·Rx
b = MathF.Asin(Math.Clamp(c10, -1f, 1f));
a = MathF.Atan2(-c12, c11);
c = MathF.Atan2(-c20, c00);
return new Vector3(a * radToDeg, c * radToDeg, b * radToDeg);
case 2: // YZX: C = Rx·Rz·Ry
b = MathF.Asin(Math.Clamp(-c01, -1f, 1f));
a = MathF.Atan2(c02, c00);
c = MathF.Atan2(c21, c11);
return new Vector3(c * radToDeg, a * radToDeg, b * radToDeg);
case 3: // YXZ: C = Rz·Rx·Ry
b = MathF.Asin(Math.Clamp(c21, -1f, 1f));
a = MathF.Atan2(-c20, c22);
c = MathF.Atan2(-c01, c11);
return new Vector3(b * radToDeg, a * radToDeg, c * radToDeg);
case 4: // ZXY: C = Ry·Rx·Rz
b = MathF.Asin(Math.Clamp(-c12, -1f, 1f));
a = MathF.Atan2(c02, c22);
c = MathF.Atan2(c10, c11);
return new Vector3(b * radToDeg, a * radToDeg, c * radToDeg);
case 5: // ZYX: C = Rx·Ry·Rz
case 6: // eSphericXYZ treated as XYZ on read; mirror that here
default:
if (order == 5)
{
b = MathF.Asin(Math.Clamp(c02, -1f, 1f));
a = MathF.Atan2(-c01, c00);
c = MathF.Atan2(-c12, c22);
return new Vector3(c * radToDeg, b * radToDeg, a * radToDeg);
}
goto case 0;
}
}
}
Game
library
#nullable enable annotations
using System;
using System.Collections.Generic;
using HumanoidRetargeter.Cleanup;
using HumanoidRetargeter.Formats;
using HumanoidRetargeter.Mapping;
using HumanoidRetargeter.Solve;
using HumanoidRetargeter.Target;
namespace HumanoidRetargeter;
/// <summary>Which solver retargets a request's clips (design §10).</summary>
public enum SolverKind
{
/// <summary>The deterministic <see cref="Solve.GeometricSolver"/> (default; better
/// wherever a role mapping exists).</summary>
Geometric,
/// <summary>The experimental skeleton-agnostic deep-learning solver
/// (<see cref="Dl.DlSolver"/>, SAME pretrained checkpoint) — the no-profile fallback.
/// Requires <see cref="RetargetTargetSpec.DlWeights"/>; ignores per-role mapping
/// (only hips/alignment heuristics consult it) and leaves fingers at rest.</summary>
DeepLearning,
}
/// <summary>
/// One source animation file to retarget (engine-agnostic: bytes in, no file IO). Every
/// request runs its OWN profile detection, so a single batch may mix Mixamo + ActorCore +
/// BVH sources — unless <see cref="MappingOverride"/> supplies a mapping explicitly.
/// </summary>
public sealed class RetargetRequest
{
/// <summary>Solver choice for this request's clips. <see cref="SolverKind.DeepLearning"/>
/// requires the batch's <see cref="RetargetTargetSpec.DlWeights"/> to be set; the
/// conversion fails per-clip with a clear error otherwise.</summary>
public SolverKind Solver { get; init; } = SolverKind.Geometric;
/// <summary>Raw bytes of the source file (.fbx, .bvh, .glb, .gltf, .vrm, .anm or .an5).</summary>
public required byte[] SourceData { get; init; }
/// <summary>
/// Source file name (used for the report and DMX provenance). The extension drives the
/// format choice (<c>.fbx</c> / <c>.bvh</c> / <c>.glb</c> / <c>.gltf</c> / <c>.vrm</c> —
/// a VRM is a glTF container whose authored humanoid bone map becomes the mapping — /
/// <c>.anm</c> / <c>.an5</c> RenderWare animations, which additionally need
/// <see cref="SkeletonData"/>); when the extension is unknown the content is sniffed
/// (FBX binary magic / "FBXHeaderExtension" / BVH "HIERARCHY" / GLB 'glTF' magic /
/// glTF JSON / RenderWare 0x1B animation chunk).
/// </summary>
public required string SourceFileName { get; init; }
/// <summary>
/// Raw bytes of a companion SKELETON file for formats whose animation files carry no
/// skeleton of their own: RenderWare <c>.anm</c>/<c>.an5</c> sources require the
/// character model's <c>.dff</c> here (callers resolve the file — e.g. a .dff sitting
/// next to the animation; the facade does no file IO). Ignored by self-contained
/// formats. A RenderWare request without it fails with an instructive error.
/// </summary>
public byte[]? SkeletonData { get; init; }
/// <summary>
/// Caller-supplied identity of this request, echoed verbatim on every produced
/// <see cref="ClipResult.SourceId"/> so callers can join results back to their own
/// entries unambiguously (e.g. the editor window passes the FULL file path here, since
/// two files in different folders may share the same <see cref="SourceFileName"/>).
/// Null = <see cref="SourceFileName"/>.
/// </summary>
public string? SourceId { get; init; }
/// <summary>
/// Import sample rate the source clips are resampled to (BVH native frames / FBX curves
/// are evaluated on this grid). Null = the importer default (30 fps).
/// </summary>
public float? SampleFps { get; init; }
/// <summary>
/// Restricts the conversion to ONE take of the source file (0-based index into the
/// imported scene's clips). Null = convert all takes. Out of range fails the request's
/// clip result with a clear error (the batch continues). UI listings that expand a
/// multi-take file into one entry per take submit one request per selected take.
/// When <see cref="ClipDefinitions"/> is set this index addresses the DEFINITIONS
/// instead (each definition is what a UI row represents then).
/// </summary>
public int? TakeIndex { get; init; }
/// <summary>
/// Optional external clip definitions, parsed from a Unity <c><file>.fbx.meta</c>
/// sidecar (<see cref="UnityMeta.ParseClipAnimations"/>): Unity animation packs ship FBX
/// files whose clips are sub-ranges of ONE source timeline. When set (non-empty), the
/// conversion produces one output clip per definition instead of one per take: the
/// definition's take (matched by <see cref="ExternalClipDef.TakeName"/>, falling back to
/// the file's first take) is sliced to the definition's native-frame range
/// (<see cref="UnityMeta.Slice"/>), named <see cref="ExternalClipDef.Name"/> (sanitized
/// like take names, collision-suffixed across the batch) and looped per
/// <see cref="ExternalClipDef.Loop"/> unless <see cref="LoopingOverride"/> is set.
/// <see cref="TakeIndex"/> then indexes INTO this list. Null = no definitions.
/// </summary>
public IReadOnlyList<ExternalClipDef>? ClipDefinitions { get; init; }
/// <summary>
/// UI-supplied mapping (manual mapping table or a user preset loaded Editor-side).
/// Null = auto-detect per request: preset profiles via <see cref="ProfileDetector"/>,
/// then the <see cref="AutoMapper"/> as best-effort fallback.
/// </summary>
public MappingResult? MappingOverride { get; init; }
/// <summary>Solver tunables (hip scales, finger transfer). ClipIndex/ClipName are managed
/// by the pipeline per take and ignored here. Null = defaults.</summary>
public SolveOptions? Solve { get; init; }
/// <summary>
/// Root-motion handling. <see cref="RootMotionMode.Extract"/> on a target without a
/// dedicated animated root bone (the s&box rig: pelvis is parentless, root_IK is
/// IkBaked) leaves the frames untouched and instead sets the ExtractMotion flag on the
/// clip's vmdl AnimFile entry — Source 2's compile-time extraction replaces the missing
/// bone-level extraction. <see cref="RootMotionMode.InPlace"/> always operates on the
/// hips directly.
/// </summary>
public RootMotionMode RootMotion { get; init; } = RootMotionMode.Off;
/// <summary>Run the Kovar foot-plant cleanup pass on the solved frames (default on).</summary>
public bool FootPlantCleanup { get; init; } = true;
/// <summary>
/// Copy the source clip's per-frame LOCAL translations onto same-named target bones
/// (hips and its ancestors excluded — trajectory stays solver-owned). For SAME-RIG
/// conversions of authored takes (a target FBX's own embedded animations): the solver
/// pins every non-hips bone to its rest translation, silently dropping a Biped take's
/// animated spine/thigh translations (~19cm of authored body sway on a death fall).
/// Meaningless across different rigs — leave off (default) for real retargets.
/// </summary>
public bool PreserveSourceTranslations { get; init; }
/// <summary>
/// Optional arm end-effector IK pass pulling the wrists onto limb-length-normalized
/// source hand positions. Default OFF: the geometric solver already matches anatomical
/// directions, so arm IK only helps reach-critical work (props, contact poses) and can
/// otherwise disturb elbow styling.
/// </summary>
public bool ArmEffectorIk { get; init; }
/// <summary>
/// Generate <c>AE_FOOTSTEP</c> AnimEvent nodes on each produced clip's vmdl AnimFile
/// entry (default OFF). After solving and cleanup, foot-plant intervals are detected on
/// the SOLVED target clip (<see cref="Cleanup.FootPlant.DetectPlantIntervals"/>); each
/// plant's start frame is a touchdown and becomes one footstep event, in the exact node
/// shape the shipped citizen data uses (see <see cref="Target.FootstepEvents"/>).
/// Skipped (with a report note) when the target rig lacks complete leg chains.
/// </summary>
public bool GenerateFootstepEvents { get; init; }
/// <summary>
/// Additionally produce a mirrored twin of every converted clip (default OFF), named
/// <c><clip>_M</c> (collision-suffixed across the batch as usual). Mirroring runs
/// in TARGET space on the solved clip (<see cref="Solve.ClipMirror"/>): left/right role
/// bone channels swap and everything is reflected across the target character's sagittal
/// plane; IK-baked helper bones are re-baked from the mirrored body afterwards.
/// </summary>
public bool CreateMirroredVariant { get; init; }
/// <summary>
/// Additionally register an additive (delta) twin of every converted clip in the
/// generated/augmented vmdl (default OFF), named <c><clip>_delta</c> (the shipped
/// citizen naming; collision-suffixed across the batch as usual). The twin is a second
/// AnimFile entry REUSING the clip's DMX with an <c>AnimSubtract</c> child
/// (<c>anim_name</c> = the base sequence, <c>frame</c> = 0) — exactly the shipped
/// <c>IdleLayer_01</c>/<c>IdleLayer_01_delta</c> pattern, where resourcecompiler
/// subtracts the reference frame at compile time (no frame math happens here). The
/// resulting <c>_delta</c> sequence is what s&box layered animation additively
/// blends on top of a base pose.
/// </summary>
public bool CreateAdditiveVariant { get; init; }
/// <summary>Output clip name override; with multiple takes an index suffix is appended.
/// Null = the source take name.</summary>
public string? ClipNameOverride { get; init; }
/// <summary>Force the looping flag on the output sequence(s); null = the source clip's flag.</summary>
public bool? LoopingOverride { get; init; }
}
/// <summary>
/// Axis/unit convention of a <see cref="RetargetTargetSpec"/>'s rig data — drives the DMX
/// axis-system declaration, foot-plant threshold units, and the editor preview's
/// rig-space → engine-space conversion.
/// </summary>
public enum TargetUpAxis
{
/// <summary>
/// The s&box source convention: rig authored in centimeters, Y-up (the shipped
/// citizen rig, FBX targets). The vmdl's ScaleAndMirror 0.3937 + resourcecompiler's
/// Y-up→Z-up conversion take it to engine space at compile time. Default.
/// </summary>
YUpCm,
/// <summary>
/// Engine space already: rig read from a compiled model's <c>Model.Bones</c>
/// (inches, Z-up). The DMX declares a Z-up axis system so the compiler performs no
/// further axis conversion.
/// </summary>
ZUpEngine,
/// <summary>
/// A Z-up rig authored in centimeters: FBX targets whose GlobalSettings declare a Z
/// up-axis (UE and 3ds Max exports; Maya/Blender exports are Y-up). The DMX declares
/// Z-up (no compile-time rotation — the mesh source is in the same Z-up space) while
/// the vmdl's ScaleAndMirror 0.3937 still converts cm→inches. Without this, a Z-up
/// FBX target compiles lying on its back.
/// </summary>
ZUpCm,
}
/// <summary>
/// The conversion target shared by all requests of one <see cref="Retargeter.Convert"/> /
/// <see cref="Retargeter.ConvertBatch"/> call: the rig plus the vmdl generation parameters.
/// </summary>
public sealed class RetargetTargetSpec
{
/// <summary>The s&box-source → engine-units vmdl scale (cm rigs like the citizen).</summary>
public const float SboxSourceScale = 0.3937f;
/// <summary>The committed asset path of the s&box human male model.</summary>
public const string SboxHumanMalePath = "models/citizen_human/citizen_human_male.vmdl";
/// <summary>The committed asset path of the classic (4-finger) s&box citizen model.</summary>
public const string SboxCitizenPath = "models/citizen/citizen.vmdl";
/// <summary>Target rig (skeleton + bone classes + roles).</summary>
public required TargetRig Rig { get; set; }
/// <summary>ModelModifier_ScaleAndMirror scale written into standalone vmdls:
/// <c>0.3937</c> for cm-authored s&box-source rigs, <c>1.0</c> for engine-unit rigs
/// (the modifier node is omitted at 1.0).</summary>
public required float VmdlScale { get; init; }
/// <summary>base_model_name of generated standalone vmdls (the model that owns the mesh).</summary>
public string BaseModelPath { get; init; } = "";
/// <summary>
/// Assets-relative mesh source file (e.g. an <c>.fbx</c>) embedded in generated
/// standalone vmdls as a <c>RenderMeshList/RenderMeshFile</c> node. Custom FBX targets
/// have no compiled base model to point <see cref="BaseModelPath"/> at — without a mesh
/// source their standalone vmdl compiles into an EMPTY model (0 bones, 0 sequences) and
/// playing it does nothing. Callers own copying the file into the project (this type
/// does no IO); settable so the editor can fill it at convert time once the output
/// folder is known. Empty (default) = no mesh node.
/// </summary>
public string MeshFilePath { get; set; } = "";
/// <summary>
/// Import scale of <see cref="MeshFilePath"/> (raw mesh-file units → the target
/// skeleton's units). resourcecompiler reads mesh files' raw values ignoring their unit
/// metadata, while the importer normalizes the target skeleton to centimeters — a
/// meters-authored FBX therefore needs 100 here (the importer's recorded
/// source-unit→cm factor) for the mesh to match the animation skeleton.
/// </summary>
public float MeshImportScale { get; set; } = 1.0f;
/// <summary>
/// Material remaps written into generated standalone vmdls as a
/// MaterialGroupList/DefaultMaterialGroup (bare mesh material reference → assets-relative
/// vmat path, e.g. <c>"mi_dante_head.vmat" → "animations/retargeted/mi_dante_head.vmat"</c>).
/// FBX materials carry bare names the compiler cannot resolve as resource paths
/// ("Trying to load an illegal resource name X.vmat"); this remap table — the same
/// mechanism the shipped citizen vmdl uses — points them at real files. Null/empty =
/// no material group node (default).
/// </summary>
public IReadOnlyDictionary<string, string>? MaterialRemaps { get; set; }
/// <summary>
/// Additional AnimFile entries appended to generated/augmented vmdls verbatim —
/// the target FBX's OWN embedded animations (an FBX with an animation on it must keep
/// that animation when new ones are retargeted onto it; the AnimFile references the
/// FBX directly, exactly like the shipped citizen animation list references its
/// Citizen@*.fbx files, so the import is lossless). Augmentation skips entries the
/// existing vmdl already carries (idempotent re-runs). Null/empty = none (default).
/// </summary>
public IReadOnlyList<Target.AnimEntry>? ExtraAnimFiles { get; set; }
/// <summary>default_root_bone_name of the generated AnimationList (also the bone vmdl
/// ExtractMotion nodes operate on).</summary>
public string DefaultRootBone { get; set; } = "pelvis";
/// <summary>
/// Axis/unit convention of <see cref="Rig"/>. <see cref="TargetUpAxis.YUpCm"/> (default)
/// for cm Y-up source-space rigs (DMX declares Y-up, compiler converts);
/// <see cref="TargetUpAxis.ZUpEngine"/> for rigs read from compiled engine models
/// (DMX declares Z-up so no double conversion happens at compile, and cm-tuned cleanup
/// thresholds are rescaled to inches).
/// </summary>
public TargetUpAxis UpAxis { get; init; } = TargetUpAxis.YUpCm;
/// <summary>
/// Raw bytes of the committed SAME weight blob
/// (<c>Assets/humanoid_retargeter/dl/same_v1.weights</c>; callers do the file IO).
/// Required only when a request selects <see cref="SolverKind.DeepLearning"/>; the
/// solver instance is built once per batch from these bytes.
/// </summary>
public byte[]? DlWeights { get; init; }
/// <summary>
/// The shipped s&box default target: rig parsed from the committed
/// <c>Assets/humanoid_retargeter/target_rig_sbox.json</c> text (callers do the file IO),
/// 0.3937 vmdl scale, citizen human male base model, pelvis root. Pass the committed
/// SAME weight bytes as <paramref name="dlWeights"/> to enable the deep-learning solver.
/// </summary>
public static RetargetTargetSpec SboxDefault(string targetRigJson, byte[]? dlWeights = null) => new()
{
Rig = TargetRig.SboxDefault(targetRigJson),
VmdlScale = SboxSourceScale,
BaseModelPath = SboxHumanMalePath,
DefaultRootBone = "pelvis",
DlWeights = dlWeights,
};
/// <summary>
/// The classic (4-finger) s&box citizen target: rig parsed from the committed
/// <c>Assets/humanoid_retargeter/target_rig_sbox_citizen.json</c> text (callers do the
/// file IO), 0.3937 vmdl scale, citizen base model, pelvis root, Y-up cm. The rig has no
/// pinky bones, so pinky roles stay unassigned — the engine's own constraints handle the
/// pinky at runtime for models that have one. Pass the committed SAME weight bytes as
/// <paramref name="dlWeights"/> to enable the deep-learning solver.
/// </summary>
public static RetargetTargetSpec SboxCitizen(string targetRigJson, byte[]? dlWeights = null) => new()
{
Rig = TargetRig.Load(targetRigJson),
VmdlScale = SboxSourceScale,
BaseModelPath = SboxCitizenPath,
DefaultRootBone = "pelvis",
UpAxis = TargetUpAxis.YUpCm,
DlWeights = dlWeights,
};
}
/// <summary>Options for <see cref="Retargeter.ConvertBatch"/> output assembly.</summary>
public sealed class BatchOptions
{
/// <summary>
/// When set, the batch additionally augments this existing vmdl text (all successful
/// clips spliced into its AnimationList via <see cref="VmdlAugmenter"/>) and returns the
/// result in <see cref="RetargetBatchResult.AugmentedVmdl"/>.
/// </summary>
public string? AugmentVmdlText { get; init; }
/// <summary>Assets-relative folder the DMX files will be written to by the caller; used
/// to build each AnimFile's <c>source_filename</c>.</summary>
public string DmxFolderRelative { get; init; } = "animations/retargeted";
/// <summary>
/// Animation source paths (<c>source_filename</c> values of the augment target's existing
/// AnimFile nodes, assets-relative) that the IO-owning caller has determined NO LONGER
/// EXIST on disk. Stale AnimFile entries referencing them are REMOVED from the augmented
/// vmdl (reported on <see cref="RetargetBatchResult.Warnings"/>) — one unresolvable
/// source otherwise fails the ENTIRE vmdl recompile ("Node 'X' resolve failure"), taking
/// every newly added animation down with it. Entries this batch overwrites (their DMX is
/// about to be written) are never pruned. The facade itself never touches the filesystem:
/// callers probe <see cref="Target.VmdlAugmenter.CollectAnimSourcePaths"/> results against
/// their content roots and pass the missing ones here. Null/empty = keep everything.
/// </summary>
public IReadOnlyCollection<string>? MissingAnimSources { get; init; }
/// <summary>Auto-suffix colliding clip names (<c>_2</c>, <c>_3</c>, …) across the whole
/// batch (default on). When off, duplicate names are kept as-is.</summary>
public bool AutoSuffixCollisions { get; init; } = true;
/// <summary>
/// After conversion, scan the batch's successful clip names for directional locomotion
/// families (default OFF): <c>_N</c>/<c>_NE</c>/…/<c>_NW</c> compass suffixes and
/// <c>_Forward</c>/<c>_Backward</c>(/<c>_Back</c>)/<c>_Left</c>/<c>_Right</c> word forms
/// sharing a stem. Each complete family (all four cardinals) is grouped under a Folder
/// node with a <c>2DBlend</c> wired to the citizen <c>move_x</c>/<c>move_y</c> pose
/// parameters, replicating the shipped citizen locomotion layout (see
/// <see cref="Target.LocomotionSetDetector"/>); detection results land on
/// <see cref="RetargetBatchResult.LocomotionSets"/>. Custom (non-citizen) base models
/// must declare <c>move_x</c>/<c>move_y</c> pose parameters themselves for the blends to
/// be drivable.
/// </summary>
public bool DetectLocomotionSets { get; init; }
}
Game
library
#nullable enable annotations
using System.Collections.Generic;
using System.Numerics;
using HumanoidRetargeter.Mapping;
using HumanoidRetargeter.Maths;
namespace HumanoidRetargeter.Solve;
using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)
/// <summary>
/// Hand rest-geometry helpers shared by <see cref="CanonicalFrames"/> (finger secondary axes)
/// and <see cref="RestNormalizer"/> (palm-down roll correction). Everything derives from joint
/// positions only — bone local axes carry no anatomical meaning on the s&box rig.
/// </summary>
internal static class HandGeometry
{
private static readonly BoneRole[] LeftProximals =
{
BoneRole.ThumbProxL, BoneRole.IndexProxL, BoneRole.MiddleProxL, BoneRole.RingProxL, BoneRole.PinkyProxL,
};
private static readonly BoneRole[] RightProximals =
{
BoneRole.ThumbProxR, BoneRole.IndexProxR, BoneRole.MiddleProxR, BoneRole.RingProxR, BoneRole.PinkyProxR,
};
// Index → pinky order; the knuckle line is taken from the first and last mapped of these.
private static readonly BoneRole[] LeftNonThumbProximals =
{
BoneRole.IndexProxL, BoneRole.MiddleProxL, BoneRole.RingProxL, BoneRole.PinkyProxL,
};
private static readonly BoneRole[] RightNonThumbProximals =
{
BoneRole.IndexProxR, BoneRole.MiddleProxR, BoneRole.RingProxR, BoneRole.PinkyProxR,
};
/// <summary>
/// Midpoint of all mapped finger proximal heads of one hand (the hand's anatomical
/// "chain child" point), or null when no finger proximal is mapped.
/// </summary>
public static Vector3? FingerProximalMidpoint(MappingResult map, IReadOnlyList<XForm> worldRest, bool left)
{
var sum = Vector3.Zero;
var count = 0;
foreach (var role in left ? LeftProximals : RightProximals)
{
if (map.RoleToBone.TryGetValue(role, out var index))
{
sum += worldRest[index].Pos;
count++;
}
}
return count > 0 ? sum / count : null;
}
/// <summary>
/// Dorsal palm normal of one hand: the unit vector pointing out of the <b>back</b> of the
/// hand (away from the palm), or null when the hand/finger geometry is unmapped or
/// degenerate.
/// </summary>
/// <remarks>
/// Formula (mirror-consistent by construction, verified on the ActorCore fixture by the
/// finger-curl test): <c>dorsal = sideSign · cross(knuckle, fingerDir)</c> with
/// <c>sideSign = +1</c> left / <c>−1</c> right, <c>knuckle = IndexProx.head −
/// PinkyProx.head</c> (first/last mapped non-thumb proximal), and <c>fingerDir =
/// FingerProximalMidpoint − Hand.head</c>. On every fixture rig the thumb proximal lies on
/// the −dorsal (palmar) side of the hand plane, grounding the sign anatomically. A positive
/// rotation about a finger frame's hinge axis (frame Y = cross(dorsal, fingerChainDir))
/// curls the fingertip toward the palm on <b>both</b> hands.
/// </remarks>
public static Vector3? Dorsal(MappingResult map, IReadOnlyList<XForm> worldRest, bool left)
{
if (!map.RoleToBone.TryGetValue(left ? BoneRole.HandL : BoneRole.HandR, out var handIndex))
return null;
var hand = worldRest[handIndex].Pos;
var nonThumb = left ? LeftNonThumbProximals : RightNonThumbProximals;
Vector3? first = null, last = null;
foreach (var role in nonThumb)
{
if (!map.RoleToBone.TryGetValue(role, out var index))
continue;
first ??= worldRest[index].Pos;
last = worldRest[index].Pos;
}
if (first is null || last is null || (first.Value - last.Value).LengthSquared() < 1e-8f)
return null;
var midpoint = FingerProximalMidpoint(map, worldRest, left);
if (midpoint is null)
return null;
var knuckle = first.Value - last.Value;
var fingerDir = midpoint.Value - hand;
var raw = Vector3.Cross(knuckle, fingerDir) * (left ? 1f : -1f);
return raw.LengthSquared() < 1e-8f ? null : Vector3.Normalize(raw);
}
}
Game
library
#nullable enable annotations // Engine-agnostic retargeting core. No Sandbox/Editor references allowed in this tree: // these sources also compile in the plain net8.0 dev harness (dev/HumanoidRetargeter.Dev.csproj).
Game
library
#nullable enable annotations
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Text;
using System.Text.Json;
namespace HumanoidRetargeter.Formats.Gltf;
using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)
/// <summary>One glTF node, reduced to what skeleton import needs (TRS rest + hierarchy).</summary>
internal sealed class GltfNode
{
public string? Name;
public int[] Children = Array.Empty<int>();
public int Parent = -1;
public bool HasMesh;
// Rest local transform: TRS properties, or the decomposed "matrix" property (the spec
// makes them exclusive; animated nodes must use TRS). Shear is not representable.
public Vector3 Translation; // meters
public Quaternion Rotation = Quaternion.Identity; // xyzw
public Vector3 Scale = Vector3.One;
}
/// <summary>One decoded animation channel: keyframe times + values for one node property.</summary>
internal sealed class GltfChannel
{
public required int NodeIndex;
public required bool IsRotation; // true = rotation (VEC4 quat), false = translation (VEC3)
public required float[] Times; // seconds, ascending
public required float[] Values; // flattened; 4 (or 3) floats per element
public required string Interpolation; // LINEAR / STEP / CUBICSPLINE
/// <summary>Floats per element (3 translation / 4 rotation).</summary>
public int Comps => IsRotation ? 4 : 3;
/// <summary>Elements stored per key: CUBICSPLINE keys carry in-tangent/value/out-tangent.</summary>
public int ElementsPerKey => Interpolation == "CUBICSPLINE" ? 3 : 1;
/// <summary>Number of keys.</summary>
public int KeyCount => Times.Length;
}
/// <summary>One glTF animation with its decoded rotation/translation channels.</summary>
internal sealed class GltfAnimation
{
public string? Name;
public List<GltfChannel> Channels { get; } = new();
}
/// <summary>
/// Container + JSON layer of the glTF importer: parses a GLB binary container or a plain
/// .gltf JSON document, resolves buffers (GLB BIN chunk and base64 <c>data:</c> URIs — file
/// IO is banned in Code/, so external file URIs throw), and decodes nodes, skin joints and
/// animation samplers into plain arrays. Throws <see cref="FormatException"/> on anything
/// malformed or unsupported.
/// </summary>
internal sealed class GltfDocument
{
private const uint GlbMagic = 0x46546C67; // 'glTF' little-endian
private const uint ChunkJson = 0x4E4F534A; // 'JSON'
private const uint ChunkBin = 0x004E4942; // 'BIN\0'
/// <summary>All nodes, indexed as in the file, with parents resolved from children lists.</summary>
public List<GltfNode> Nodes { get; } = new();
/// <summary>Union of all skins' joint node indices.</summary>
public HashSet<int> SkinJoints { get; } = new();
/// <summary>All animations with decoded rotation/translation channels (scale/weights ignored).</summary>
public List<GltfAnimation> Animations { get; } = new();
/// <summary>
/// The VRM humanoid bone map authored in the file, when present: VRM bone name
/// (<c>hips</c>, <c>leftUpperArm</c>, …) → node index. Read from BOTH extension layouts:
/// VRM 0.x <c>extensions.VRM.humanoid.humanBones</c> (an ARRAY of
/// <c>{ "bone": "hips", "node": 14 }</c> entries) and VRM 1.0
/// <c>extensions.VRMC_vrm.humanoid.humanBones</c> (an OBJECT
/// <c>{ "hips": { "node": 14 }, … }</c>). Null when the file carries neither.
/// </summary>
public Dictionary<string, int>? VrmHumanBones { get; private set; }
/// <summary>Which VRM extension supplied <see cref="VrmHumanBones"/>: <c>0</c> for the
/// 0.x <c>VRM</c> extension, <c>1</c> for the 1.0 <c>VRMC_vrm</c> extension, <c>-1</c>
/// when none.</summary>
public int VrmVersion { get; private set; } = -1;
private GltfDocument()
{
}
/// <summary>Parses GLB or plain-JSON glTF bytes.</summary>
/// <exception cref="FormatException">Truncated/malformed container, invalid JSON,
/// unresolvable buffers, or unsupported accessor layouts.</exception>
public static GltfDocument Parse(byte[] data)
{
ArgumentNullException.ThrowIfNull(data);
byte[] json;
byte[]? bin = null;
if (data.Length >= 4 && ReadU32(data, 0) == GlbMagic)
(json, bin) = ParseGlbContainer(data);
else
json = data;
JsonElement root;
try
{
// Parse via string: Memory<T>/ReadOnlyMemory<T> are not on the s&box runtime
// whitelist (SB1000), and the string path also lets us strip a UTF-8 BOM
// (Utf8JsonReader rejects raw BOM bytes). Clone detaches from the disposed
// JsonDocument.
var text = System.Text.Encoding.UTF8.GetString(json).TrimStart('\uFEFF');
using var doc = JsonDocument.Parse(text);
root = doc.RootElement.Clone();
}
catch (JsonException e)
{
throw new FormatException($"glTF: invalid JSON ({e.Message})");
}
if (root.ValueKind != JsonValueKind.Object || !root.TryGetProperty("asset", out _))
throw new FormatException("glTF: missing required 'asset' object (not a glTF file?).");
var document = new GltfDocument();
var buffers = ResolveBuffers(root, bin);
document.ReadNodes(root);
document.ReadSkins(root);
document.ReadAnimations(root, buffers);
document.ReadVrmHumanoid(root);
return document;
}
// ================================================================== VRM humanoid
/// <summary>
/// Reads the authored humanoid bone map of a VRM file (a .vrm is a regular glTF 2.0/GLB
/// container plus a VRM extension). VRM 1.0's <c>VRMC_vrm</c> wins when both extensions
/// are present. Defensive throughout: malformed entries and out-of-range node indices
/// are skipped (a broken bone map degrades to the regular detection cascade rather than
/// failing the import).
/// </summary>
private void ReadVrmHumanoid(JsonElement root)
{
if (!root.TryGetProperty("extensions", out var extensions)
|| extensions.ValueKind != JsonValueKind.Object)
return;
// ---- VRM 1.0: extensions.VRMC_vrm.humanoid.humanBones = { "<bone>": { "node": n } } ----
if (TryGetHumanBones(extensions, "VRMC_vrm", out var humanBones1)
&& humanBones1.ValueKind == JsonValueKind.Object)
{
var map = new Dictionary<string, int>(StringComparer.Ordinal);
foreach (var property in humanBones1.EnumerateObject())
{
if (property.Value.ValueKind == JsonValueKind.Object
&& property.Value.TryGetProperty("node", out var node)
&& node.ValueKind == JsonValueKind.Number
&& node.TryGetInt32(out var index)
&& index >= 0 && index < Nodes.Count)
{
map[property.Name] = index;
}
}
if (map.Count > 0)
{
VrmHumanBones = map;
VrmVersion = 1;
return;
}
}
// ---- VRM 0.x: extensions.VRM.humanoid.humanBones = [ { "bone": "...", "node": n } ] ----
if (TryGetHumanBones(extensions, "VRM", out var humanBones0)
&& humanBones0.ValueKind == JsonValueKind.Array)
{
var map = new Dictionary<string, int>(StringComparer.Ordinal);
foreach (var entry in humanBones0.EnumerateArray())
{
if (entry.ValueKind == JsonValueKind.Object
&& entry.TryGetProperty("bone", out var bone)
&& bone.ValueKind == JsonValueKind.String
&& entry.TryGetProperty("node", out var node)
&& node.ValueKind == JsonValueKind.Number
&& node.TryGetInt32(out var index)
&& index >= 0 && index < Nodes.Count)
{
map[bone.GetString()!] = index;
}
}
if (map.Count > 0)
{
VrmHumanBones = map;
VrmVersion = 0;
}
}
}
private static bool TryGetHumanBones(JsonElement extensions, string extensionName, out JsonElement humanBones)
{
humanBones = default;
return extensions.TryGetProperty(extensionName, out var vrm)
&& vrm.ValueKind == JsonValueKind.Object
&& vrm.TryGetProperty("humanoid", out var humanoid)
&& humanoid.ValueKind == JsonValueKind.Object
&& humanoid.TryGetProperty("humanBones", out humanBones);
}
// ================================================================== GLB container
/// <summary>GLB layout: 12-byte header (magic 'glTF', u32 version = 2, u32 length),
/// then chunks of (u32 length, u32 type, bytes): one JSON chunk, optionally one BIN.</summary>
private static (byte[] Json, byte[]? Bin) ParseGlbContainer(byte[] data)
{
if (data.Length < 12)
throw new FormatException("GLB: truncated header (need 12 bytes).");
uint version = ReadU32(data, 4);
if (version != 2)
throw new FormatException($"GLB: unsupported container version {version} (expected 2).");
long declared = ReadU32(data, 8);
if (declared > data.Length)
throw new FormatException(
$"GLB: truncated file (header declares {declared} bytes, got {data.Length}).");
byte[]? json = null, bin = null;
long offset = 12;
while (offset + 8 <= declared)
{
long length = ReadU32(data, (int)offset);
uint type = ReadU32(data, (int)offset + 4);
offset += 8;
if (offset + length > data.Length)
throw new FormatException("GLB: truncated chunk (declared length exceeds the file).");
if (type == ChunkJson && json is null)
json = data.AsSpan((int)offset, (int)length).ToArray();
else if (type == ChunkBin && bin is null)
bin = data.AsSpan((int)offset, (int)length).ToArray();
// Unknown chunk types are skipped per spec.
offset += length + (length % 4 == 0 ? 0 : 4 - length % 4); // chunks are 4-aligned
}
if (json is null)
throw new FormatException("GLB: no JSON chunk found.");
return (json, bin);
}
private static uint ReadU32(byte[] data, int offset)
=> (uint)(data[offset] | data[offset + 1] << 8 | data[offset + 2] << 16 | data[offset + 3] << 24);
// ================================================================== buffers
/// <summary>
/// Resolves every entry of <c>buffers</c>: no <c>uri</c> = the GLB BIN chunk (spec: only
/// buffer 0 may do this), <c>data:</c> URIs are base64-decoded inline. External file
/// URIs are NOT supported — this library does no file IO; users should export .glb.
/// </summary>
private static List<byte[]> ResolveBuffers(JsonElement root, byte[]? bin)
{
var buffers = new List<byte[]>();
if (!root.TryGetProperty("buffers", out var array) || array.ValueKind != JsonValueKind.Array)
return buffers;
foreach (var buffer in array.EnumerateArray())
{
if (!buffer.TryGetProperty("uri", out var uriProp))
{
buffers.Add(bin ?? throw new FormatException(
"glTF: buffer has no uri but the file has no GLB BIN chunk."));
continue;
}
var uri = uriProp.GetString() ?? "";
if (uri.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
{
int comma = uri.IndexOf(',');
if (comma < 0 || !uri[..comma].EndsWith(";base64", StringComparison.OrdinalIgnoreCase))
throw new FormatException("glTF: only base64 data: URIs are supported for buffers.");
try
{
buffers.Add(Convert.FromBase64String(uri[(comma + 1)..]));
}
catch (Exception e) when (e is FormatException or ArgumentException)
{
throw new FormatException("glTF: invalid base64 in buffer data: URI.");
}
}
else
{
throw new FormatException(
$"glTF: buffer references an external file ('{uri}') which this importer cannot "
+ "read (no file IO). Export as .glb (binary, self-contained) instead.");
}
}
return buffers;
}
// ================================================================== nodes + skins
private void ReadNodes(JsonElement root)
{
if (!root.TryGetProperty("nodes", out var array) || array.ValueKind != JsonValueKind.Array)
return;
Span<float> m = stackalloc float[16]; // matrix scratch (outside the loop: CA2014)
foreach (var n in array.EnumerateArray())
{
var node = new GltfNode
{
Name = n.TryGetProperty("name", out var name) ? name.GetString() : null,
HasMesh = n.TryGetProperty("mesh", out _),
};
if (n.TryGetProperty("children", out var children) && children.ValueKind == JsonValueKind.Array)
{
var list = new List<int>();
foreach (var c in children.EnumerateArray())
list.Add(c.GetInt32());
node.Children = list.ToArray();
}
if (n.TryGetProperty("matrix", out var matrix) && matrix.ValueKind == JsonValueKind.Array)
{
// Column-major 16 floats; the element order maps 1:1 onto System.Numerics'
// row-vector matrices (translation in elements 12..14 either way).
int i = 0;
foreach (var v in matrix.EnumerateArray())
{
if (i >= 16)
break;
m[i++] = v.GetSingle();
}
if (i < 16)
throw new FormatException("glTF: node matrix has fewer than 16 elements.");
var local = new Matrix4x4(
m[0], m[1], m[2], m[3],
m[4], m[5], m[6], m[7],
m[8], m[9], m[10], m[11],
m[12], m[13], m[14], m[15]);
if (Matrix4x4.Decompose(local, out var scale, out var rot, out var pos))
{
node.Translation = pos;
node.Rotation = rot;
node.Scale = scale;
}
else
{
node.Translation = local.Translation; // degenerate: keep position at least
}
}
else
{
node.Translation = ReadVec3(n, "translation", Vector3.Zero);
node.Scale = ReadVec3(n, "scale", Vector3.One);
if (n.TryGetProperty("rotation", out var r) && r.ValueKind == JsonValueKind.Array
&& r.GetArrayLength() >= 4)
{
node.Rotation = new Quaternion(
r[0].GetSingle(), r[1].GetSingle(), r[2].GetSingle(), r[3].GetSingle());
}
}
Nodes.Add(node);
}
// Resolve parents (per spec a node is referenced by at most one other node's children).
for (int i = 0; i < Nodes.Count; i++)
{
foreach (var child in Nodes[i].Children)
{
if (child < 0 || child >= Nodes.Count)
throw new FormatException($"glTF: node {i} references nonexistent child {child}.");
if (Nodes[child].Parent < 0)
Nodes[child].Parent = i;
}
}
}
private static Vector3 ReadVec3(JsonElement element, string property, Vector3 fallback)
{
if (!element.TryGetProperty(property, out var v) || v.ValueKind != JsonValueKind.Array
|| v.GetArrayLength() < 3)
return fallback;
return new Vector3(v[0].GetSingle(), v[1].GetSingle(), v[2].GetSingle());
}
private void ReadSkins(JsonElement root)
{
if (!root.TryGetProperty("skins", out var array) || array.ValueKind != JsonValueKind.Array)
return;
foreach (var skin in array.EnumerateArray())
{
if (!skin.TryGetProperty("joints", out var joints) || joints.ValueKind != JsonValueKind.Array)
continue;
foreach (var j in joints.EnumerateArray())
{
int index = j.GetInt32();
if (index >= 0 && index < Nodes.Count)
SkinJoints.Add(index);
}
}
}
// ================================================================== animations
private void ReadAnimations(JsonElement root, List<byte[]> buffers)
{
if (!root.TryGetProperty("animations", out var array) || array.ValueKind != JsonValueKind.Array)
return;
root.TryGetProperty("accessors", out var accessors);
root.TryGetProperty("bufferViews", out var views);
foreach (var a in array.EnumerateArray())
{
var animation = new GltfAnimation
{
Name = a.TryGetProperty("name", out var name) ? name.GetString() : null,
};
if (!a.TryGetProperty("channels", out var channels) || !a.TryGetProperty("samplers", out var samplers))
{
Animations.Add(animation);
continue;
}
foreach (var channel in channels.EnumerateArray())
{
if (!channel.TryGetProperty("target", out var target)
|| !target.TryGetProperty("node", out var nodeProp)
|| !target.TryGetProperty("path", out var pathProp))
continue; // extension targets (e.g. KHR_animation_pointer) are ignored
var path = pathProp.GetString();
if (path is not ("rotation" or "translation"))
continue; // scale / weights channels are ignored by design
int node = nodeProp.GetInt32();
if (node < 0 || node >= Nodes.Count)
continue;
int samplerIndex = channel.TryGetProperty("sampler", out var s) ? s.GetInt32() : -1;
if (samplerIndex < 0 || samplerIndex >= samplers.GetArrayLength())
throw new FormatException("glTF: animation channel references a nonexistent sampler.");
var sampler = samplers[samplerIndex];
var interpolation = sampler.TryGetProperty("interpolation", out var interp)
? interp.GetString() ?? "LINEAR"
: "LINEAR";
bool isRotation = path == "rotation";
int comps = isRotation ? 4 : 3;
var times = ReadAccessor(accessors, views, buffers,
RequiredInt(sampler, "input", "animation sampler"), 1, normalizedAllowed: false);
var values = ReadAccessor(accessors, views, buffers,
RequiredInt(sampler, "output", "animation sampler"), comps, normalizedAllowed: isRotation);
int elementsPerKey = interpolation == "CUBICSPLINE" ? 3 : 1;
if (times.Length == 0 || values.Length < times.Length * elementsPerKey * comps)
continue; // empty or under-filled sampler: nothing usable
animation.Channels.Add(new GltfChannel
{
NodeIndex = node,
IsRotation = isRotation,
Times = times,
Values = values,
Interpolation = interpolation,
});
}
Animations.Add(animation);
}
}
private static int RequiredInt(JsonElement element, string property, string context)
{
if (!element.TryGetProperty(property, out var v))
throw new FormatException($"glTF: {context} is missing '{property}'.");
return v.GetInt32();
}
// ================================================================== accessors
/// <summary>
/// Decodes an accessor to floats. Component types: f32 directly; normalized i8/u8/i16/u16
/// per the spec's normalization rules when <paramref name="normalizedAllowed"/> (rotation
/// outputs); anything else throws. Honors accessor/bufferView byte offsets and an
/// explicit byteStride. Sparse accessors are not supported.
/// </summary>
private static float[] ReadAccessor(
JsonElement accessors, JsonElement views, List<byte[]> buffers,
int accessorIndex, int expectedComps, bool normalizedAllowed)
{
if (accessors.ValueKind != JsonValueKind.Array || accessorIndex < 0
|| accessorIndex >= accessors.GetArrayLength())
throw new FormatException($"glTF: accessor {accessorIndex} does not exist.");
var accessor = accessors[accessorIndex];
if (accessor.TryGetProperty("sparse", out _))
throw new FormatException("glTF: sparse accessors are not supported.");
var type = accessor.TryGetProperty("type", out var t) ? t.GetString() : null;
int comps = type switch
{
"SCALAR" => 1,
"VEC3" => 3,
"VEC4" => 4,
_ => throw new FormatException($"glTF: unsupported accessor type '{type}'."),
};
if (comps != expectedComps)
throw new FormatException(
$"glTF: accessor {accessorIndex} is {type}, expected {expectedComps} component(s).");
int count = RequiredInt(accessor, "count", "accessor");
int componentType = RequiredInt(accessor, "componentType", "accessor");
bool normalized = accessor.TryGetProperty("normalized", out var n) && n.GetBoolean();
// The count is attacker-controlled: validate it BEFORE any allocation sized by it.
// Negative would throw OverflowException from the array allocation (breaking the
// FormatException malformed-file contract); huge would OOM; count * comps can wrap.
if (count < 0)
throw new FormatException($"glTF: accessor {accessorIndex} has a negative count ({count}).");
int compSize = componentType switch
{
5126 => 4, // FLOAT
5120 or 5121 => 1, // BYTE / UNSIGNED_BYTE
5122 or 5123 => 2, // SHORT / UNSIGNED_SHORT
_ => throw new FormatException(
$"glTF: unsupported accessor componentType {componentType}."),
};
if (componentType != 5126 && !(normalized && normalizedAllowed))
throw new FormatException(
$"glTF: accessor {accessorIndex} must be float (or a normalized integer "
+ "rotation output).");
int elementSize = comps * compSize;
if (!accessor.TryGetProperty("bufferView", out var viewIndexProp))
{
// Zero-filled when no bufferView (legal per spec) — but then nothing backs the
// count, so cap it by the file's total decoded buffer bytes (a real file's
// accessors never outgrow its payload; a small floor keeps tiny legitimate
// zero-filled accessors working in buffer-less documents).
long totalBufferBytes = 0;
foreach (var b in buffers)
totalBufferBytes += b.Length;
long capacity = Math.Min(
Math.Max(totalBufferBytes / elementSize, 65536),
int.MaxValue / comps); // keeps count * comps int-representable
if (count > capacity)
throw new FormatException(
$"glTF: accessor {accessorIndex} count {count} exceeds what the file's "
+ "buffers could back (malformed or hostile file).");
return new float[checked(count * comps)];
}
int viewIndex = viewIndexProp.GetInt32();
if (views.ValueKind != JsonValueKind.Array || viewIndex < 0 || viewIndex >= views.GetArrayLength())
throw new FormatException($"glTF: bufferView {viewIndex} does not exist.");
var view = views[viewIndex];
int bufferIndex = RequiredInt(view, "buffer", "bufferView");
if (bufferIndex < 0 || bufferIndex >= buffers.Count)
throw new FormatException($"glTF: buffer {bufferIndex} does not exist.");
var buffer = buffers[bufferIndex];
int viewOffset = view.TryGetProperty("byteOffset", out var vo) ? vo.GetInt32() : 0;
int accessorOffset = accessor.TryGetProperty("byteOffset", out var ao) ? ao.GetInt32() : 0;
int stride = view.TryGetProperty("byteStride", out var st) ? st.GetInt32() : elementSize;
if (stride < elementSize)
throw new FormatException("glTF: bufferView byteStride is smaller than the element size.");
// Bounds check in long arithmetic BEFORE allocating: the backing range must fit the
// buffer, which also caps count at buffer.Length / stride (+1) — so the allocation
// below is bounded by the actual file size and checked() can no longer overflow.
long start = (long)viewOffset + accessorOffset;
long end = start + (long)(count - 1) * stride + elementSize;
if (count > 0 && (start < 0 || end > buffer.Length))
throw new FormatException(
$"glTF: accessor {accessorIndex} reads past the end of its buffer (truncated file?).");
var result = new float[checked(count * comps)];
for (int element = 0; element < count; element++)
{
int offset = (int)(start + (long)element * stride);
for (int c = 0; c < comps; c++)
{
int at = offset + c * compSize;
result[element * comps + c] = componentType switch
{
5126 => BitConverter.ToSingle(buffer, at),
5120 => MathF.Max((sbyte)buffer[at] / 127f, -1f),
5121 => buffer[at] / 255f,
5122 => MathF.Max(BitConverter.ToInt16(buffer, at) / 32767f, -1f),
_ => BitConverter.ToUInt16(buffer, at) / 65535f,
};
}
}
return result;
}
}
Game
library
#nullable enable annotations
using System;
using System.Numerics;
namespace HumanoidRetargeter.Maths;
using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)
/// <summary>
/// A rigid transform: rotation followed by translation (no scale or shear).
/// </summary>
/// <remarks>
/// Project conventions (fixed for the whole library):
/// <list type="bullet">
/// <item>Positions are centimeters.</item>
/// <item>Quaternions are XYZW unit quaternions (<see cref="System.Numerics.Quaternion"/> native layout).</item>
/// <item>Column-vector convention: a local-space point maps to outer space as
/// <c>p' = rotate(Rot, p) + Pos</c>, and <c>a * b</c> on quaternions applies <c>b</c> first.</item>
/// </list>
/// </remarks>
public struct XForm : IEquatable<XForm>
{
/// <summary>Translation component, in centimeters.</summary>
public Vector3 Pos;
/// <summary>Rotation component, XYZW unit quaternion.</summary>
public Quaternion Rot;
/// <summary>Creates a transform from a translation and a rotation.</summary>
public XForm(Vector3 pos, Quaternion rot)
{
Pos = pos;
Rot = rot;
}
/// <summary>The identity transform (zero translation, identity rotation).</summary>
public static XForm Identity => new(Vector3.Zero, Quaternion.Identity);
/// <summary>
/// Composes a parent transform with a child-local transform, producing the child's
/// transform in the parent's outer space (world = parent ∘ local):
/// <c>pos = parent.Pos + rotate(parent.Rot, local.Pos)</c>, <c>rot = parent.Rot * local.Rot</c>.
/// The resulting rotation is re-normalized to suppress floating-point drift.
/// </summary>
public static XForm Compose(in XForm parent, in XForm local)
=> new(
parent.Pos + Vector3.Transform(local.Pos, parent.Rot),
MathQ.Normalize(parent.Rot * local.Rot));
/// <summary>
/// Returns the inverse transform, such that <c>Compose(x, x.Inverse())</c> and
/// <c>Compose(x.Inverse(), x)</c> are both identity.
/// </summary>
public readonly XForm Inverse()
{
var invRot = Quaternion.Conjugate(MathQ.Normalize(Rot));
return new XForm(-Vector3.Transform(Pos, invRot), invRot);
}
/// <summary>
/// Re-expresses a world transform relative to a parent world transform; the inverse of
/// <see cref="Compose"/>: <c>ToLocal(p, Compose(p, l)) == l</c>.
/// </summary>
public static XForm ToLocal(in XForm parentWorld, in XForm world)
=> Compose(parentWorld.Inverse(), world);
/// <summary>Transforms a point from this transform's local space to its outer space.</summary>
public readonly Vector3 TransformPoint(Vector3 point) => Pos + Vector3.Transform(point, Rot);
/// <summary>Rotates a direction vector by this transform's rotation (translation ignored).</summary>
public readonly Vector3 TransformVector(Vector3 vector) => Vector3.Transform(vector, Rot);
/// <inheritdoc />
public readonly bool Equals(XForm other) => Pos.Equals(other.Pos) && Rot.Equals(other.Rot);
/// <inheritdoc />
public override readonly bool Equals(object? obj) => obj is XForm other && Equals(other);
/// <inheritdoc />
public override readonly int GetHashCode() => HashCode.Combine(Pos, Rot);
/// <summary>Componentwise equality (no tolerance).</summary>
public static bool operator ==(XForm left, XForm right) => left.Equals(right);
/// <summary>Componentwise inequality (no tolerance).</summary>
public static bool operator !=(XForm left, XForm right) => !left.Equals(right);
/// <inheritdoc />
public override readonly string ToString() => $"XForm(Pos={Pos}, Rot={Rot})";
}
Game
library
#nullable enable annotations
using System;
using System.Collections.Generic;
using System.Numerics;
using HumanoidRetargeter.Mapping;
using HumanoidRetargeter.Maths;
using SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;
namespace HumanoidRetargeter.Solve;
using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)
/// <summary>
/// Canonical anatomical frames: one world-space rest basis per mapped <see cref="BoneRole"/>,
/// derived from rest <b>geometry</b> (joint head positions) of any rig plus its mapping.
/// Built with the same deterministic convention on source and target, so world-rotation deltas
/// conjugated through these frames transfer between rigs with different bone local axes
/// (the s&box Citizen rig's local axes encode no anatomy — bone-Y points chest-forward).
/// </summary>
/// <remarks>
/// <para><b>Frame convention</b> — for each role the frame quaternion <c>F</c> rotates unit
/// axes onto: <c>X = P</c> (primary), <c>Z</c> = the secondary hint <c>S</c> orthonormalized
/// against <c>P</c>, <c>Y = cross(Z, X)</c> (right-handed; for fingers Y is the curl hinge).</para>
/// <para><b>Primary axis P</b> = normalize(chain-child head − bone head), where the chain
/// child is the next <i>mapped</i> role down the bone's anatomical chain
/// (Hips→Spine0..4→Neck→Head; Clavicle→UpperArm→LowerArm→Hand; UpperLeg→LowerLeg→Foot→Toe;
/// per-finger Meta→Prox→Mid→Dist). Tips: the Head inherits its previous chain segment
/// (neck→head — the skull-base axis, real anatomy; measured 0–27° forward of character up
/// across neutral-rest rigs), falling back to a virtual character-up extension only when
/// that segment is absent or degenerate; Hand points at the midpoint of its mapped finger
/// proximals (else along the forearm); Foot without a toe and Toe extend along character
/// forward; finger distals extend along their previous segment. Other bones with nothing
/// mapped below inherit the previous chain segment's direction.</para>
/// <para><b>Secondary axis S</b> by bone class: spine/neck/head/hips and legs use character
/// forward (knee hinge lateral); clavicle/arms/hands use <c>cross(P, characterUp)</c>
/// (elbow hinge ⊥ limb in the character's horizontal plane at T-pose), falling back to
/// character forward when P is vertical; feet/toes use character up; fingers use the hand's
/// dorsal palm normal (see <see cref="HandGeometry.Dorsal"/>) so a positive rotation about
/// frame Y curls fingertips toward the palm on both hands.</para>
/// <para>When used by the solver, build the frames on the <see cref="RestNormalizer"/>-
/// normalized rest via <see cref="Build(SkeletonModel, MappingResult, IReadOnlyList{XForm})"/>;
/// this class itself just measures whatever rest it is given.</para>
/// </remarks>
public sealed class CanonicalFrames
{
private readonly Dictionary<BoneRole, Quaternion> _frames;
private readonly HashSet<BoneRole> _virtualPrimary;
/// <summary>Character forward (the direction the toes point at rest), unit length.</summary>
public Vector3 CharacterForward { get; }
/// <summary>Character up (hips toward shoulders at rest), unit length.</summary>
public Vector3 CharacterUp { get; }
/// <summary>Rest hip height above the lowest foot/toe point, along character up, cm.</summary>
public float HipHeight { get; }
private CanonicalFrames(
Dictionary<BoneRole, Quaternion> frames, HashSet<BoneRole> virtualPrimary,
Vector3 forward, Vector3 up, float hipHeight)
{
_frames = frames;
_virtualPrimary = virtualPrimary;
CharacterForward = forward;
CharacterUp = up;
HipHeight = hipHeight;
}
/// <summary>True when a canonical frame exists for <paramref name="role"/> (the role is
/// mapped and its chain geometry is resolvable).</summary>
public bool Has(BoneRole role) => _frames.ContainsKey(role);
/// <summary>
/// True when the role's primary axis is a <b>virtual</b> character-axis extension rather
/// than real joint geometry (e.g. a Foot with no mapped Toe extends along character
/// forward; mapped Toes extend along character forward by convention; a Head whose
/// neck→head segment is degenerate extends along character up). Absolute direction
/// matching against a virtual primary imposes an arbitrary direction, so the solver
/// falls back to delta transfer when the source is virtual but the target is real
/// (see <see cref="GeometricSolver"/> remarks).
/// </summary>
public bool HasVirtualPrimary(BoneRole role) => _virtualPrimary.Contains(role);
/// <summary>The world-space canonical rest frame of <paramref name="role"/>.</summary>
/// <exception cref="InvalidOperationException">Thrown when <see cref="Has"/> is false for
/// the role.</exception>
public Quaternion WorldFrameOf(BoneRole role)
=> _frames.TryGetValue(role, out var frame)
? frame
: throw new InvalidOperationException($"No canonical frame for role {role} (not mapped or unresolvable).");
/// <summary>Builds frames from the skeleton's bind rest (<c>skeleton.RestWorld</c>).</summary>
public static CanonicalFrames Build(SkeletonModel skeleton, MappingResult map)
=> Build(skeleton, map, (skeleton ?? throw new ArgumentNullException(nameof(skeleton))).RestWorld);
/// <summary>
/// Builds frames from explicit rest world transforms (e.g. a <see cref="RestPose"/>
/// produced by <see cref="RestNormalizer"/>), indexed like <c>skeleton.Bones</c>.
/// </summary>
public static CanonicalFrames Build(
SkeletonModel skeleton, MappingResult map, IReadOnlyList<XForm> worldRest)
{
ArgumentNullException.ThrowIfNull(skeleton);
ArgumentNullException.ThrowIfNull(map);
ArgumentNullException.ThrowIfNull(worldRest);
if (worldRest.Count != skeleton.Count)
throw new ArgumentException(
$"worldRest has {worldRest.Count} entries for a {skeleton.Count}-bone skeleton.");
var cf = CharacterFrame.Compute(skeleton, map, worldRest);
var frames = new Dictionary<BoneRole, Quaternion>();
var virtualPrimary = new HashSet<BoneRole>();
foreach (var (chain, kind, left) in Chains())
BuildChainFrames(chain, kind, left, map, worldRest, cf, frames, virtualPrimary);
return new CanonicalFrames(frames, virtualPrimary, cf.Forward, cf.Up, cf.HipHeight);
}
// ---------------------------------------------------------------- chain construction
private enum ChainKind
{
Body,
Arm,
Leg,
Finger,
}
private static IEnumerable<(BoneRole[] Chain, ChainKind Kind, bool Left)> Chains()
{
yield return (new[]
{
BoneRole.Hips, BoneRole.Spine0, BoneRole.Spine1, BoneRole.Spine2, BoneRole.Spine3,
BoneRole.Spine4, BoneRole.Neck, BoneRole.Head,
}, ChainKind.Body, false);
foreach (var left in new[] { true, false })
{
var s = left ? "L" : "R";
yield return (new[]
{
Role("Clavicle", s), Role("UpperArm", s), Role("LowerArm", s), Role("Hand", s),
}, ChainKind.Arm, left);
yield return (new[]
{
Role("UpperLeg", s), Role("LowerLeg", s), Role("Foot", s), Role("Toe", s),
}, ChainKind.Leg, left);
foreach (var finger in new[] { "Thumb", "Index", "Middle", "Ring", "Pinky" })
{
yield return (new[]
{
Role(finger + "Meta", s), Role(finger + "Prox", s),
Role(finger + "Mid", s), Role(finger + "Dist", s),
}, ChainKind.Finger, left);
}
}
}
private static BoneRole Role(string baseName, string side) => Enum.Parse<BoneRole>(baseName + side);
private static void BuildChainFrames(
BoneRole[] chain, ChainKind kind, bool left, MappingResult map,
IReadOnlyList<XForm> worldRest, CharacterFrame cf, Dictionary<BoneRole, Quaternion> frames,
HashSet<BoneRole> virtualPrimary)
{
// Collapse to the mapped chain members; gaps are skipped so e.g. a missing Spine1
// makes Spine0 point straight at Spine2.
var mapped = new List<(BoneRole Role, Vector3 Pos)>(chain.Length);
foreach (var role in chain)
{
if (map.RoleToBone.TryGetValue(role, out var index))
mapped.Add((role, worldRest[index].Pos));
}
Vector3? dorsal = kind == ChainKind.Finger ? HandGeometry.Dorsal(map, worldRest, left) : null;
for (var i = 0; i < mapped.Count; i++)
{
var (role, pos) = mapped[i];
Vector3? prevDir = i > 0 ? pos - mapped[i - 1].Pos : null;
var (primary, isVirtual) = i + 1 < mapped.Count
? ((Vector3?)(mapped[i + 1].Pos - pos), false)
: TipPrimary(kind, role, pos, prevDir, left, map, worldRest, cf);
if (primary is null || primary.Value.LengthSquared() < 1e-8f)
continue;
var secondary = Secondary(kind, role, primary.Value, dorsal, cf);
frames[role] = BasisFromPrimarySecondary(primary.Value, secondary, cf);
if (isVirtual)
virtualPrimary.Add(role);
}
}
/// <summary>Primary direction for the last mapped bone of a chain. <c>Virtual</c> is true
/// when the direction is a character-axis convention rather than this rig's real joint
/// geometry (see <see cref="HasVirtualPrimary"/>).</summary>
private static (Vector3? Dir, bool Virtual) TipPrimary(
ChainKind kind, BoneRole role, Vector3 pos, Vector3? prevDir, bool left,
MappingResult map, IReadOnlyList<XForm> worldRest, CharacterFrame cf)
{
switch (kind)
{
case ChainKind.Body:
// Head: its primary is the REAL previous chain segment (neck→head — the
// skull-base axis; the rest lean of that segment is head-joint-placement
// anatomy the delta transfer modes reference, and the posed-rest gaze
// fallback measures — see GeometricSolver remarks). Only a degenerate or
// absent segment falls back to the virtual character-up extension (e.g. a
// head stacked on the neck). A body chain that ends early keeps its
// previous segment direction, defaulting to up.
if (role == BoneRole.Head)
return prevDir is { } seg && seg.LengthSquared() >= 1e-8f ? (seg, false) : (cf.Up, true);
return prevDir is not null ? (prevDir, false) : (cf.Up, true);
case ChainKind.Arm:
if (role is BoneRole.HandL or BoneRole.HandR)
{
var knuckles = HandGeometry.FingerProximalMidpoint(map, worldRest, left);
if (knuckles is not null)
return (knuckles.Value - pos, false);
}
return (prevDir, false); // along the forearm / previous segment; null → no frame
case ChainKind.Leg:
// Foot without a mapped toe, and the toe itself, extend along character
// forward (toes point forward by the character-frame convention).
if (role is BoneRole.FootL or BoneRole.FootR or BoneRole.ToeL or BoneRole.ToeR)
return (cf.Forward, true);
return (prevDir, false);
case ChainKind.Finger:
if (prevDir is not null)
return (prevDir, false); // distal tip extrapolates its previous segment
// Single mapped finger bone: point away from the hand when possible.
var handRole = left ? BoneRole.HandL : BoneRole.HandR;
if (map.RoleToBone.TryGetValue(handRole, out var handIndex))
return (pos - worldRest[handIndex].Pos, false);
return (null, false);
default:
return (null, false);
}
}
/// <summary>Secondary (Z) hint by bone class; see the class remarks for rationale.</summary>
private static Vector3 Secondary(ChainKind kind, BoneRole role, Vector3 primary, Vector3? dorsal, CharacterFrame cf)
{
switch (kind)
{
case ChainKind.Body:
return cf.Forward;
case ChainKind.Arm:
{
var hinge = Vector3.Cross(Vector3.Normalize(primary), cf.Up);
return hinge.LengthSquared() < 1e-6f ? cf.Forward : hinge;
}
case ChainKind.Leg:
// Feet and toes lie near the character-forward direction, so they use up as
// the secondary; thigh/calf use forward (knee hinge lateral).
if (role is BoneRole.FootL or BoneRole.FootR or BoneRole.ToeL or BoneRole.ToeR)
return cf.Up;
return cf.Forward;
case ChainKind.Finger:
if (dorsal is not null)
return dorsal.Value;
var fallback = Vector3.Cross(Vector3.Normalize(primary), cf.Up);
return fallback.LengthSquared() < 1e-6f ? cf.Forward : fallback;
default:
return cf.Forward;
}
}
/// <summary>
/// Orthonormal right-handed basis: <c>X = normalize(primary)</c>, <c>Z = secondary</c>
/// Gram-Schmidt-orthonormalized against X (falling back to character forward, then up,
/// then world axes when degenerate), <c>Y = cross(Z, X)</c>.
/// </summary>
private static Quaternion BasisFromPrimarySecondary(Vector3 primary, Vector3 secondary, CharacterFrame cf)
{
var x = Vector3.Normalize(primary);
var z = Orthonormalized(secondary, x)
?? Orthonormalized(cf.Forward, x)
?? Orthonormalized(cf.Up, x)
?? Orthonormalized(Vector3.UnitZ, x)
?? Orthonormalized(Vector3.UnitX, x)!.Value;
var y = Vector3.Cross(z, x);
// System.Numerics matrices act on row vectors: the rows are the images of the unit
// axes under the rotation (row1 = R*X, row2 = R*Y, row3 = R*Z).
var m = new Matrix4x4(
x.X, x.Y, x.Z, 0f,
y.X, y.Y, y.Z, 0f,
z.X, z.Y, z.Z, 0f,
0f, 0f, 0f, 1f);
return MathQ.Normalize(Quaternion.CreateFromRotationMatrix(m));
}
private static Vector3? Orthonormalized(Vector3 hint, Vector3 x)
{
var z = hint - x * Vector3.Dot(hint, x);
return z.LengthSquared() < 1e-6f ? null : Vector3.Normalize(z);
}
}
Game
library
#nullable enable annotations
using System.Collections.Generic;
using System.Numerics;
using HumanoidRetargeter.Mapping;
using HumanoidRetargeter.Maths;
namespace HumanoidRetargeter.Solve;
using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)
/// <summary>
/// Hand rest-geometry helpers shared by <see cref="CanonicalFrames"/> (finger secondary axes)
/// and <see cref="RestNormalizer"/> (palm-down roll correction). Everything derives from joint
/// positions only — bone local axes carry no anatomical meaning on the s&box rig.
/// </summary>
internal static class HandGeometry
{
private static readonly BoneRole[] LeftProximals =
{
BoneRole.ThumbProxL, BoneRole.IndexProxL, BoneRole.MiddleProxL, BoneRole.RingProxL, BoneRole.PinkyProxL,
};
private static readonly BoneRole[] RightProximals =
{
BoneRole.ThumbProxR, BoneRole.IndexProxR, BoneRole.MiddleProxR, BoneRole.RingProxR, BoneRole.PinkyProxR,
};
// Index → pinky order; the knuckle line is taken from the first and last mapped of these.
private static readonly BoneRole[] LeftNonThumbProximals =
{
BoneRole.IndexProxL, BoneRole.MiddleProxL, BoneRole.RingProxL, BoneRole.PinkyProxL,
};
private static readonly BoneRole[] RightNonThumbProximals =
{
BoneRole.IndexProxR, BoneRole.MiddleProxR, BoneRole.RingProxR, BoneRole.PinkyProxR,
};
/// <summary>
/// Midpoint of all mapped finger proximal heads of one hand (the hand's anatomical
/// "chain child" point), or null when no finger proximal is mapped.
/// </summary>
public static Vector3? FingerProximalMidpoint(MappingResult map, IReadOnlyList<XForm> worldRest, bool left)
{
var sum = Vector3.Zero;
var count = 0;
foreach (var role in left ? LeftProximals : RightProximals)
{
if (map.RoleToBone.TryGetValue(role, out var index))
{
sum += worldRest[index].Pos;
count++;
}
}
return count > 0 ? sum / count : null;
}
/// <summary>
/// Dorsal palm normal of one hand: the unit vector pointing out of the <b>back</b> of the
/// hand (away from the palm), or null when the hand/finger geometry is unmapped or
/// degenerate.
/// </summary>
/// <remarks>
/// Formula (mirror-consistent by construction, verified on the ActorCore fixture by the
/// finger-curl test): <c>dorsal = sideSign · cross(knuckle, fingerDir)</c> with
/// <c>sideSign = +1</c> left / <c>−1</c> right, <c>knuckle = IndexProx.head −
/// PinkyProx.head</c> (first/last mapped non-thumb proximal), and <c>fingerDir =
/// FingerProximalMidpoint − Hand.head</c>. On every fixture rig the thumb proximal lies on
/// the −dorsal (palmar) side of the hand plane, grounding the sign anatomically. A positive
/// rotation about a finger frame's hinge axis (frame Y = cross(dorsal, fingerChainDir))
/// curls the fingertip toward the palm on <b>both</b> hands.
/// </remarks>
public static Vector3? Dorsal(MappingResult map, IReadOnlyList<XForm> worldRest, bool left)
{
if (!map.RoleToBone.TryGetValue(left ? BoneRole.HandL : BoneRole.HandR, out var handIndex))
return null;
var hand = worldRest[handIndex].Pos;
var nonThumb = left ? LeftNonThumbProximals : RightNonThumbProximals;
Vector3? first = null, last = null;
foreach (var role in nonThumb)
{
if (!map.RoleToBone.TryGetValue(role, out var index))
continue;
first ??= worldRest[index].Pos;
last = worldRest[index].Pos;
}
if (first is null || last is null || (first.Value - last.Value).LengthSquared() < 1e-8f)
return null;
var midpoint = FingerProximalMidpoint(map, worldRest, left);
if (midpoint is null)
return null;
var knuckle = first.Value - last.Value;
var fingerDir = midpoint.Value - hand;
var raw = Vector3.Cross(knuckle, fingerDir) * (left ? 1f : -1f);
return raw.LengthSquared() < 1e-8f ? null : Vector3.Normalize(raw);
}
}
Game
library
#nullable enable annotations
using HumanoidRetargeter.Mapping;
using HumanoidRetargeter.Target;
namespace HumanoidRetargeter.Solve;
/// <summary>
/// Bridges a <see cref="TargetRig"/>'s role annotations into the <see cref="MappingResult"/>
/// shape shared with source mappings, so target-side machinery (rest normalization, canonical
/// frames) can run on the exact same code paths as the source side.
/// </summary>
public static class TargetRigMappingExtensions
{
/// <summary>Role → target bone index mapping of the rig's annotated animated bones.</summary>
public static MappingResult ToMappingResult(this TargetRig rig)
{
ArgumentNullException.ThrowIfNull(rig);
var map = new MappingResult(rig.Name, MappingSource.Preset) { Confidence = 1f };
for (var i = 0; i < rig.Skeleton.Count; i++)
{
if (rig.RoleOf(i) is BoneRole role)
map.RoleToBone[role] = i;
}
return map;
}
}
Game
library
#nullable enable annotations // Global usings for the s&box in-engine compiler. // // The plain net8.0 dev harness gets these automatically via <ImplicitUsings>, // but s&box's compiler injects no BCL usings at all - without this file the // library fails to compile inside the editor (CS0246 on List<>, IEnumerable<>, // FormatException, ...). Duplicating the SDK's implicit set is harmless there // (verified: no warnings). global using System; global using System.Collections.Generic; global using System.IO; global using System.Linq; global using System.Threading; global using System.Threading.Tasks; // NOTE on Vector3: s&box declares its own Vector3 in the *global namespace*, // which wins over `using System.Numerics;` imports during name lookup and // breaks this System.Numerics-based core (.X/.Y/.Z, static helpers, delegate // signatures). A global using-alias does NOT fix this (CS0576: alias conflicts // with the global-namespace type at every use site). The working fix is a // *namespace-scoped* alias, declared after the file-scoped namespace line: // // namespace HumanoidRetargeter.Xyz; // using Vector3 = System.Numerics.Vector3; // // Every file in this tree that uses the simple name Vector3 carries that line. // (Quaternion and Matrix4x4 are not global-namespace types in s&box and need // no alias.)
Debug: View Raw JSON Response
{
"TotalCount": 158,
"Files": [
{
"Ident": "notpointless.chomnr_humanoid_retargeter",
"Path": "Code/HumanoidRetargeter/Cleanup/FootGroundAlign.cs",
"FileName": "FootGroundAlign.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 311783,
"Code": "#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Numerics;\r\nusing HumanoidRetargeter.Maths;\r\nusing SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;\r\n\r\nnamespace HumanoidRetargeter.Cleanup;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)\r\n\r\n/// <summary>Tunables for the grounded-foot stance recalibration pass.</summary>\r\npublic sealed class FootGroundAlignOptions\r\n{\r\n /// <summary>\r\n /// Dead zone (degrees): measured stance offsets at or below this are genuine planted\r\n /// articulation (heel-roll bias, natural lean \u2014 measured 2\u20134\u00b0 on well-rested rigs and\r\n /// on citizen clips) and are left untouched, keeping the transfer byte-faithful there.\r\n /// Only offsets beyond it are clearly rest-pose artifacts (measured 12\u201325\u00b0 on the\r\n /// repro rig) and get recalibrated.\r\n /// </summary>\r\n public float MinCorrectionDeg { get; set; } = 8f;\r\n\r\n /// <summary>\r\n /// Maximum mean sole deviation (degrees) a plant may show and still count as a STANCE\r\n /// for the offset measurement. Plants beyond this are not standing on the sole (crawls,\r\n /// kneels, prone contact \u2014 measured 60\u201390\u00b0 there) and are excluded; genuine rest-pose\r\n /// stance artifacts measure well below it (largest seen: 27\u00b0).\r\n /// </summary>\r\n public float MaxStanceDeviationDeg { get; set; } = 35f;\r\n}\r\n\r\n/// <summary>Per-foot results of a <see cref=\"FootGroundAlign.Apply\"/> run.</summary>\r\npublic sealed class FootGroundAlignFootReport\r\n{\r\n /// <summary>Plants that contributed to the stance measurement.</summary>\r\n public int StancePlants { get; set; }\r\n\r\n /// <summary>Plants excluded as non-stance (mean sole deviation beyond\r\n /// <see cref=\"FootGroundAlignOptions.MaxStanceDeviationDeg\"/>).</summary>\r\n public int SkippedPlants { get; set; }\r\n\r\n /// <summary>Measured planted sole offset from the ground plane, degrees (0 when no\r\n /// stance plants exist).</summary>\r\n public float MeasuredOffsetDeg { get; set; }\r\n\r\n /// <summary>Foot correction applied to every frame, degrees (0 = inside the dead zone,\r\n /// nothing changed).</summary>\r\n public float AppliedFootDeg { get; set; }\r\n\r\n /// <summary>Toe correction applied to every frame, degrees.</summary>\r\n public float AppliedToeDeg { get; set; }\r\n}\r\n\r\n/// <summary>Results of a <see cref=\"FootGroundAlign.Apply\"/> run.</summary>\r\npublic sealed class FootGroundAlignReport\r\n{\r\n /// <summary>Left-foot results.</summary>\r\n public required FootGroundAlignFootReport Left { get; init; }\r\n\r\n /// <summary>Right-foot results.</summary>\r\n public required FootGroundAlignFootReport Right { get; init; }\r\n}\r\n\r\n/// <summary>\r\n/// Grounded-foot stance recalibration: measures how far the foot's SOLE sits from the ground\r\n/// plane while planted, and \u2014 when that offset is clearly a rest-pose artifact \u2014 rotates it\r\n/// out with one constant per foot, applied to every frame of the clip.\r\n/// </summary>\r\n/// <remarks>\r\n/// <para><b>Why a cleanup pass.</b> The solver transfers feet as rest-relative deltas\r\n/// (<see cref=\"Solve.RoleTransferMode.CharacterDeltaFromRest\"/>), so the target keeps its own\r\n/// ankle anatomy \u2014 correct whenever the source's rest pose is a flat-footed stance (the delta\r\n/// is then \"deviation from standing\"). Some rigs ship a NON-stance rest (measured: an\r\n/// Auto-Rig-Pro export whose rest foot sits 12\u201325\u00b0 from its planted stance), and that constant\r\n/// offset rides into every frame of the replay \u2014 planted feet hover toe-down/heel-up. What a\r\n/// stance actually looks like is animation evidence (planted phases), which a per-frame\r\n/// solver cannot see, so the recalibration lives here.</para>\r\n/// <para><b>Measurement.</b> Per foot: over every planted frame, the sole normal = rest up\r\n/// carried by the foot's world delta from the target bind rest (whose feet stand on the\r\n/// ground by construction); plants whose own mean normal sits beyond\r\n/// <see cref=\"FootGroundAlignOptions.MaxStanceDeviationDeg\"/> are excluded (crawl/kneel/prone\r\n/// contact is not a stance). The pooled mean normal's deviation from up is the stance\r\n/// offset.</para>\r\n/// <para><b>Correction.</b> Offsets inside <see cref=\"FootGroundAlignOptions.MinCorrectionDeg\"/>\r\n/// are genuine articulation \u2014 nothing is changed (well-rested rigs and same-rig round trips\r\n/// stay byte-identical through this pass). Beyond it, the shortest-arc rotation taking the\r\n/// pooled normal back to up (pitch+roll only \u2014 yaw/toe-out is pose and follows the source)\r\n/// premultiplies the foot's world rotation on EVERY frame: a rest artifact is constant, so\r\n/// the fix is too \u2014 within-plant heel-roll, swing styling and frame-to-frame continuity are\r\n/// preserved exactly, and no blending is needed. The toe then receives its own residual\r\n/// constant measured on top of the corrected foot (it neither double-rotates with the foot\r\n/// fix nor inherits the source toe's own rest artifact). Corrections rotate bones about\r\n/// their own joints: ankle positions are untouched, so the pass composes freely with the\r\n/// <see cref=\"FootPlant\"/> position pinning (which preserves foot world rotations).</para>\r\n/// <para><b>Plant intervals come from the caller</b> (the pipeline detects them on the\r\n/// SOURCE clip via <see cref=\"FootPlant.DetectPlantIntervals\"/> \u2014 ground truth, immune to\r\n/// the hip-height rescaling that can push target-side trajectories outside the cm-tuned\r\n/// Kovar thresholds). So does the decision to run at all: the pipeline invokes this pass\r\n/// only when the source's normalized rest is implausible as a flat stance (toe at/above\r\n/// ankle level or asymmetric feet \u2014 see <c>Retargeter.GroundAlignFeet</c>); on plausible\r\n/// stance rests the solver's rest-relative transfer is already faithful and planted-sole\r\n/// deviations are genuine articulation (boxing stances, heel rolls) that must not be\r\n/// flattened.</para>\r\n/// </remarks>\r\npublic static class FootGroundAlign\r\n{\r\n /// <summary>Measures planted stance offsets and recalibrates feet whose offset is a\r\n /// rest-pose artifact; returns what was measured and done.</summary>\r\n /// <param name=\"frames\">Per-frame local transforms (skeleton bone order); modified in place.</param>\r\n /// <param name=\"skeleton\">Bone hierarchy the frames are expressed against; its bind rest\r\n /// is the flat-stance reference.</param>\r\n /// <param name=\"left\">Left leg chain bone indices.</param>\r\n /// <param name=\"right\">Right leg chain bone indices.</param>\r\n /// <param name=\"up\">World up direction of the clip's space.</param>\r\n /// <param name=\"leftPlants\">Left-foot plant intervals (frame indices into\r\n /// <paramref name=\"frames\"/>; out-of-range parts are clamped/ignored).</param>\r\n /// <param name=\"rightPlants\">Right-foot plant intervals.</param>\r\n /// <param name=\"options\">Tunables; defaults used when null.</param>\r\n public static FootGroundAlignReport Apply(\r\n List<XForm[]> frames,\r\n SkeletonModel skeleton,\r\n FootChain left,\r\n FootChain right,\r\n Vector3 up,\r\n IReadOnlyList<FrameRange> leftPlants,\r\n IReadOnlyList<FrameRange> rightPlants,\r\n FootGroundAlignOptions? options = null)\r\n {\r\n ArgumentNullException.ThrowIfNull(frames);\r\n ArgumentNullException.ThrowIfNull(skeleton);\r\n ArgumentNullException.ThrowIfNull(left);\r\n ArgumentNullException.ThrowIfNull(right);\r\n ArgumentNullException.ThrowIfNull(leftPlants);\r\n ArgumentNullException.ThrowIfNull(rightPlants);\r\n\r\n options ??= new FootGroundAlignOptions();\r\n var report = new FootGroundAlignReport\r\n {\r\n Left = new FootGroundAlignFootReport(),\r\n Right = new FootGroundAlignFootReport(),\r\n };\r\n if (frames.Count == 0 || up.LengthSquared() < 1e-12f)\r\n return report;\r\n up = Vector3.Normalize(up);\r\n\r\n RecalibrateFoot(frames, skeleton, left, up, leftPlants, options, report.Left);\r\n RecalibrateFoot(frames, skeleton, right, up, rightPlants, options, report.Right);\r\n return report;\r\n }\r\n\r\n private static void RecalibrateFoot(\r\n List<XForm[]> frames, SkeletonModel skeleton, FootChain chain, Vector3 up,\r\n IReadOnlyList<FrameRange> plants, FootGroundAlignOptions options,\r\n FootGroundAlignFootReport report)\r\n {\r\n int n = frames.Count;\r\n var foot = chain.Ankle;\r\n var restFootRotInv = Quaternion.Conjugate(skeleton.RestWorld[foot].Rot);\r\n var maxStanceCos = MathF.Cos(options.MaxStanceDeviationDeg * MathF.PI / 180f);\r\n\r\n // ---- measurement: pooled planted sole normal over the stance plants ----\r\n var pooled = Vector3.Zero;\r\n foreach (var plant in plants)\r\n {\r\n int start = Math.Max(plant.Start, 0);\r\n int end = Math.Min(plant.End, n - 1);\r\n if (start > end)\r\n continue;\r\n\r\n var plantSum = Vector3.Zero;\r\n for (int f = start; f <= end; f++)\r\n {\r\n var footRot = FkUtil.BoneWorld(frames[f], skeleton, foot).Rot;\r\n plantSum += Vector3.Transform(up, MathQ.Normalize(footRot * restFootRotInv));\r\n }\r\n if (plantSum.LengthSquared() < 1e-8f\r\n || Vector3.Dot(Vector3.Normalize(plantSum), up) < maxStanceCos)\r\n {\r\n report.SkippedPlants++; // not standing on the sole \u2014 crawl/kneel/toe contact\r\n continue;\r\n }\r\n report.StancePlants++;\r\n pooled += plantSum; // frame-count-weighted: longer stances dominate\r\n }\r\n if (pooled.LengthSquared() < 1e-8f)\r\n return;\r\n pooled = Vector3.Normalize(pooled);\r\n\r\n var offsetDeg = MathQ.AngleBetween(pooled, up) * (180f / MathF.PI);\r\n report.MeasuredOffsetDeg = offsetDeg;\r\n if (offsetDeg <= options.MinCorrectionDeg)\r\n return; // genuine planted articulation \u2014 leave the transfer byte-faithful\r\n\r\n // ---- correction: one constant per foot, every frame ----\r\n var footFix = MathQ.FromTo(pooled, up);\r\n report.AppliedFootDeg = offsetDeg;\r\n\r\n // Toe residual measured on top of the corrected foot, same dead zone.\r\n var toeFix = Quaternion.Identity;\r\n if (chain.Toe is { } toe && skeleton[toe].ParentIndex == foot)\r\n {\r\n var restToeRotInv = Quaternion.Conjugate(skeleton.RestWorld[toe].Rot);\r\n var toePooled = Vector3.Zero;\r\n foreach (var plant in plants)\r\n {\r\n int start = Math.Max(plant.Start, 0);\r\n int end = Math.Min(plant.End, n - 1);\r\n for (int f = start; f <= end && f >= 0; f++)\r\n {\r\n var toeRot = FkUtil.BoneWorld(frames[f], skeleton, toe).Rot;\r\n toePooled += Vector3.Transform(\r\n up, MathQ.Normalize(footFix * toeRot * restToeRotInv));\r\n }\r\n }\r\n if (toePooled.LengthSquared() > 1e-8f)\r\n {\r\n toePooled = Vector3.Normalize(toePooled);\r\n var toeDeg = MathQ.AngleBetween(toePooled, up) * (180f / MathF.PI);\r\n if (toeDeg > options.MinCorrectionDeg && Vector3.Dot(toePooled, up) >= maxStanceCos)\r\n {\r\n toeFix = MathQ.FromTo(toePooled, up);\r\n report.AppliedToeDeg = toeDeg;\r\n }\r\n }\r\n }\r\n\r\n for (int f = 0; f < n; f++)\r\n CorrectFrame(frames[f], skeleton, chain, footFix, toeFix);\r\n }\r\n\r\n /// <summary>Premultiplies the foot's world rotation by the constant fix (the joint\r\n /// position is untouched \u2014 the rotation pivots the foot about its own head), then gives\r\n /// the toe its own residual on top of the corrected foot.</summary>\r\n private static void CorrectFrame(\r\n XForm[] locals, SkeletonModel skeleton, FootChain chain,\r\n Quaternion footFix, Quaternion toeFix)\r\n {\r\n var foot = chain.Ankle;\r\n var parent = skeleton[foot].ParentIndex;\r\n var parentRot = parent < 0\r\n ? Quaternion.Identity\r\n : FkUtil.BoneWorld(locals, skeleton, parent).Rot;\r\n\r\n var footWorld = MathQ.Normalize(parentRot * locals[foot].Rot);\r\n var newFootWorld = MathQ.Normalize(footFix * footWorld);\r\n locals[foot] = new XForm(\r\n locals[foot].Pos, MathQ.Normalize(Quaternion.Conjugate(parentRot) * newFootWorld));\r\n\r\n if (chain.Toe is { } toe && skeleton[toe].ParentIndex == foot)\r\n {\r\n // Desired toe world = toeFix \u2218 footFix \u2218 original world; re-derive its local\r\n // against the corrected foot so it does not double-rotate with the foot fix.\r\n var toeWorldOld = MathQ.Normalize(footWorld * locals[toe].Rot);\r\n var desired = MathQ.Normalize(toeFix * footFix * toeWorldOld);\r\n locals[toe] = new XForm(\r\n locals[toe].Pos, MathQ.Normalize(Quaternion.Conjugate(newFootWorld) * desired));\r\n }\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_retargeter",
"Path": "Code/HumanoidRetargeter/Dl/SameFeatures.cs",
"FileName": "SameFeatures.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 311783,
"Code": "#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Numerics;\r\nusing HumanoidRetargeter.Mapping;\r\nusing HumanoidRetargeter.Maths;\r\nusing HumanoidRetargeter.Skeleton;\r\nusing HumanoidRetargeter.Solve;\r\nusing SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;\r\n\r\nnamespace HumanoidRetargeter.Dl;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)\r\n\r\n/// <summary>The z-normalization statistics shipped with the SAME checkpoint\r\n/// (<c>ms_dict</c>): per-feature mean/std applied to every input except contact.</summary>\r\npublic sealed class SameStats\r\n{\r\n internal float[] LoM, LoS, GoM, GoS, QM, QS, PM, PS, RM, RS, PvM, PvS, QvM, QvS, PprevM, PprevS;\r\n\r\n /// <summary>Reads the 16 <c>ms.*</c> arrays from a parsed weight blob.</summary>\r\n public SameStats(SameWeights weights)\r\n {\r\n ArgumentNullException.ThrowIfNull(weights);\r\n LoM = weights.Stat(\"lo_m\"); LoS = weights.Stat(\"lo_s\");\r\n GoM = weights.Stat(\"go_m\"); GoS = weights.Stat(\"go_s\");\r\n QM = weights.Stat(\"q_m\"); QS = weights.Stat(\"q_s\");\r\n PM = weights.Stat(\"p_m\"); PS = weights.Stat(\"p_s\");\r\n RM = weights.Stat(\"r_m\"); RS = weights.Stat(\"r_s\");\r\n PvM = weights.Stat(\"pv_m\"); PvS = weights.Stat(\"pv_s\");\r\n QvM = weights.Stat(\"qv_m\"); QvS = weights.Stat(\"qv_s\");\r\n PprevM = weights.Stat(\"pprev_m\"); PprevS = weights.Stat(\"pprev_s\");\r\n }\r\n}\r\n\r\n/// <summary>A batched per-frame source graph ready for <see cref=\"SameModel.Encode\"/>.</summary>\r\npublic sealed class SameSourceGraph\r\n{\r\n /// <summary>Normalized node features, flat [FrameCount\u00b7JointCount \u00d7 32].</summary>\r\n public required float[] X { get; init; }\r\n\r\n /// <summary>Edge sources (bidirectional + self-loops, all frames).</summary>\r\n public required int[] EdgeSrc { get; init; }\r\n\r\n /// <summary>Edge destinations.</summary>\r\n public required int[] EdgeDst { get; init; }\r\n\r\n /// <summary>Frame id per node.</summary>\r\n public required int[] Batch { get; init; }\r\n\r\n /// <summary>Number of feature frames (matches the clip's frame count in production\r\n /// mode; native frames \u2212 2 in golden-parity mode).</summary>\r\n public required int FrameCount { get; init; }\r\n\r\n /// <summary>Graph joints per frame (hips subtree + end joints).</summary>\r\n public required int JointCount { get; init; }\r\n\r\n /// <summary>Graph node names within one frame (bone names; synthesized leaf tips get\r\n /// a <c>_end</c> suffix). For diagnostics and parity tests.</summary>\r\n public required string[] JointNames { get; init; }\r\n}\r\n\r\n/// <summary>\r\n/// Source-side feature pipeline of the SAME port (FEASIBILITY.md \"C# port work list\"\r\n/// steps 1\u20135): skeleton normalization, cm/Y-up/+Z-facing alignment, per-frame\r\n/// q/p/r/pv/qv/pprev/c features in the root-facing frame, z-normalization, and the\r\n/// bidirectional+self-loop edge list.\r\n/// </summary>\r\n/// <remarks>\r\n/// <para><b>Skeleton normalization without an intermediate skeleton.</b> SAME's\r\n/// <c>motion_normalize</c> rebuilds the rig with identity rest-local rotations and\r\n/// re-expresses every frame against it. Algebraically the normalized motion's world\r\n/// rotations are exactly the world-space deltas from the T-pose,\r\n/// <c>\u011c(j,t) = G(j,t) \u00b7 G_tpose(j)\u207b\u00b9</c>, its local rotations are\r\n/// <c>\u011c(parent)\u207b\u00b9 \u00b7 \u011c(j)</c>, and its world positions equal the original world positions\r\n/// \u2014 so this port computes the features directly from FK world transforms, no rebuilt\r\n/// skeleton needed (verified against the Python pipeline by the golden-vector tests).</para>\r\n/// <para><b>T-pose reference.</b> SAME consumes the source clip's first frame as the\r\n/// reference; production keeps that convention but emits one feature frame per clip frame\r\n/// (the sequence is computed over [f0, f0\u2026fN\u22121] with f0 doubling as the reference \u2014 see\r\n/// <see cref=\"TposeReference\"/> for why the rest-pose alternative measurably loses).\r\n/// Golden-parity mode replicates Python's frame accounting exactly (frame 0 = reference,\r\n/// frame 1 dropped).</para>\r\n/// <para><b>Alignment.</b> Features assume cm (guaranteed by the importers), Y-up and\r\n/// rest facing +Z with +X to the character's left. The source is rotated by a world\r\n/// alignment derived from the rig's rest geometry (<see cref=\"CharacterFrame\"/> via the\r\n/// mapping when computable, else the file's axis metadata), snapped to the nearest whole\r\n/// axis permutation (an exact-axis rig must map to the identity \u2014 the rest-geometry tilt\r\n/// of a few degrees otherwise leaks into every feature), and shifted so the lowest joint\r\n/// over the clip sits on the ground plane.</para>\r\n/// <para><b>Graph.</b> Nodes are the hips subtree (hips = mapped Hips role, else the\r\n/// shallowest branch bone) in skeleton order \u2014 hips is always node 0, which is where the\r\n/// root feature row lives \u2014 plus one synthesized end joint per childless leaf (BVH End\r\n/// Sites already import as <c>_end</c> bones and are used as-is; FBX leaves get a\r\n/// half-length continuation of their parent segment).</para>\r\n/// </remarks>\r\npublic static class SameFeatures\r\n{\r\n /// <summary>How the T-pose reference (skeleton normalization + lo/go features) is chosen.</summary>\r\n public enum TposeReference\r\n {\r\n /// <summary>The clip's own first frame \u2014 SAME's native convention and the\r\n /// production default. Empirically the pretrained checkpoint tracks arms FAR\r\n /// better against the clip's first frame than against a synthesized true T-pose,\r\n /// even though its training references are T-poses (measured on the fixture clip:\r\n /// mean role cosine vs the geometric solver 0.94 first-frame vs 0.57 rest-pose,\r\n /// hands flipping negative \u2014 reproduced identically in the Python reference\r\n /// pipeline, so it is a property of the checkpoint, not of this port).</summary>\r\n FirstFrame,\r\n\r\n /// <summary>Synthesize the reference from the skeleton's rest pose (the\r\n /// FEASIBILITY suggestion; kept for experiments \u2014 see above for why it lost).</summary>\r\n RestPose,\r\n }\r\n\r\n /// <summary>Options for <see cref=\"BuildSourceGraph\"/>; defaults are production mode.</summary>\r\n public sealed class SourceOptions\r\n {\r\n /// <summary>T-pose reference choice (see <see cref=\"TposeReference\"/>).</summary>\r\n public TposeReference Reference { get; init; } = TposeReference.FirstFrame;\r\n\r\n /// <summary>SAME's native frame accounting: the first frame is consumed as the\r\n /// reference and the next dropped for its undefined velocity, so the output has\r\n /// two frames fewer than the clip. Golden-parity tests only \u2014 production emits\r\n /// one feature frame per clip frame (the first frame doubles as the reference\r\n /// and gets zero velocity).</summary>\r\n public bool NativeFrameDrop { get; init; }\r\n\r\n /// <summary>Apply the rest-geometry world alignment (Y-up, +Z facing). Disabled\r\n /// only by golden-parity tests (Python applies none).</summary>\r\n public bool Align { get; init; } = true;\r\n\r\n /// <summary>Ground both the T-pose reference and the animation: the T-pose is\r\n /// shifted so its lowest joint sits at height 0 (a BVH rest pose has its root at\r\n /// the origin and would otherwise put the hips on the floor), and the animation is\r\n /// shifted by its own lowest joint height over the clip (no-op for the usual\r\n /// authored-ground-at-0 data). Disabled only by golden-parity tests (the Python\r\n /// reference consumes data as authored).</summary>\r\n public bool GroundShift { get; init; } = true;\r\n }\r\n\r\n private const float ContactHeightCm = 5f;\r\n private const float ContactSpeedMps = 0.4f;\r\n private const float VelocityFps = 30f;\r\n\r\n /// <summary>\r\n /// Builds the batched source graph for one clip: graph selection, alignment, per-frame\r\n /// features, normalization, edges.\r\n /// </summary>\r\n /// <param name=\"scene\">Imported source (cm, native axes).</param>\r\n /// <param name=\"clipIndex\">Clip to encode.</param>\r\n /// <param name=\"map\">Source mapping; used only for hips identification and the\r\n /// rest-geometry alignment (the model itself is skeleton-agnostic). May be sparse \u2014\r\n /// heuristics cover missing roles.</param>\r\n /// <param name=\"stats\">Normalization statistics.</param>\r\n /// <param name=\"options\">Null = production mode.</param>\r\n public static SameSourceGraph BuildSourceGraph(\r\n SourceScene scene, int clipIndex, MappingResult? map, SameStats stats, SourceOptions? options = null)\r\n {\r\n ArgumentNullException.ThrowIfNull(scene);\r\n ArgumentNullException.ThrowIfNull(stats);\r\n options ??= new SourceOptions();\r\n if (clipIndex < 0 || clipIndex >= scene.Clips.Count)\r\n throw new ArgumentOutOfRangeException(nameof(clipIndex));\r\n var clip = scene.Clips[clipIndex];\r\n if (clip.FrameCount < 1)\r\n throw new ArgumentException(\"Clip has no frames.\", nameof(clipIndex));\r\n if (options.NativeFrameDrop && clip.FrameCount < 3)\r\n throw new ArgumentException(\"Native frame accounting needs at least 3 frames.\", nameof(options));\r\n\r\n var skeleton = scene.Skeleton;\r\n var hips = FindHips(skeleton, map);\r\n var nodes = GraphNodes.Build(skeleton, hips);\r\n\r\n var align = options.Align ? ComputeAlignment(skeleton, map, scene) : Quaternion.Identity;\r\n\r\n // T-pose reference world transforms (aligned), grounded on its own lowest joint\r\n // (a BVH rest pose has the root at the origin \u2014 ungrounded, its hips would sit on\r\n // the floor and every height-bearing feature would be wrong).\r\n var tposeLocals = options.Reference == TposeReference.RestPose\r\n ? Pose.Rest(skeleton).Locals\r\n : clip.Frames[0];\r\n var tposeWorld = AlignedWorld(skeleton, tposeLocals, align, nodes);\r\n if (options.GroundShift)\r\n ShiftToGround(tposeWorld.Pos);\r\n\r\n // The pose sequence the features run over; features are emitted for seq[1..].\r\n var seq = new List<XForm[]>();\r\n if (options.NativeFrameDrop)\r\n {\r\n for (var f = 1; f < clip.FrameCount; f++)\r\n seq.Add(clip.Frames[f]);\r\n }\r\n else\r\n {\r\n seq.Add(clip.Frames[0]); // duplicated: gives the real first frame zero velocity\r\n for (var f = 0; f < clip.FrameCount; f++)\r\n seq.Add(clip.Frames[f]);\r\n }\r\n\r\n var frames = seq.Count - 1;\r\n var j = nodes.Count;\r\n\r\n // Pass 0: aligned world transforms; ground the whole clip on its lowest joint.\r\n var worlds = new AlignedFrame[seq.Count];\r\n for (var t = 0; t < seq.Count; t++)\r\n worlds[t] = AlignedWorld(skeleton, seq[t], align, nodes);\r\n if (options.GroundShift)\r\n {\r\n var ground = float.PositiveInfinity;\r\n foreach (var world in worlds)\r\n {\r\n foreach (var p in world.Pos)\r\n ground = MathF.Min(ground, p.Y);\r\n }\r\n if (float.IsFinite(ground) && ground != 0f)\r\n {\r\n foreach (var world in worlds)\r\n {\r\n for (var i = 0; i < j; i++)\r\n world.Pos[i].Y -= ground;\r\n }\r\n }\r\n }\r\n\r\n // Pass 1: normalized-skeleton local rotations + facing per frame.\r\n var localRots = new Quaternion[seq.Count][]; // facing-adjusted at the root row\r\n var facing = new (float Yaw, Vector3 Pos)[seq.Count];\r\n for (var t = 0; t < seq.Count; t++)\r\n {\r\n var world = worlds[t];\r\n\r\n // Normalized-skeleton world rotations: world delta from the T-pose.\r\n var normWorld = new Quaternion[j];\r\n for (var i = 0; i < j; i++)\r\n normWorld[i] = MathQ.Normalize(world.Rot[i] * Quaternion.Conjugate(tposeWorld.Rot[i]));\r\n\r\n // Root facing: yaw (about +Y) of the normalized root rotation, at the root's\r\n // ground-plane position.\r\n var yaw = YawAngle(normWorld[0]);\r\n facing[t] = (yaw, new Vector3(world.Pos[0].X, 0f, world.Pos[0].Z));\r\n\r\n // Normalized-skeleton local rotations; root premultiplied by the inverse facing.\r\n var locals = new Quaternion[j];\r\n locals[0] = MathQ.Normalize(Quaternion.CreateFromAxisAngle(Vector3.UnitY, -yaw) * normWorld[0]);\r\n for (var i = 1; i < j; i++)\r\n {\r\n locals[i] = MathQ.Normalize(\r\n Quaternion.Conjugate(normWorld[nodes.Parent[i]]) * normWorld[i]);\r\n }\r\n localRots[t] = locals;\r\n }\r\n\r\n // Pass 2: feature rows.\r\n var x = new float[frames * j * SameModel.InputDim];\r\n for (var t = 1; t < seq.Count; t++)\r\n {\r\n var f = t - 1;\r\n var (yaw, fpos) = facing[t];\r\n var invFacing = Quaternion.CreateFromAxisAngle(Vector3.UnitY, -yaw);\r\n var (yawPrev, fposPrev) = facing[t - 1];\r\n var invFacingPrev = Quaternion.CreateFromAxisAngle(Vector3.UnitY, -yawPrev);\r\n\r\n // r: facing delta (d\u03b8, dx, dz) + absolute root height.\r\n var dTheta = WrapPi(yaw - yawPrev);\r\n var dPlanar = Vector3.Transform(fpos - fposPrev, invFacingPrev);\r\n var rootHeight = worlds[t].Pos[0].Y;\r\n\r\n for (var i = 0; i < j; i++)\r\n {\r\n var row = (f * j + i) * SameModel.InputDim;\r\n var col = 0;\r\n\r\n // ---- skel: lo, go (tiled per frame) -------------------------------------\r\n Vector3 lo, go;\r\n if (i == 0)\r\n {\r\n lo = new Vector3(0f, tposeWorld.Pos[0].Y, 0f);\r\n go = lo;\r\n }\r\n else\r\n {\r\n lo = tposeWorld.Pos[i] - tposeWorld.Pos[nodes.Parent[i]];\r\n go = tposeWorld.Pos[i] - new Vector3(tposeWorld.Pos[0].X, 0f, tposeWorld.Pos[0].Z);\r\n }\r\n WriteNorm3(x, row, ref col, lo, stats.LoM, stats.LoS);\r\n WriteNorm3(x, row, ref col, go, stats.GoM, stats.GoS);\r\n\r\n // ---- q ------------------------------------------------------------------\r\n WriteNorm6(x, row, ref col, SixD(localRots[t][i]), stats.QM, stats.QS);\r\n\r\n // ---- p (facing-frame-relative global position) --------------------------\r\n var p = Vector3.Transform(worlds[t].Pos[i] - fpos, invFacing);\r\n WriteNorm3(x, row, ref col, p, stats.PM, stats.PS);\r\n\r\n // ---- r (root row only; other rows are the mean \u2192 zeros after norm) ------\r\n if (i == 0)\r\n {\r\n x[row + col++] = (dTheta - stats.RM[0]) / stats.RS[0];\r\n x[row + col++] = (dPlanar.X - stats.RM[1]) / stats.RS[1];\r\n x[row + col++] = (dPlanar.Z - stats.RM[2]) / stats.RS[2];\r\n x[row + col++] = (rootHeight - stats.RM[3]) / stats.RS[3];\r\n }\r\n else\r\n {\r\n col += 4; // already zero\r\n }\r\n\r\n // ---- pv (facing-frame velocity, \u00d730 fps) ---------------------------------\r\n var pv = Vector3.Transform(worlds[t].Pos[i] - worlds[t - 1].Pos[i], invFacing) * VelocityFps;\r\n WriteNorm3(x, row, ref col, pv, stats.PvM, stats.PvS);\r\n\r\n // ---- qv (local rotation delta) -------------------------------------------\r\n var qv = MathQ.Normalize(Quaternion.Conjugate(localRots[t - 1][i]) * localRots[t][i]);\r\n WriteNorm6(x, row, ref col, SixD(qv), stats.QvM, stats.QvS);\r\n\r\n // ---- pprev (previous position in the CURRENT facing frame) ---------------\r\n var pprev = Vector3.Transform(worlds[t - 1].Pos[i] - fpos, invFacing);\r\n WriteNorm3(x, row, ref col, pprev, stats.PprevM, stats.PprevS);\r\n\r\n // ---- c (ground contact; not normalized) -----------------------------------\r\n var speedMps = (worlds[t].Pos[i] - worlds[t - 1].Pos[i]).Length() * VelocityFps / 100f;\r\n x[row + col] = worlds[t].Pos[i].Y < ContactHeightCm && speedMps < ContactSpeedMps ? 1f : 0f;\r\n }\r\n }\r\n\r\n var (edgeSrc, edgeDst) = BuildEdges(nodes.Parent, frames);\r\n var batch = new int[frames * j];\r\n for (var f = 0; f < frames; f++)\r\n {\r\n for (var i = 0; i < j; i++)\r\n batch[f * j + i] = f;\r\n }\r\n\r\n AssertFinite(x, \"SAME source features\");\r\n return new SameSourceGraph\r\n {\r\n X = x,\r\n EdgeSrc = edgeSrc,\r\n EdgeDst = edgeDst,\r\n Batch = batch,\r\n FrameCount = frames,\r\n JointCount = j,\r\n JointNames = nodes.Names,\r\n };\r\n }\r\n\r\n // ================================================================ graph topology\r\n\r\n /// <summary>The per-frame graph node set: hips-subtree bones in skeleton order\r\n /// (hips first) plus synthesized end joints for childless leaves.</summary>\r\n internal sealed class GraphNodes\r\n {\r\n /// <summary>Skeleton bone index per node; -1 for synthesized end joints.</summary>\r\n public required int[] Bone { get; init; }\r\n\r\n /// <summary>Graph-parent node index; -1 for the root (node 0).</summary>\r\n public required int[] Parent { get; init; }\r\n\r\n /// <summary>For synthesized end joints: the rest-local offset from the leaf bone\r\n /// (zero vector for real bones).</summary>\r\n public required Vector3[] EndOffset { get; init; }\r\n\r\n public required string[] Names { get; init; }\r\n\r\n public int Count => Bone.Length;\r\n\r\n public static GraphNodes Build(SkeletonModel skeleton, int hips)\r\n {\r\n // Hips subtree, skeleton order (parents precede children, hips first).\r\n var inSubtree = new bool[skeleton.Count];\r\n inSubtree[hips] = true;\r\n var bones = new List<int> { hips };\r\n for (var i = hips + 1; i < skeleton.Count; i++)\r\n {\r\n var parent = skeleton[i].ParentIndex;\r\n if (parent >= 0 && inSubtree[parent])\r\n {\r\n inSubtree[i] = true;\r\n bones.Add(i);\r\n }\r\n }\r\n\r\n var nodeOfBone = new Dictionary<int, int>(bones.Count);\r\n for (var n = 0; n < bones.Count; n++)\r\n nodeOfBone[bones[n]] = n;\r\n\r\n var hasChild = new bool[skeleton.Count];\r\n foreach (var b in bones)\r\n {\r\n var parent = skeleton[b].ParentIndex;\r\n if (parent >= 0 && inSubtree[parent])\r\n hasChild[parent] = true;\r\n }\r\n\r\n var bone = new List<int>(bones);\r\n var parentNode = new List<int>(bones.Count);\r\n var endOffset = new List<Vector3>(bones.Count);\r\n var names = new List<string>(bones.Count);\r\n foreach (var b in bones)\r\n {\r\n var p = skeleton[b].ParentIndex;\r\n parentNode.Add(b == hips ? -1 : nodeOfBone[p]);\r\n endOffset.Add(Vector3.Zero);\r\n names.Add(skeleton[b].Name);\r\n }\r\n\r\n // Synthesized end joints: leaves with no children anywhere in the skeleton.\r\n // BVH End Sites already import as real `_end`/`_End` bones and ARE the end\r\n // joints \u2014 no tip on a tip. The tip continues the parent\u2192leaf segment at half\r\n // length \u2014 a neutral stand-in for the unknown bone tail (FBX carries none).\r\n foreach (var b in bones)\r\n {\r\n if (hasChild[b]\r\n || skeleton[b].Name.EndsWith(\"_end\", StringComparison.OrdinalIgnoreCase))\r\n continue;\r\n var p = skeleton[b].ParentIndex;\r\n var segment = p >= 0\r\n ? skeleton.RestWorld[b].Pos - skeleton.RestWorld[p].Pos\r\n : Vector3.Zero;\r\n var tip = segment.Length() > 1e-4f ? segment * 0.5f : new Vector3(0f, 2f, 0f);\r\n // Express in the leaf's rest-local frame (applied via the leaf's world rot).\r\n var local = Vector3.Transform(tip, Quaternion.Conjugate(skeleton.RestWorld[b].Rot));\r\n bone.Add(-1);\r\n parentNode.Add(nodeOfBone[b]);\r\n endOffset.Add(local);\r\n names.Add(skeleton[b].Name + \"_end\");\r\n }\r\n\r\n return new GraphNodes\r\n {\r\n Bone = bone.ToArray(),\r\n Parent = parentNode.ToArray(),\r\n EndOffset = endOffset.ToArray(),\r\n Names = names.ToArray(),\r\n };\r\n }\r\n }\r\n\r\n /// <summary>Aligned world transforms of the graph nodes for one pose.</summary>\r\n internal readonly struct AlignedFrame\r\n {\r\n public required Vector3[] Pos { get; init; }\r\n public required Quaternion[] Rot { get; init; }\r\n }\r\n\r\n private static AlignedFrame AlignedWorld(\r\n SkeletonModel skeleton, XForm[] locals, Quaternion align, GraphNodes nodes)\r\n {\r\n var world = new Pose(locals).ToWorld(skeleton);\r\n var pos = new Vector3[nodes.Count];\r\n var rot = new Quaternion[nodes.Count];\r\n for (var n = 0; n < nodes.Count; n++)\r\n {\r\n XForm w;\r\n if (nodes.Bone[n] >= 0)\r\n {\r\n w = world[nodes.Bone[n]];\r\n }\r\n else\r\n {\r\n // Synthesized end joint: rides its leaf bone (identity local rotation).\r\n var leaf = world[nodes.Bone[nodes.Parent[n]]];\r\n w = new XForm(leaf.TransformPoint(nodes.EndOffset[n]), leaf.Rot);\r\n }\r\n pos[n] = Vector3.Transform(w.Pos, align);\r\n rot[n] = MathQ.Normalize(align * w.Rot);\r\n }\r\n return new AlignedFrame { Pos = pos, Rot = rot };\r\n }\r\n\r\n /// <summary>Bidirectional parent\u2194child pairs plus one self-loop per node, replicated\r\n /// per frame with node indices offset.</summary>\r\n internal static (int[] Src, int[] Dst) BuildEdges(int[] parent, int frames)\r\n {\r\n var j = parent.Length;\r\n var nonRoot = 0;\r\n for (var i = 0; i < j; i++)\r\n {\r\n if (parent[i] >= 0)\r\n nonRoot++;\r\n }\r\n var perFrame = nonRoot * 2 + j;\r\n var src = new int[perFrame * frames];\r\n var dst = new int[perFrame * frames];\r\n var e = 0;\r\n for (var f = 0; f < frames; f++)\r\n {\r\n var offset = f * j;\r\n for (var i = 0; i < j; i++)\r\n {\r\n if (parent[i] < 0)\r\n continue;\r\n src[e] = offset + parent[i];\r\n dst[e] = offset + i;\r\n e++;\r\n src[e] = offset + i;\r\n dst[e] = offset + parent[i];\r\n e++;\r\n }\r\n for (var i = 0; i < j; i++)\r\n {\r\n src[e] = offset + i;\r\n dst[e] = offset + i;\r\n e++;\r\n }\r\n }\r\n return (src, dst);\r\n }\r\n\r\n // ================================================================ alignment + hips\r\n\r\n /// <summary>Mapped Hips role when available, else the shallowest bone with two or more\r\n /// children (the hips of any humanoid: the legs/spine branch point).</summary>\r\n internal static int FindHips(SkeletonModel skeleton, MappingResult? map)\r\n {\r\n if (map is not null && map.RoleToBone.TryGetValue(BoneRole.Hips, out var mapped)\r\n && mapped >= 0 && mapped < skeleton.Count)\r\n return mapped;\r\n\r\n var childCount = new int[skeleton.Count];\r\n for (var i = 0; i < skeleton.Count; i++)\r\n {\r\n if (skeleton[i].ParentIndex >= 0)\r\n childCount[skeleton[i].ParentIndex]++;\r\n }\r\n\r\n var best = -1;\r\n var bestDepth = int.MaxValue;\r\n for (var i = 0; i < skeleton.Count; i++)\r\n {\r\n if (childCount[i] < 2)\r\n continue;\r\n var depth = 0;\r\n for (var a = skeleton[i].ParentIndex; a >= 0; a = skeleton[a].ParentIndex)\r\n depth++;\r\n if (depth < bestDepth)\r\n {\r\n best = i;\r\n bestDepth = depth;\r\n }\r\n }\r\n return best >= 0 ? best : 0;\r\n }\r\n\r\n /// <summary>\r\n /// World rotation taking the rig into the canonical SAME frame (X = character left,\r\n /// Y = up, Z = facing): rest-geometry character frame when computable from the mapping,\r\n /// else the file's recorded axis conventions.\r\n /// </summary>\r\n internal static Quaternion ComputeAlignment(SkeletonModel skeleton, MappingResult? map, SourceScene? scene)\r\n {\r\n if (map is not null)\r\n {\r\n try\r\n {\r\n var frame = CharacterFrame.Compute(skeleton, map, skeleton.RestWorld);\r\n return AlignFromBasis(frame.Lateral, frame.Up, frame.Forward);\r\n }\r\n catch (ArgumentException)\r\n {\r\n // fall through to axis metadata\r\n }\r\n }\r\n\r\n if (scene is not null)\r\n {\r\n var up = AxisVector(scene.UpAxis, scene.UpAxisSign);\r\n var forward = AxisVector(scene.FrontAxis, scene.FrontAxisSign);\r\n if (MathF.Abs(Vector3.Dot(up, forward)) < 0.5f)\r\n return AlignFromBasis(Vector3.Cross(up, forward), up, forward);\r\n }\r\n\r\n return Quaternion.Identity;\r\n }\r\n\r\n /// <summary>\r\n /// Rotation mapping the given (left, up, forward) world directions onto (+X, +Y, +Z),\r\n /// snapped to the nearest whole axis permutation when one is unambiguous: rigs authored\r\n /// on exact axes (BVH Y-up/+Z, the s&box rig, Z-up FBX) must map by an exact\r\n /// quarter-turn \u2014 the few degrees of rest-geometry tilt (shoulders not exactly above\r\n /// hips) otherwise leak into every feature and measurably cost accuracy.\r\n /// </summary>\r\n internal static Quaternion AlignFromBasis(Vector3 left, Vector3 up, Vector3 forward)\r\n {\r\n var l = SnapAxis(left);\r\n var u = SnapAxis(up);\r\n var f = SnapAxis(forward);\r\n if (MathF.Abs(Vector3.Dot(l, u)) > 0.5f || MathF.Abs(Vector3.Dot(l, f)) > 0.5f\r\n || MathF.Abs(Vector3.Dot(u, f)) > 0.5f)\r\n {\r\n // Genuinely oblique rig: keep the exact (orthonormalized) directions.\r\n l = Vector3.Normalize(left);\r\n u = Vector3.Normalize(up - l * Vector3.Dot(up, l));\r\n f = Vector3.Cross(l, u);\r\n }\r\n\r\n // Row-major with rows = basis images maps +X\u2192left, +Y\u2192up, +Z\u2192forward\r\n // (System.Numerics row-vector convention); the alignment is its inverse.\r\n var m = new Matrix4x4(\r\n l.X, l.Y, l.Z, 0f,\r\n u.X, u.Y, u.Z, 0f,\r\n f.X, f.Y, f.Z, 0f,\r\n 0f, 0f, 0f, 1f);\r\n return Quaternion.Conjugate(MathQ.Normalize(Quaternion.CreateFromRotationMatrix(m)));\r\n }\r\n\r\n private static Vector3 SnapAxis(Vector3 v)\r\n {\r\n var ax = MathF.Abs(v.X);\r\n var ay = MathF.Abs(v.Y);\r\n var az = MathF.Abs(v.Z);\r\n if (ax >= ay && ax >= az)\r\n return new Vector3(MathF.Sign(v.X), 0f, 0f);\r\n if (ay >= az)\r\n return new Vector3(0f, MathF.Sign(v.Y), 0f);\r\n return new Vector3(0f, 0f, MathF.Sign(v.Z));\r\n }\r\n\r\n private static Vector3 AxisVector(int axis, int sign) => axis switch\r\n {\r\n 0 => new Vector3(sign, 0f, 0f),\r\n 2 => new Vector3(0f, 0f, sign),\r\n _ => new Vector3(0f, sign, 0f),\r\n };\r\n\r\n // ================================================================ small math\r\n\r\n /// <summary>The yaw (rotation about +Y) closest to <paramref name=\"q\"/> \u2014 fairmotion's\r\n /// <c>Q_closest(q, identity, +Y)</c>, reproduced exactly for parity.</summary>\r\n internal static float YawAngle(Quaternion q)\r\n {\r\n var alpha = Math.Atan2(q.W, q.Y);\r\n var theta1 = -2.0 * alpha + Math.PI;\r\n var theta2 = -2.0 * alpha - Math.PI;\r\n var d1 = q.Y * Math.Sin(theta1 * 0.5) + q.W * Math.Cos(theta1 * 0.5);\r\n var d2 = q.Y * Math.Sin(theta2 * 0.5) + q.W * Math.Cos(theta2 * 0.5);\r\n return (float)(d1 > d2 ? theta1 : theta2);\r\n }\r\n\r\n private static void ShiftToGround(Vector3[] positions)\r\n {\r\n var ground = float.PositiveInfinity;\r\n foreach (var p in positions)\r\n ground = MathF.Min(ground, p.Y);\r\n if (!float.IsFinite(ground) || ground == 0f)\r\n return;\r\n for (var i = 0; i < positions.Length; i++)\r\n positions[i].Y -= ground;\r\n }\r\n\r\n internal static float WrapPi(float angle)\r\n {\r\n while (angle > MathF.PI)\r\n angle -= 2f * MathF.PI;\r\n while (angle < -MathF.PI)\r\n angle += 2f * MathF.PI;\r\n return angle;\r\n }\r\n\r\n /// <summary>6D rotation representation: the first two columns of the rotation matrix\r\n /// (<c>R\u00b7e_x</c> then <c>R\u00b7e_y</c>).</summary>\r\n internal static (Vector3 C0, Vector3 C1) SixD(Quaternion q)\r\n => (Vector3.Transform(Vector3.UnitX, q), Vector3.Transform(Vector3.UnitY, q));\r\n\r\n private static void WriteNorm3(float[] x, int row, ref int col, Vector3 v, float[] m, float[] s)\r\n {\r\n x[row + col++] = (v.X - m[0]) / s[0];\r\n x[row + col++] = (v.Y - m[1]) / s[1];\r\n x[row + col++] = (v.Z - m[2]) / s[2];\r\n }\r\n\r\n private static void WriteNorm6(float[] x, int row, ref int col, (Vector3 C0, Vector3 C1) sixD, float[] m, float[] s)\r\n {\r\n x[row + col++] = (sixD.C0.X - m[0]) / s[0];\r\n x[row + col++] = (sixD.C0.Y - m[1]) / s[1];\r\n x[row + col++] = (sixD.C0.Z - m[2]) / s[2];\r\n x[row + col++] = (sixD.C1.X - m[3]) / s[3];\r\n x[row + col++] = (sixD.C1.Y - m[4]) / s[4];\r\n x[row + col++] = (sixD.C1.Z - m[5]) / s[5];\r\n }\r\n\r\n internal static void AssertFinite(float[] values, string what)\r\n {\r\n foreach (var v in values)\r\n {\r\n if (!float.IsFinite(v))\r\n throw new InvalidOperationException($\"{what} contain non-finite values.\");\r\n }\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_retargeter",
"Path": "Code/HumanoidRetargeter/Formats/Fbx/FbxBinaryWriter.cs",
"FileName": "FbxBinaryWriter.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 311783,
"Code": "#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Buffers.Binary;\r\nusing System.IO;\r\nusing System.Text;\r\n\r\nnamespace HumanoidRetargeter.Formats.Fbx;\r\n\r\n/// <summary>\r\n/// Serializes an <see cref=\"FbxNode\"/> tree back to binary FBX (version 7400 layout \u2014\r\n/// u32 header fields, universally readable). The inverse of\r\n/// <see cref=\"FbxTokenizer.Parse\"/>: a tree parsed from a 7.x binary file and written\r\n/// here re-parses to an identical tree (arrays are written uncompressed; zlib-encoded\r\n/// inputs therefore round-trip by VALUE, not byte-for-byte).\r\n/// </summary>\r\n/// <remarks>\r\n/// Used by <see cref=\"FbxBindPoseFixer\"/> to persist repaired node transforms. The footer\r\n/// is written the way Blender's exporter does: a fixed 16-byte watermark (importers treat\r\n/// it as opaque), zero padding to a 16-byte boundary, the version echo, 120 zero bytes and\r\n/// the closing magic. The FBX SDK computes a content hash here, but every consumer we\r\n/// target (s&box, Blender, assimp) ignores it.\r\n/// </remarks>\r\npublic static class FbxBinaryWriter\r\n{\r\n private const uint Version = 7400;\r\n\r\n private static readonly byte[] HeaderMagic =\r\n \"Kaydara FBX Binary \\0\\x1a\\0\"u8.ToArray();\r\n\r\n // Blender's fbx_binary.py FOOT_ID + closing magic bytes.\r\n private static readonly byte[] FooterWatermark =\r\n {\r\n 0xfa, 0xbc, 0xab, 0x09, 0xd0, 0xc8, 0xd4, 0x66, 0xb1, 0x76, 0xfb, 0x83, 0x1c, 0xf7, 0x26, 0x7e,\r\n };\r\n\r\n private static readonly byte[] FooterMagic =\r\n {\r\n 0xf8, 0x5a, 0x8c, 0x6a, 0xde, 0xf5, 0xd9, 0x7e, 0xec, 0xe9, 0x0c, 0x6e, 0x0c, 0xc0, 0x00, 0x00,\r\n };\r\n\r\n /// <summary>\r\n /// Serializes <paramref name=\"root\"/> (a virtual root whose children are the top-level\r\n /// document nodes, as produced by <see cref=\"FbxTokenizer.Parse\"/>).\r\n /// </summary>\r\n public static byte[] Write(FbxNode root)\r\n {\r\n ArgumentNullException.ThrowIfNull(root);\r\n\r\n using var ms = new MemoryStream();\r\n ms.Write(HeaderMagic);\r\n WriteU32(ms, Version);\r\n\r\n foreach (var child in root.Children)\r\n WriteNode(ms, child);\r\n WriteNullRecord(ms);\r\n\r\n WriteFooter(ms);\r\n return ms.ToArray();\r\n }\r\n\r\n // ------------------------------------------------------------------ nodes\r\n\r\n private static void WriteNode(MemoryStream ms, FbxNode node)\r\n {\r\n long headerAt = ms.Position;\r\n // Placeholder header: endOffset, numProps, propListLen (patched after the body).\r\n WriteU32(ms, 0);\r\n WriteU32(ms, (uint)node.Properties.Count);\r\n WriteU32(ms, 0);\r\n var nameBytes = Encoding.ASCII.GetBytes(node.Name);\r\n if (nameBytes.Length > byte.MaxValue)\r\n throw new FormatException($\"FBX write: node name too long ({node.Name.Length} chars).\");\r\n ms.WriteByte((byte)nameBytes.Length);\r\n ms.Write(nameBytes);\r\n\r\n long propsAt = ms.Position;\r\n foreach (var p in node.Properties)\r\n WriteProperty(ms, p, node.Name);\r\n long propsLen = ms.Position - propsAt;\r\n\r\n if (node.Children.Count > 0)\r\n {\r\n foreach (var child in node.Children)\r\n WriteNode(ms, child);\r\n WriteNullRecord(ms);\r\n }\r\n\r\n long endAt = ms.Position;\r\n ms.Position = headerAt;\r\n WriteU32(ms, checked((uint)endAt));\r\n WriteU32(ms, (uint)node.Properties.Count);\r\n WriteU32(ms, checked((uint)propsLen));\r\n ms.Position = endAt;\r\n }\r\n\r\n private static void WriteNullRecord(MemoryStream ms)\r\n {\r\n Span<byte> zeros = stackalloc byte[13];\r\n zeros.Clear();\r\n ms.Write(zeros);\r\n }\r\n\r\n // ------------------------------------------------------------------ properties\r\n\r\n private static void WriteProperty(MemoryStream ms, object value, string owner)\r\n {\r\n switch (value)\r\n {\r\n case short y:\r\n ms.WriteByte((byte)'Y');\r\n WriteI16(ms, y);\r\n break;\r\n case bool c:\r\n ms.WriteByte((byte)'C');\r\n ms.WriteByte(c ? (byte)1 : (byte)0);\r\n break;\r\n case int i:\r\n ms.WriteByte((byte)'I');\r\n WriteI32(ms, i);\r\n break;\r\n case float f:\r\n ms.WriteByte((byte)'F');\r\n WriteF32(ms, f);\r\n break;\r\n case double d:\r\n ms.WriteByte((byte)'D');\r\n WriteF64(ms, d);\r\n break;\r\n case long l:\r\n ms.WriteByte((byte)'L');\r\n WriteI64(ms, l);\r\n break;\r\n\r\n case float[] fa:\r\n WriteArrayHeader(ms, 'f', fa.Length, 4);\r\n foreach (var x in fa)\r\n WriteF32(ms, x);\r\n break;\r\n case double[] da:\r\n WriteArrayHeader(ms, 'd', da.Length, 8);\r\n foreach (var x in da)\r\n WriteF64(ms, x);\r\n break;\r\n case long[] la:\r\n WriteArrayHeader(ms, 'l', la.Length, 8);\r\n foreach (var x in la)\r\n WriteI64(ms, x);\r\n break;\r\n case int[] ia:\r\n WriteArrayHeader(ms, 'i', ia.Length, 4);\r\n foreach (var x in ia)\r\n WriteI32(ms, x);\r\n break;\r\n case bool[] ba:\r\n WriteArrayHeader(ms, 'b', ba.Length, 1);\r\n foreach (var x in ba)\r\n ms.WriteByte(x ? (byte)1 : (byte)0);\r\n break;\r\n\r\n case string s:\r\n {\r\n ms.WriteByte((byte)'S');\r\n var bytes = Encoding.UTF8.GetBytes(s);\r\n WriteU32(ms, (uint)bytes.Length);\r\n ms.Write(bytes);\r\n break;\r\n }\r\n case byte[] r:\r\n ms.WriteByte((byte)'R');\r\n WriteU32(ms, (uint)r.Length);\r\n ms.Write(r);\r\n break;\r\n\r\n default:\r\n throw new FormatException(\r\n $\"FBX write: node '{owner}': unsupported property CLR type {value?.GetType().Name ?? \"null\"}.\");\r\n }\r\n }\r\n\r\n private static void WriteArrayHeader(MemoryStream ms, char code, int count, int elemSize)\r\n {\r\n ms.WriteByte((byte)code);\r\n WriteU32(ms, (uint)count);\r\n WriteU32(ms, 0); // encoding 0 = uncompressed\r\n WriteU32(ms, checked((uint)(count * elemSize)));\r\n }\r\n\r\n // ------------------------------------------------------------------ footer\r\n\r\n private static void WriteFooter(MemoryStream ms)\r\n {\r\n ms.Write(FooterWatermark);\r\n\r\n // Zero-pad so the version echo starts 16-aligned (Blender pads at least 1 byte).\r\n int pad = (int)(16 - ms.Position % 16);\r\n for (int i = 0; i < pad; i++)\r\n ms.WriteByte(0);\r\n\r\n WriteU32(ms, Version);\r\n Span<byte> zeros = stackalloc byte[120];\r\n zeros.Clear();\r\n ms.Write(zeros);\r\n ms.Write(FooterMagic);\r\n }\r\n\r\n // ------------------------------------------------------------------ primitives\r\n\r\n private static void WriteU32(MemoryStream ms, uint v)\r\n {\r\n Span<byte> b = stackalloc byte[4];\r\n BinaryPrimitives.WriteUInt32LittleEndian(b, v);\r\n ms.Write(b);\r\n }\r\n\r\n private static void WriteI16(MemoryStream ms, short v)\r\n {\r\n Span<byte> b = stackalloc byte[2];\r\n BinaryPrimitives.WriteInt16LittleEndian(b, v);\r\n ms.Write(b);\r\n }\r\n\r\n private static void WriteI32(MemoryStream ms, int v)\r\n {\r\n Span<byte> b = stackalloc byte[4];\r\n BinaryPrimitives.WriteInt32LittleEndian(b, v);\r\n ms.Write(b);\r\n }\r\n\r\n private static void WriteI64(MemoryStream ms, long v)\r\n {\r\n Span<byte> b = stackalloc byte[8];\r\n BinaryPrimitives.WriteInt64LittleEndian(b, v);\r\n ms.Write(b);\r\n }\r\n\r\n private static void WriteF32(MemoryStream ms, float v)\r\n {\r\n Span<byte> b = stackalloc byte[4];\r\n BinaryPrimitives.WriteSingleLittleEndian(b, v);\r\n ms.Write(b);\r\n }\r\n\r\n private static void WriteF64(MemoryStream ms, double v)\r\n {\r\n Span<byte> b = stackalloc byte[8];\r\n BinaryPrimitives.WriteDoubleLittleEndian(b, v);\r\n ms.Write(b);\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_retargeter",
"Path": "Code/HumanoidRetargeter/Mapping/ProfileLibrary.cs",
"FileName": "ProfileLibrary.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 311783,
"Code": "#nullable enable annotations\r\n\r\nusing System.Collections.Generic;\r\n\r\nnamespace HumanoidRetargeter.Mapping;\r\n\r\n/// <summary>\r\n/// Built-in preset profiles, embedded as C# data (the same data is written to\r\n/// <c>Assets/humanoid_retargeter/profiles/*.json</c> by a regenerate-and-diff test so the\r\n/// shipped JSON can never drift from the code).\r\n/// </summary>\r\npublic static class ProfileLibrary\r\n{\r\n /// <summary>Mixamo / Adobe rigs: <c>mixamorig[N]:</c> namespace, <c>LeftArm</c> /\r\n /// <c>LeftForeArm</c> / <c>LeftHandIndex1..3</c> style names.</summary>\r\n public static Profile Mixamo { get; } = BuildMixamo();\r\n\r\n /// <summary>\r\n /// Reallusion ActorCore / AccuRig / Character Creator rigs (<c>CC_Base_*</c>).\r\n /// Empirical notes from <c>research/rig_actorcore.json</c>:\r\n /// <list type=\"bullet\">\r\n /// <item><c>CC_Base_Hip</c> is the parent of BOTH <c>CC_Base_Pelvis</c> (leg branch) and\r\n /// <c>CC_Base_Waist</c> (spine branch), i.e. the LCA of legs+spine and the true animated\r\n /// hips root \u2192 it carries <see cref=\"BoneRole.Hips\"/>; <c>CC_Base_Pelvis</c> is a\r\n /// leg-branch intermediate and stays unmapped.</item>\r\n /// <item>The neck chain is <c>CC_Base_NeckTwist01 \u2192 CC_Base_NeckTwist02 \u2192 CC_Base_Head</c>;\r\n /// despite the name, <c>NeckTwist01</c> IS the neck bone (there is no plain\r\n /// <c>CC_Base_Neck</c>), so it is the <see cref=\"BoneRole.Neck\"/> alias. NeckTwist02 is\r\n /// left unmapped. All other Twist/ShareBone helpers are excluded (no aliases).</item>\r\n /// <item><c>CC_Base_L_ToeBase</c> is the toe role; the co-located\r\n /// <c>CC_Base_L_ToeBaseShareBone</c> is a helper and must never be mapped.</item>\r\n /// </list>\r\n /// </summary>\r\n public static Profile ActorCoreCc { get; } = BuildActorCoreCc();\r\n\r\n /// <summary>Unreal Engine mannequin (UE4/UE5): <c>pelvis</c>, <c>spine_01..05</c>,\r\n /// <c>clavicle_l</c>, <c>thumb_01_l</c>, UE5 <c>*_metacarpal_*</c>; <c>*_twist_*</c>\r\n /// bones have no aliases and are never mapped.</summary>\r\n public static Profile UeMannequin { get; } = BuildUeMannequin();\r\n\r\n /// <summary>Rokoko / Xsens style BVH rigs: plain <c>Hips</c>/<c>Spine..Spine4</c>/<c>\r\n /// LeftArm|LeftUpperArm</c> name variants, usually no fingers.</summary>\r\n public static Profile RokokoBvh { get; } = BuildRokokoBvh();\r\n\r\n /// <summary>\r\n /// SMPL body model family (AMASS exports, Meshcapade FBX rigs). Joint names per the\r\n /// published model (vchoutas/smplx <c>joint_names.py</c>, Meshcapade wiki):\r\n /// <c>pelvis</c>, sided <c>hip\u2192knee\u2192ankle\u2192foot</c> legs (the \"hip\" joint IS the thigh;\r\n /// \"ankle\" is the foot, \"foot\" is the toe region) and <c>collar\u2192shoulder\u2192elbow\u2192wrist</c>\r\n /// arms (\"shoulder\" is the upper arm, \"wrist\" is the hand; the <c>hand</c> joint is a\r\n /// finger stub and stays unmapped). Both spellings occur in the wild: <c>left_hip</c>\r\n /// (model joints) and <c>L_Hip</c> with gendered FBX prefixes <c>m_avg_</c>/<c>f_avg_</c>\r\n /// (SMPL Unity/FBX rigs). No fingers \u2014 that is SMPL-X (<see cref=\"SmplX\"/>), kept as a\r\n /// separate preset so a finger-less SMPL rig still reaches full optional coverage.\r\n /// </summary>\r\n public static Profile Smpl { get; } = BuildSmpl(withFingers: false);\r\n\r\n /// <summary>\r\n /// SMPL-X: the SMPL body joints (<see cref=\"Smpl\"/>) plus articulated hands \u2014\r\n /// <c>left_thumb1..3</c>/<c>left_index1..3</c>-style finger joints per\r\n /// vchoutas/smplx <c>joint_names.py</c> (jaw/eye joints carry no humanoid role).\r\n /// Evaluated before <see cref=\"Smpl\"/> so it wins the tie on SMPL-X rigs (both score\r\n /// the body fully; only this one maps the fingers).\r\n /// </summary>\r\n public static Profile SmplX { get; } = BuildSmpl(withFingers: true);\r\n\r\n /// <summary>\r\n /// NVIDIA SOMA uniform-proportion skeleton (SOMA/SEED BVH exports, e.g.\r\n /// github.com/NVIDIA/soma-retargeter <c>assets/motions/bvh</c>). Mixamo-like upper-body\r\n /// names, but: spine is <c>Spine1\u2192Spine2\u2192Chest</c> (no plain \"Spine\"), neck is\r\n /// <c>Neck1\u2192Neck2</c>, the legs are <c>LeftLeg\u2192LeftShin</c> \u2014 SOMA's <c>LeftLeg</c> is\r\n /// the THIGH (mixamo's is the calf), which is exactly why the mixamo preset must never\r\n /// claim these rigs \u2014 and the four fingers have FOUR segments where segment 1 is a\r\n /// metacarpal (<c>LeftHandIndex1..4</c>; mixamo's 1..3 are the phalanges), so the\r\n /// phalanx roles map to segments 2/3/4.\r\n /// </summary>\r\n public static Profile SomaBvh { get; } = BuildSomaBvh();\r\n\r\n /// <summary>\r\n /// Classic BVH / Character-Studio-friendly naming (MotionBuilder \"Export BVH to\r\n /// Character Studio\" convention, ACCAD-style mocap BVHs): <c>Hips</c>,\r\n /// <c>Chest[2..4]</c> spine, arms <c>Collar\u2192Shoulder\u2192Elbow\u2192Wrist</c> (the \"Shoulder\"\r\n /// is the upper arm) and legs <c>Hip\u2192Knee\u2192Ankle\u2192Toe</c> (the sided \"Hip\" is the\r\n /// thigh). No fingers.\r\n /// </summary>\r\n public static Profile ClassicBvh { get; } = BuildClassicBvh();\r\n\r\n /// <summary>\r\n /// 3ds Max Character Studio Biped rigs: every bone is \"<BipedName> <Part>\"\r\n /// where the biped name defaults to <c>Bip01</c> (3ds Max \u22642009) / <c>Bip001</c>\r\n /// (2010+) per the Autodesk \"Naming the Biped\" documentation; some exporters mangle\r\n /// the spaces to underscores (<c>Bip01_L_Thigh</c>), hence the <c>^Bip\\d+[ _]</c>\r\n /// namespace pattern (alias comparison is separator-insensitive, so \"L UpperArm\" and\r\n /// \"L_UpperArm\" normalize identically). Sided bones use a bare mid-name <c>L/R</c>:\r\n /// <c>L Clavicle\u2192L UpperArm\u2192L Forearm\u2192L Hand</c> arms,\r\n /// <c>L Thigh\u2192L Calf\u2192L Foot\u2192L Toe0</c> legs. Fingers are numbered chains\r\n /// <c>L Finger0..4</c> (0 = thumb) with phalanx segments <c>Finger01/Finger02</c>\r\n /// etc. (MotionBuilder's \"3ds Max Biped Template\" characterization maps exactly these\r\n /// names). The COM root <c>Bip01</c> itself, <c>Footsteps</c>, toe segments\r\n /// <c>Toe01/Toe02</c> and <c>HorseLink</c> carry no aliases and are never mapped.\r\n /// </summary>\r\n public static Profile Biped { get; } = BuildBiped();\r\n\r\n /// <summary>\r\n /// DAZ/Poser classic naming (Poser 4 era figures, DAZ Generation-4 V4/M4, Genesis 1/2,\r\n /// MakeHuman's \"Poser/DAZ names\" BVH export \u2014 verified against the local\r\n /// <c>dev/corpus/unknown_rigs/makehuman_cmu_03_03_dazNames.bvh</c>): camel-case bones\r\n /// with a lower-case <c>l</c>/<c>r</c> side prefix \u2014 <c>hip</c> (the translating\r\n /// root), <c>abdomen[\u2192abdomen2]\u2192chest</c> spine, <c>neck</c>, <c>head</c>,\r\n /// <c>lCollar\u2192lShldr\u2192lForeArm\u2192lHand</c> arms, <c>lThigh\u2192lShin\u2192lFoot\u2192lToe</c> legs and\r\n /// <c>lThumb1..3/lIndex1..3/lMid1..3/lRing1..3/lPinky1..3</c> fingers. The\r\n /// <c>l/rButtock</c> thigh helpers and eye bones carry no aliases and stay unmapped.\r\n /// DAZ Genesis 3/8/9 renamed the skeleton (<c>abdomenLower</c>, <c>lShldrBend</c>, \u2026)\r\n /// and is NOT covered by this preset.\r\n /// </summary>\r\n public static Profile DazPoser { get; } = BuildDazPoser();\r\n\r\n /// <summary>\r\n /// Blender Rigify human rigs, per the metarig definition in the rigify add-on\r\n /// (<c>rigify/metarigs/human.py</c>) and the Blender manual's basic.human reference:\r\n /// the spine chain is <c>spine\u2192spine.001..spine.006</c> where <c>spine</c> IS the\r\n /// pelvis/hips bone (it sits at the pelvis and parents the thighs), spine.001\u2013003 are\r\n /// the torso, spine.004/005 the two neck bones (004 carries <see cref=\"BoneRole.Neck\"/>,\r\n /// 005 stays unmapped \u2014 same policy as ActorCore's NeckTwist02) and spine.006 is the\r\n /// head. Limbs: <c>shoulder.L\u2192upper_arm.L\u2192forearm.L\u2192hand.L</c>,\r\n /// <c>thigh.L\u2192shin.L\u2192foot.L\u2192toe.L</c>; fingers <c>thumb.01.L..03.L</c> and\r\n /// <c>f_index/f_middle/f_ring/f_pinky.01.L..03.L</c>. The <c>^DEF-</c> namespace\r\n /// pattern also matches rigify's generated deform skeleton (<c>DEF-spine.001</c>,\r\n /// <c>DEF-upper_arm.L</c>, \u2026); the segmented deform twins (<c>DEF-upper_arm.L.001</c>),\r\n /// <c>palm.*</c>, <c>pelvis.L/R</c>, <c>heel.02.L</c>, face bones and the generated\r\n /// ORG-/MCH-/control bones have no aliases and are never mapped.\r\n /// </summary>\r\n public static Profile Rigify { get; } = BuildRigify();\r\n\r\n /// <summary>\r\n /// VRoid Studio / VRM avatars (UniVRM exports): <c>J_Bip_<side>_<Part></c>\r\n /// bones where side is <c>C</c> (center), <c>L</c> or <c>R</c> \u2014 the standard VRoid\r\n /// skeleton behind the VRM humanoid spec (vrm-c/vrm-specification, humanoid bone map):\r\n /// <c>J_Bip_C_Hips/Spine/Chest/UpperChest/Neck/Head</c>,\r\n /// <c>J_Bip_L_Shoulder\u2192UpperArm\u2192LowerArm\u2192Hand</c>,\r\n /// <c>J_Bip_L_UpperLeg\u2192LowerLeg\u2192Foot\u2192ToeBase</c>, fingers\r\n /// <c>J_Bip_L_Thumb1..3/Index1..3/Middle1..3/Ring1..3/Little1..3</c> (\"Little\" is the\r\n /// pinky, per the VRM littleProximal/Intermediate/Distal humanoid bones). Secondary\r\n /// physics/adjust bones (<c>J_Sec_*</c>, <c>J_Adj_*</c>) and the <c>Root</c> bone have\r\n /// no aliases and are never mapped.\r\n /// </summary>\r\n public static Profile Vrm { get; } = BuildVrm();\r\n\r\n /// <summary>\r\n /// Blender Auto-Rig Pro humanoid FBX exports \u2014 bone names verified empirically against\r\n /// the local user repro <c>dev/corpus/todo/Defenses.fbx</c> (the PunchPerfect family):\r\n /// <c>.x</c> suffix marks center bones, <c>.l/.r</c> the sides, and the exported limb\r\n /// deform bones carry the <c>_stretch</c> twin name \u2014 <c>root.x</c> is the hips\r\n /// (under a ground bone <c>root</c>), <c>spine_01.x\u2192spine_02.x\u2192spine_03.x</c>,\r\n /// <c>neck.x</c>, <c>head.x</c>, arms <c>shoulder.l\u2192arm_stretch.l\u2192forearm_stretch.l\u2192\r\n /// hand.l</c> (plain \"arm\", NOT \"upperarm\"), legs <c>thigh_stretch.l\u2192leg_stretch.l\u2192\r\n /// foot.l\u2192toes_01.l</c> (\"leg\" is the calf). Fingers keep Auto-Rig Pro's <c>c_</c>\r\n /// control prefix on the exported deform chain: <c>c_thumb1.l..3.l</c>,\r\n /// <c>c_index/c_middle/c_ring/c_pinky1.l..3.l</c>. Leftover finger-tip markers\r\n /// (<c>mixamorig:LeftHandIndex4</c> in the repro) and <c>root</c> have no aliases.\r\n /// </summary>\r\n public static Profile AutoRigPro { get; } = BuildAutoRigPro();\r\n\r\n /// <summary>\r\n /// Xsens MVN exports (23-segment MVN body model; MVN Animate/Analyze FBX and BVH):\r\n /// anatomical vertebra names for the spine chain <c>Pelvis\u2192L5\u2192L3\u2192T12\u2192T8</c> (the four\r\n /// exported lumbar/thoracic segments of the MVN model), <c>Neck\u2192Head</c>, arms\r\n /// <c>RightShoulder\u2192RightUpperArm\u2192RightForeArm\u2192RightHand</c> and legs\r\n /// <c>RightUpperLeg\u2192RightLowerLeg\u2192RightFoot\u2192RightToe</c>. Body-suit capture only \u2014 no\r\n /// finger segments (Xsens gloves ship as separate data), so the hands are chain tips.\r\n /// </summary>\r\n public static Profile XsensMvn { get; } = BuildXsensMvn();\r\n\r\n /// <summary>\r\n /// Perception Neuron / Axis Neuron BVH exports: mixamo-like limb and finger names\r\n /// (<c>RightArm\u2192RightForeArm\u2192RightHand</c>, <c>RightUpLeg\u2192RightLeg\u2192RightFoot</c>,\r\n /// <c>RightHandThumb1..3</c>) but a FOUR-bone spine (<c>Spine\u2192Spine1..Spine3</c>), no\r\n /// toe joints (the feet are chain tips), and per-finger <c>RightInHandIndex</c>-style\r\n /// metacarpal helpers between the hand and the <c>RightHandIndex1..3</c> phalanges.\r\n /// The InHand metacarpals carry no aliases (barely animated palm helpers; mapping them\r\n /// as phalanges would shift every curl one joint outward \u2014 the SOMA finger bug class).\r\n /// The extra Spine3 is what lets this preset outscore mixamo on Neuron rigs (and\r\n /// mixamo's toes keep mixamo ahead on real Mixamo rigs).\r\n /// </summary>\r\n public static Profile PerceptionNeuron { get; } = BuildPerceptionNeuron();\r\n\r\n /// <summary>\r\n /// Source engine ValveBiped skeletons (HL2/GMod humanoids, playermodels):\r\n /// <c>ValveBiped.Bip01_*</c> names \u2014 3ds-Max-Biped-derived parts behind the fixed\r\n /// namespace, with underscores and a spine chain that SKIPS Spine3\r\n /// (<c>Spine\u2192Spine1\u2192Spine2\u2192Spine4</c>), <c>Neck1</c>/<c>Head1</c>, arms\r\n /// <c>L_Clavicle\u2192L_UpperArm\u2192L_Forearm\u2192L_Hand</c>, legs\r\n /// <c>L_Thigh\u2192L_Calf\u2192L_Foot\u2192L_Toe0</c> and numbered finger chains\r\n /// <c>L_Finger0/01/02</c> (0 = thumb) \u2026 <c>L_Finger4/41/42</c> (pinky). Evaluated\r\n /// before <see cref=\"Biped\"/> (same Bip01 ancestry; the plain-Biped preset must never\r\n /// claim a ValveBiped rig). Attachment/weapon helpers (<c>ValveBiped.forward</c>,\r\n /// <c>ValveBiped.Anim_Attachment_*</c>) have no aliases and are never mapped.\r\n /// </summary>\r\n public static Profile ValveBiped { get; } = BuildValveBiped();\r\n\r\n /// <summary>\r\n /// DAZ Genesis 3/8(.1) figures: renamed Genesis skeleton (NOT covered by\r\n /// <see cref=\"DazPoser\"/>) \u2014 <c>hip</c> is the translating root and the LCA of the\r\n /// <c>pelvis</c> leg branch and the <c>abdomenLower\u2192abdomenUpper\u2192chestLower\u2192chestUpper</c>\r\n /// spine (so <c>hip</c> carries <see cref=\"BoneRole.Hips\"/> and <c>pelvis</c> stays\r\n /// unmapped, same policy as ActorCore's Hip/Pelvis pair). Neck chain\r\n /// <c>neckLower\u2192neckUpper\u2192head</c> (neckUpper unmapped, NeckTwist02 policy). Limbs use\r\n /// Bend/Twist pairs: the Bend bones (<c>lShldrBend</c>, <c>lForearmBend</c>,\r\n /// <c>lThighBend</c>) are the primary limb bones; the co-linear Twist roll helpers\r\n /// (<c>lShldrTwist</c>, <c>lForearmTwist</c>, <c>lThighTwist</c>) carry no aliases and\r\n /// are never mapped. Legs <c>lThighBend\u2192lShin\u2192lFoot\u2192lToe</c> (<c>lMetatarsals</c> is an\r\n /// arch helper between foot and toe, unmapped). Fingers are the classic DAZ\r\n /// <c>lThumb1..3/lIndex1..3/lMid1..3/lRing1..3/lPinky1..3</c>. Genesis 9 renamed the\r\n /// skeleton again (<c>l_upperarm</c>, \u2026) and is NOT covered by this preset.\r\n /// </summary>\r\n public static Profile DazGenesis { get; } = BuildDazGenesis();\r\n\r\n /// <summary>\r\n /// The s&box citizen-family skeleton itself (<c>citizen.vmdl</c>,\r\n /// <c>citizen_human_*.vmdl</c> and every community model re-rigged on their skeleton):\r\n /// <c>pelvis</c>, <c>spine_0..2</c>, <c>neck_0</c>, <c>head</c>, reversed-word limb\r\n /// names <c>arm_upper_L\u2192arm_lower_L\u2192hand_L</c> / <c>leg_upper_L\u2192leg_lower_L\u2192\r\n /// ankle_L\u2192ball_L</c>, <c>clavicle_L/R</c> and fingers\r\n /// <c>finger_<name>_{meta,0,1,2}_L</c> (meta = metacarpal, 0/1/2 =\r\n /// proximal/middle/distal). The alias table mirrors\r\n /// <see cref=\"Target.SboxBoneClassifier\"/>'s curated role table \u2014 kept in sync by a\r\n /// test. This is what lets a COMPILED s&box model picked as a custom conversion\r\n /// target (or used as a source) be recognized: the reversed word order\r\n /// (<c>arm_upper</c>, not <c>upper_arm</c>) defeats generic token matching, which\r\n /// scored the citizen rig at 5% and made every custom-model pick fail with \"not\r\n /// recognized as humanoid\". Twist/helper/IK/face bones (<c>*_twist*</c>,\r\n /// <c>*_helper*</c>, <c>eye_*</c>, \u2026) have no aliases and are never mapped.\r\n /// </summary>\r\n public static Profile Sbox { get; } = BuildSbox();\r\n\r\n /// <summary>\r\n /// AdvancedSkeleton (Maya auto-rigger, ubiquitous in game rips and mobile-game rigs):\r\n /// <c>Root_M</c> hips, <c>Spine1_M(\u2192Spine2_M)\u2192Chest_M</c> spine, <c>Neck_M\u2192Head_M</c>\r\n /// (small rigs parent <c>Head_M</c> straight to the chest with no neck),\r\n /// <c>Scapula\u2192Shoulder\u2192Elbow\u2192Wrist</c> arms, <c>Hip\u2192Knee\u2192Ankle\u2192Toes</c> legs and\r\n /// <c><Name>Finger1..3</c> fingers, all sided <c>_L/_R</c> (center <c>_M</c>).\r\n /// Twist helpers (<c>ShoulderPart1</c>, <c>HipPart1</c>, \u2026), <c>Cup</c> palm bones and\r\n /// the face rig carry no aliases. Real case: a Sonic mobile-game rip whose name stage\r\n /// scored below threshold \u2014 the topology fallback then mapped ARMS AND LEGS ONTO THE\r\n /// HEAD QUILLS (long symmetric chains), playing every clip as garbage.\r\n /// </summary>\r\n public static Profile AdvancedSkeleton { get; } = BuildAdvancedSkeleton();\r\n\r\n /// <summary>All built-in presets, in detection order (first wins score ties \u2014 see\r\n /// <see cref=\"SmplX\"/> vs <see cref=\"Smpl\"/>; <see cref=\"ValveBiped\"/> is evaluated\r\n /// before <see cref=\"Biped\"/> and <see cref=\"DazGenesis\"/> before\r\n /// <see cref=\"DazPoser\"/> within their families).</summary>\r\n public static IReadOnlyList<Profile> All { get; } =\r\n new[]\r\n {\r\n Sbox, Mixamo, ActorCoreCc, UeMannequin, XsensMvn, PerceptionNeuron, RokokoBvh,\r\n SmplX, Smpl, SomaBvh, ClassicBvh, ValveBiped, Biped, DazGenesis, DazPoser,\r\n Rigify, Vrm, AutoRigPro, AdvancedSkeleton,\r\n };\r\n\r\n // ---------------------------------------------------------------- advanced skeleton\r\n\r\n private static Profile BuildAdvancedSkeleton()\r\n {\r\n var aliases = new Dictionary<BoneRole, string[]>\r\n {\r\n [BoneRole.Hips] = new[] { \"Root_M\" },\r\n [BoneRole.Spine0] = new[] { \"Spine1_M\" },\r\n [BoneRole.Spine1] = new[] { \"Spine2_M\" },\r\n [BoneRole.Spine2] = new[] { \"Chest_M\" },\r\n [BoneRole.Neck] = new[] { \"Neck_M\" },\r\n [BoneRole.Head] = new[] { \"Head_M\" },\r\n };\r\n foreach (var side in new[] { \"L\", \"R\" })\r\n {\r\n aliases[Role(\"Clavicle\", side)] = new[] { $\"Scapula_{side}\" };\r\n aliases[Role(\"UpperArm\", side)] = new[] { $\"Shoulder_{side}\" };\r\n aliases[Role(\"LowerArm\", side)] = new[] { $\"Elbow_{side}\" };\r\n aliases[Role(\"Hand\", side)] = new[] { $\"Wrist_{side}\" };\r\n aliases[Role(\"UpperLeg\", side)] = new[] { $\"Hip_{side}\" };\r\n aliases[Role(\"LowerLeg\", side)] = new[] { $\"Knee_{side}\" };\r\n aliases[Role(\"Foot\", side)] = new[] { $\"Ankle_{side}\" };\r\n aliases[Role(\"Toe\", side)] = new[] { $\"Toes_{side}\" };\r\n\r\n foreach (var finger in new[] { \"Thumb\", \"Index\", \"Middle\", \"Ring\", \"Pinky\" })\r\n {\r\n aliases[Role($\"{finger}Prox\", side)] = new[] { $\"{finger}Finger1_{side}\" };\r\n aliases[Role($\"{finger}Mid\", side)] = new[] { $\"{finger}Finger2_{side}\" };\r\n aliases[Role($\"{finger}Dist\", side)] = new[] { $\"{finger}Finger3_{side}\" };\r\n }\r\n }\r\n\r\n return new Profile(\"advanced_skeleton\", new string[0], aliases);\r\n }\r\n\r\n // ---------------------------------------------------------------- sbox\r\n\r\n private static Profile BuildSbox()\r\n {\r\n var aliases = new Dictionary<BoneRole, string[]>\r\n {\r\n [BoneRole.Hips] = new[] { \"pelvis\" },\r\n [BoneRole.Spine0] = new[] { \"spine_0\" },\r\n [BoneRole.Spine1] = new[] { \"spine_1\" },\r\n [BoneRole.Spine2] = new[] { \"spine_2\" },\r\n [BoneRole.Spine3] = new[] { \"spine_3\" },\r\n [BoneRole.Neck] = new[] { \"neck_0\" },\r\n [BoneRole.Head] = new[] { \"head\" },\r\n };\r\n foreach (var side in new[] { \"L\", \"R\" })\r\n {\r\n aliases[Role(\"Clavicle\", side)] = new[] { $\"clavicle_{side}\" };\r\n aliases[Role(\"UpperArm\", side)] = new[] { $\"arm_upper_{side}\" };\r\n aliases[Role(\"LowerArm\", side)] = new[] { $\"arm_lower_{side}\" };\r\n aliases[Role(\"Hand\", side)] = new[] { $\"hand_{side}\" };\r\n aliases[Role(\"UpperLeg\", side)] = new[] { $\"leg_upper_{side}\" };\r\n aliases[Role(\"LowerLeg\", side)] = new[] { $\"leg_lower_{side}\" };\r\n aliases[Role(\"Foot\", side)] = new[] { $\"ankle_{side}\" };\r\n aliases[Role(\"Toe\", side)] = new[] { $\"ball_{side}\" };\r\n\r\n foreach (var (finger, rolePrefix) in new[]\r\n {\r\n (\"thumb\", \"Thumb\"), (\"index\", \"Index\"), (\"middle\", \"Middle\"),\r\n (\"ring\", \"Ring\"), (\"pinky\", \"Pinky\"),\r\n })\r\n {\r\n aliases[Role($\"{rolePrefix}Meta\", side)] = new[] { $\"finger_{finger}_meta_{side}\" };\r\n aliases[Role($\"{rolePrefix}Prox\", side)] = new[] { $\"finger_{finger}_0_{side}\" };\r\n aliases[Role($\"{rolePrefix}Mid\", side)] = new[] { $\"finger_{finger}_1_{side}\" };\r\n aliases[Role($\"{rolePrefix}Dist\", side)] = new[] { $\"finger_{finger}_2_{side}\" };\r\n }\r\n }\r\n\r\n return new Profile(\"sbox\", new string[0], aliases);\r\n }\r\n\r\n // ---------------------------------------------------------------- mixamo\r\n\r\n private static Profile BuildMixamo()\r\n {\r\n var aliases = new Dictionary<BoneRole, string[]>\r\n {\r\n [BoneRole.Hips] = new[] { \"Hips\" },\r\n [BoneRole.Spine0] = new[] { \"Spine\" },\r\n [BoneRole.Spine1] = new[] { \"Spine1\" },\r\n [BoneRole.Spine2] = new[] { \"Spine2\" },\r\n [BoneRole.Neck] = new[] { \"Neck\" },\r\n [BoneRole.Head] = new[] { \"Head\" },\r\n };\r\n foreach (var (roleSide, nameSide) in Sides())\r\n {\r\n aliases[Role(\"Clavicle\", roleSide)] = new[] { $\"{nameSide}Shoulder\" };\r\n aliases[Role(\"UpperArm\", roleSide)] = new[] { $\"{nameSide}Arm\" };\r\n aliases[Role(\"LowerArm\", roleSide)] = new[] { $\"{nameSide}ForeArm\" };\r\n aliases[Role(\"Hand\", roleSide)] = new[] { $\"{nameSide}Hand\" };\r\n aliases[Role(\"UpperLeg\", roleSide)] = new[] { $\"{nameSide}UpLeg\" };\r\n aliases[Role(\"LowerLeg\", roleSide)] = new[] { $\"{nameSide}Leg\" };\r\n aliases[Role(\"Foot\", roleSide)] = new[] { $\"{nameSide}Foot\" };\r\n aliases[Role(\"Toe\", roleSide)] = new[] { $\"{nameSide}ToeBase\" };\r\n\r\n foreach (var finger in new[] { \"Thumb\", \"Index\", \"Middle\", \"Ring\", \"Pinky\" })\r\n {\r\n aliases[Role($\"{finger}Prox\", roleSide)] = new[] { $\"{nameSide}Hand{finger}1\" };\r\n aliases[Role($\"{finger}Mid\", roleSide)] = new[] { $\"{nameSide}Hand{finger}2\" };\r\n aliases[Role($\"{finger}Dist\", roleSide)] = new[] { $\"{nameSide}Hand{finger}3\" };\r\n }\r\n }\r\n // Both ':' (FBX namespace) and '_' (namespace mangled by some exporters) forms occur\r\n // in the wild; some Mixamo downloads ship with no namespace at all, which still\r\n // matches because the aliases are the bare names.\r\n return new Profile(\"mixamo\", new[] { \"^mixamorig[0-9]*:\", \"^mixamorig[0-9]*_\" }, aliases);\r\n }\r\n\r\n // ---------------------------------------------------------------- actorcore / cc\r\n\r\n private static Profile BuildActorCoreCc()\r\n {\r\n var aliases = new Dictionary<BoneRole, string[]>\r\n {\r\n [BoneRole.Hips] = new[] { \"Hip\" },\r\n [BoneRole.Spine0] = new[] { \"Waist\" },\r\n [BoneRole.Spine1] = new[] { \"Spine01\" },\r\n [BoneRole.Spine2] = new[] { \"Spine02\" },\r\n [BoneRole.Neck] = new[] { \"NeckTwist01\" },\r\n [BoneRole.Head] = new[] { \"Head\" },\r\n };\r\n foreach (var roleSide in new[] { \"L\", \"R\" })\r\n {\r\n var nameSide = roleSide; // CC bones use the bare side letter: CC_Base_L_Thigh.\r\n aliases[Role(\"Clavicle\", roleSide)] = new[] { $\"{nameSide}_Clavicle\" };\r\n aliases[Role(\"UpperArm\", roleSide)] = new[] { $\"{nameSide}_Upperarm\" };\r\n aliases[Role(\"LowerArm\", roleSide)] = new[] { $\"{nameSide}_Forearm\" };\r\n aliases[Role(\"Hand\", roleSide)] = new[] { $\"{nameSide}_Hand\" };\r\n aliases[Role(\"UpperLeg\", roleSide)] = new[] { $\"{nameSide}_Thigh\" };\r\n aliases[Role(\"LowerLeg\", roleSide)] = new[] { $\"{nameSide}_Calf\" };\r\n aliases[Role(\"Foot\", roleSide)] = new[] { $\"{nameSide}_Foot\" };\r\n aliases[Role(\"Toe\", roleSide)] = new[] { $\"{nameSide}_ToeBase\" };\r\n\r\n foreach (var (role, cc) in new[]\r\n {\r\n (\"Thumb\", \"Thumb\"), (\"Index\", \"Index\"), (\"Middle\", \"Mid\"), (\"Ring\", \"Ring\"), (\"Pinky\", \"Pinky\"),\r\n })\r\n {\r\n aliases[Role($\"{role}Prox\", roleSide)] = new[] { $\"{nameSide}_{cc}1\" };\r\n aliases[Role($\"{role}Mid\", roleSide)] = new[] { $\"{nameSide}_{cc}2\" };\r\n aliases[Role($\"{role}Dist\", roleSide)] = new[] { $\"{nameSide}_{cc}3\" };\r\n }\r\n }\r\n return new Profile(\"actorcore_cc\", new[] { \"^CC_Base_\" }, aliases);\r\n }\r\n\r\n // ---------------------------------------------------------------- ue mannequin\r\n\r\n private static Profile BuildUeMannequin()\r\n {\r\n var aliases = new Dictionary<BoneRole, string[]>\r\n {\r\n [BoneRole.Hips] = new[] { \"pelvis\" },\r\n [BoneRole.Spine0] = new[] { \"spine_01\" },\r\n [BoneRole.Spine1] = new[] { \"spine_02\" },\r\n [BoneRole.Spine2] = new[] { \"spine_03\" },\r\n [BoneRole.Spine3] = new[] { \"spine_04\" },\r\n [BoneRole.Spine4] = new[] { \"spine_05\" },\r\n [BoneRole.Neck] = new[] { \"neck_01\" },\r\n [BoneRole.Head] = new[] { \"head\" },\r\n };\r\n foreach (var (roleSide, s) in new[] { (\"L\", \"l\"), (\"R\", \"r\") })\r\n {\r\n aliases[Role(\"Clavicle\", roleSide)] = new[] { $\"clavicle_{s}\" };\r\n aliases[Role(\"UpperArm\", roleSide)] = new[] { $\"upperarm_{s}\" };\r\n aliases[Role(\"LowerArm\", roleSide)] = new[] { $\"lowerarm_{s}\" };\r\n aliases[Role(\"Hand\", roleSide)] = new[] { $\"hand_{s}\" };\r\n aliases[Role(\"UpperLeg\", roleSide)] = new[] { $\"thigh_{s}\" };\r\n aliases[Role(\"LowerLeg\", roleSide)] = new[] { $\"calf_{s}\" };\r\n aliases[Role(\"Foot\", roleSide)] = new[] { $\"foot_{s}\" };\r\n aliases[Role(\"Toe\", roleSide)] = new[] { $\"ball_{s}\" };\r\n\r\n foreach (var (role, ue) in new[]\r\n {\r\n (\"Thumb\", \"thumb\"), (\"Index\", \"index\"), (\"Middle\", \"middle\"), (\"Ring\", \"ring\"), (\"Pinky\", \"pinky\"),\r\n })\r\n {\r\n // UE5 mannequin adds metacarpals for the four fingers (not the thumb).\r\n if (role != \"Thumb\")\r\n aliases[Role($\"{role}Meta\", roleSide)] = new[] { $\"{ue}_metacarpal_{s}\" };\r\n aliases[Role($\"{role}Prox\", roleSide)] = new[] { $\"{ue}_01_{s}\" };\r\n aliases[Role($\"{role}Mid\", roleSide)] = new[] { $\"{ue}_02_{s}\" };\r\n aliases[Role($\"{role}Dist\", roleSide)] = new[] { $\"{ue}_03_{s}\" };\r\n }\r\n }\r\n return new Profile(\"ue_mannequin\", new string[0], aliases);\r\n }\r\n\r\n // ---------------------------------------------------------------- rokoko / xsens bvh\r\n\r\n private static Profile BuildRokokoBvh()\r\n {\r\n var aliases = new Dictionary<BoneRole, string[]>\r\n {\r\n [BoneRole.Hips] = new[] { \"Hips\" },\r\n // Spine naming varies (Spine, Spine1..Spine4); ordered alias preference plus the\r\n // used-bone exclusion in the detector shifts the chain up when \"Spine\" is absent.\r\n [BoneRole.Spine0] = new[] { \"Spine\", \"Spine1\" },\r\n [BoneRole.Spine1] = new[] { \"Spine1\", \"Spine2\" },\r\n [BoneRole.Spine2] = new[] { \"Spine2\", \"Spine3\" },\r\n [BoneRole.Spine3] = new[] { \"Spine3\", \"Spine4\" },\r\n [BoneRole.Spine4] = new[] { \"Spine4\" },\r\n [BoneRole.Neck] = new[] { \"Neck\", \"Neck1\" },\r\n [BoneRole.Head] = new[] { \"Head\" },\r\n };\r\n foreach (var (roleSide, nameSide) in Sides())\r\n {\r\n aliases[Role(\"Clavicle\", roleSide)] = new[] { $\"{nameSide}Shoulder\", $\"{nameSide}Collar\" };\r\n aliases[Role(\"UpperArm\", roleSide)] = new[] { $\"{nameSide}Arm\", $\"{nameSide}UpperArm\" };\r\n aliases[Role(\"LowerArm\", roleSide)] = new[] { $\"{nameSide}ForeArm\", $\"{nameSide}LowerArm\" };\r\n aliases[Role(\"Hand\", roleSide)] = new[] { $\"{nameSide}Hand\" };\r\n aliases[Role(\"UpperLeg\", roleSide)] = new[] { $\"{nameSide}UpLeg\", $\"{nameSide}Thigh\", $\"{nameSide}UpperLeg\" };\r\n aliases[Role(\"LowerLeg\", roleSide)] = new[] { $\"{nameSide}Leg\", $\"{nameSide}Shin\", $\"{nameSide}LowerLeg\" };\r\n aliases[Role(\"Foot\", roleSide)] = new[] { $\"{nameSide}Foot\" };\r\n aliases[Role(\"Toe\", roleSide)] = new[] { $\"{nameSide}Toe\", $\"{nameSide}ToeBase\" };\r\n }\r\n return new Profile(\"rokoko_bvh\", new string[0], aliases);\r\n }\r\n\r\n // ---------------------------------------------------------------- xsens mvn\r\n\r\n private static Profile BuildXsensMvn()\r\n {\r\n var aliases = new Dictionary<BoneRole, string[]>\r\n {\r\n [BoneRole.Hips] = new[] { \"Pelvis\" },\r\n // MVN's exported spine segments are the anatomical vertebra levels L5/L3/T12/T8.\r\n [BoneRole.Spine0] = new[] { \"L5\" },\r\n [BoneRole.Spine1] = new[] { \"L3\" },\r\n [BoneRole.Spine2] = new[] { \"T12\" },\r\n [BoneRole.Spine3] = new[] { \"T8\" },\r\n [BoneRole.Neck] = new[] { \"Neck\" },\r\n [BoneRole.Head] = new[] { \"Head\" },\r\n };\r\n foreach (var (roleSide, nameSide) in Sides())\r\n {\r\n aliases[Role(\"Clavicle\", roleSide)] = new[] { $\"{nameSide}Shoulder\" };\r\n aliases[Role(\"UpperArm\", roleSide)] = new[] { $\"{nameSide}UpperArm\" };\r\n aliases[Role(\"LowerArm\", roleSide)] = new[] { $\"{nameSide}ForeArm\" };\r\n aliases[Role(\"Hand\", roleSide)] = new[] { $\"{nameSide}Hand\" };\r\n aliases[Role(\"UpperLeg\", roleSide)] = new[] { $\"{nameSide}UpperLeg\" };\r\n aliases[Role(\"LowerLeg\", roleSide)] = new[] { $\"{nameSide}LowerLeg\" };\r\n aliases[Role(\"Foot\", roleSide)] = new[] { $\"{nameSide}Foot\" };\r\n aliases[Role(\"Toe\", roleSide)] = new[] { $\"{nameSide}Toe\" };\r\n }\r\n // Body-suit capture: no finger segments (see the property remarks).\r\n return new Profile(\"xsens_mvn\", new string[0], aliases);\r\n }\r\n\r\n // ---------------------------------------------------------------- perception neuron\r\n\r\n private static Profile BuildPerceptionNeuron()\r\n {\r\n var aliases = new Dictionary<BoneRole, string[]>\r\n {\r\n [BoneRole.Hips] = new[] { \"Hips\" },\r\n [BoneRole.Spine0] = new[] { \"Spine\" },\r\n [BoneRole.Spine1] = new[] { \"Spine1\" },\r\n [BoneRole.Spine2] = new[] { \"Spine2\" },\r\n [BoneRole.Spine3] = new[] { \"Spine3\" },\r\n [BoneRole.Neck] = new[] { \"Neck\" },\r\n [BoneRole.Head] = new[] { \"Head\" },\r\n };\r\n foreach (var (roleSide, nameSide) in Sides())\r\n {\r\n aliases[Role(\"Clavicle\", roleSide)] = new[] { $\"{nameSide}Shoulder\" };\r\n aliases[Role(\"UpperArm\", roleSide)] = new[] { $\"{nameSide}Arm\" };\r\n aliases[Role(\"LowerArm\", roleSide)] = new[] { $\"{nameSide}ForeArm\" };\r\n aliases[Role(\"Hand\", roleSide)] = new[] { $\"{nameSide}Hand\" };\r\n aliases[Role(\"UpperLeg\", roleSide)] = new[] { $\"{nameSide}UpLeg\" };\r\n aliases[Role(\"LowerLeg\", roleSide)] = new[] { $\"{nameSide}Leg\" };\r\n aliases[Role(\"Foot\", roleSide)] = new[] { $\"{nameSide}Foot\" };\r\n // No toe joints in Axis Neuron exports; the feet are chain tips.\r\n\r\n // Phalanges only: the LeftInHandIndex-style metacarpal helpers between hand\r\n // and phalanges carry no role (see the property remarks).\r\n foreach (var finger in new[] { \"Thumb\", \"Index\", \"Middle\", \"Ring\", \"Pinky\" })\r\n {\r\n aliases[Role($\"{finger}Prox\", roleSide)] = new[] { $\"{nameSide}Hand{finger}1\" };\r\n aliases[Role($\"{finger}Mid\", roleSide)] = new[] { $\"{nameSide}Hand{finger}2\" };\r\n aliases[Role($\"{finger}Dist\", roleSide)] = new[] { $\"{nameSide}Hand{finger}3\" };\r\n }\r\n }\r\n return new Profile(\"perception_neuron\", new string[0], aliases);\r\n }\r\n\r\n // ---------------------------------------------------------------- valvebiped\r\n\r\n private static Profile BuildValveBiped()\r\n {\r\n var aliases = new Dictionary<BoneRole, string[]>\r\n {\r\n [BoneRole.Hips] = new[] { \"Pelvis\" },\r\n [BoneRole.Spine0] = new[] { \"Spine\" },\r\n [BoneRole.Spine1] = new[] { \"Spine1\" },\r\n [BoneRole.Spine2] = new[] { \"Spine2\" },\r\n // The stock HL2 chain skips Spine3 (Spine2's child IS Spine4, the chest);\r\n // ordered preference + used-bone exclusion also absorbs a variant that has\r\n // both: Spine3\u2192Spine3 and Spine4\u2192Spine4.\r\n [BoneRole.Spine3] = new[] { \"Spine3\", \"Spine4\" },\r\n [BoneRole.Spine4] = new[] { \"Spine4\" },\r\n [BoneRole.Neck] = new[] { \"Neck1\" },\r\n [BoneRole.Head] = new[] { \"Head1\" },\r\n };\r\n foreach (var s in new[] { \"L\", \"R\" })\r\n {\r\n aliases[Role(\"Clavicle\", s)] = new[] { $\"{s}_Clavicle\" };\r\n aliases[Role(\"UpperArm\", s)] = new[] { $\"{s}_UpperArm\" };\r\n aliases[Role(\"LowerArm\", s)] = new[] { $\"{s}_Forearm\" };\r\n aliases[Role(\"Hand\", s)] = new[] { $\"{s}_Hand\" };\r\n aliases[Role(\"UpperLeg\", s)] = new[] { $\"{s}_Thigh\" };\r\n aliases[Role(\"LowerLeg\", s)] = new[] { $\"{s}_Calf\" };\r\n aliases[Role(\"Foot\", s)] = new[] { $\"{s}_Foot\" };\r\n aliases[Role(\"Toe\", s)] = new[] { $\"{s}_Toe0\" };\r\n\r\n // Biped-style numbered finger chains behind the ValveBiped namespace:\r\n // Finger0 is the thumb; segments append the phalanx digit (Finger0 \u2192\r\n // Finger01 \u2192 Finger02, Finger1 \u2192 Finger11 \u2192 \u2026).\r\n foreach (var (finger, n) in new[]\r\n {\r\n (\"Thumb\", 0), (\"Index\", 1), (\"Middle\", 2), (\"Ring\", 3), (\"Pinky\", 4),\r\n })\r\n {\r\n aliases[Role($\"{finger}Prox\", s)] = new[] { $\"{s}_Finger{n}\" };\r\n aliases[Role($\"{finger}Mid\", s)] = new[] { $\"{s}_Finger{n}1\" };\r\n aliases[Role($\"{finger}Dist\", s)] = new[] { $\"{s}_Finger{n}2\" };\r\n }\r\n }\r\n // The fixed \"ValveBiped.Bip01_\" namespace: anchored, so plain \"Bip01 ...\" Character\r\n // Studio rigs never strip it (and the Biped preset's \"^Bip\\d+[ _]\" never matches\r\n // the ValveBiped prefix \u2014 the two families cannot cross-claim).\r\n return new Profile(\"valvebiped\", new[] { @\"^ValveBiped\\.Bip01_\" }, aliases);\r\n }\r\n\r\n // ---------------------------------------------------------------- daz genesis 3/8\r\n\r\n private static Profile BuildDazGenesis()\r\n {\r\n var aliases = new Dictionary<BoneRole, string[]>\r\n {\r\n // \"hip\" is the translating root and LCA of the pelvis (leg branch) and the\r\n // abdomen (spine branch); \"pelvis\" is a leg-branch intermediate and stays\r\n // unmapped \u2014 same policy as ActorCore's CC_Base_Hip/CC_Base_Pelvis pair.\r\n [BoneRole.Hips] = new[] { \"hip\" },\r\n [BoneRole.Spine0] = new[] { \"abdomenLower\" },\r\n [BoneRole.Spine1] = new[] { \"abdomenUpper\" },\r\n [BoneRole.Spine2] = new[] { \"chestLower\" },\r\n [BoneRole.Spine3] = new[] { \"chestUpper\" },\r\n // neckLower\u2192neckUpper\u2192head: neckLower IS the neck; neckUpper stays unmapped\r\n // (same policy as ActorCore's NeckTwist02 / rigify's spine.005).\r\n [BoneRole.Neck] = new[] { \"neckLower\" },\r\n [BoneRole.Head] = new[] { \"head\" },\r\n };\r\n foreach (var s in new[] { \"L\", \"R\" })\r\n {\r\n var p = s == \"L\" ? \"l\" : \"r\"; // lower-case side prefix: lShldrBend, rThighBend\r\n aliases[Role(\"Clavicle\", s)] = new[] { $\"{p}Collar\" };\r\n // Bend bones are the primary limb bones; the co-linear *Twist roll helpers\r\n // have no aliases and are never mapped.\r\n aliases[Role(\"UpperArm\", s)] = new[] { $\"{p}ShldrBend\" };\r\n aliases[Role(\"LowerArm\", s)] = new[] { $\"{p}ForearmBend\" };\r\n aliases[Role(\"Hand\", s)] = new[] { $\"{p}Hand\" };\r\n aliases[Role(\"UpperLeg\", s)] = new[] { $\"{p}ThighBend\" };\r\n aliases[Role(\"LowerLeg\", s)] = new[] { $\"{p}Shin\" };\r\n aliases[Role(\"Foot\", s)] = new[] { $\"{p}Foot\" };\r\n // lMetatarsals sits between foot and toe (arch helper, unmapped).\r\n aliases[Role(\"Toe\", s)] = new[] { $\"{p}Toe\" };\r\n\r\n foreach (var (role, daz) in new[]\r\n {\r\n (\"Thumb\", \"Thumb\"), (\"Index\", \"Index\"), (\"Middle\", \"Mid\"), (\"Ring\", \"Ring\"), (\"Pinky\", \"Pinky\"),\r\n })\r\n {\r\n aliases[Role($\"{role}Prox\", s)] = new[] { $\"{p}{daz}1\" };\r\n aliases[Role($\"{role}Mid\", s)] = new[] { $\"{p}{daz}2\" };\r\n aliases[Role($\"{role}Dist\", s)] = new[] { $\"{p}{daz}3\" };\r\n }\r\n }\r\n return new Profile(\"daz_genesis\", new string[0], aliases);\r\n }\r\n\r\n // ---------------------------------------------------------------- smpl / smpl-x\r\n\r\n private static Profile BuildSmpl(bool withFingers)\r\n {\r\n var aliases = new Dictionary<BoneRole, string[]>\r\n {\r\n [BoneRole.Hips] = new[] { \"Pelvis\" },\r\n [BoneRole.Spine0] = new[] { \"Spine1\" },\r\n [BoneRole.Spine1] = new[] { \"Spine2\" },\r\n [BoneRole.Spine2] = new[] { \"Spine3\" },\r\n [BoneRole.Neck] = new[] { \"Neck\" },\r\n [BoneRole.Head] = new[] { \"Head\" },\r\n };\r\n foreach (var (roleSide, abbr, word) in new[] { (\"L\", \"L\", \"left\"), (\"R\", \"R\", \"right\") })\r\n {\r\n // Both documented spellings per role: abbreviated FBX-rig names (\"L_Hip\") and\r\n // spelled model joint names (\"left_hip\"). Comparison is separator-insensitive.\r\n aliases[Role(\"Clavicle\", roleSide)] = new[] { $\"{abbr}_Collar\", $\"{word}_collar\" };\r\n aliases[Role(\"UpperArm\", roleSide)] = new[] { $\"{abbr}_Shoulder\", $\"{word}_shoulder\" };\r\n aliases[Role(\"LowerArm\", roleSide)] = new[] { $\"{abbr}_Elbow\", $\"{word}_elbow\" };\r\n aliases[Role(\"Hand\", roleSide)] = new[] { $\"{abbr}_Wrist\", $\"{word}_wrist\" };\r\n aliases[Role(\"UpperLeg\", roleSide)] = new[] { $\"{abbr}_Hip\", $\"{word}_hip\" };\r\n aliases[Role(\"LowerLeg\", roleSide)] = new[] { $\"{abbr}_Knee\", $\"{word}_knee\" };\r\n aliases[Role(\"Foot\", roleSide)] = new[] { $\"{abbr}_Ankle\", $\"{word}_ankle\" };\r\n aliases[Role(\"Toe\", roleSide)] = new[] { $\"{abbr}_Foot\", $\"{word}_foot\" };\r\n\r\n if (!withFingers)\r\n continue;\r\n\r\n // SMPL-X finger joints (left_index1..3 etc., per vchoutas/smplx joint_names.py).\r\n foreach (var finger in new[] { \"thumb\", \"index\", \"middle\", \"ring\", \"pinky\" })\r\n {\r\n var name = char.ToUpperInvariant(finger[0]) + finger[1..];\r\n aliases[Role($\"{name}Prox\", roleSide)] = new[] { $\"{word}_{finger}1\" };\r\n aliases[Role($\"{name}Mid\", roleSide)] = new[] { $\"{word}_{finger}2\" };\r\n aliases[Role($\"{name}Dist\", roleSide)] = new[] { $\"{word}_{finger}3\" };\r\n }\r\n }\r\n // Gendered SMPL FBX rigs prefix every bone (m_avg_L_Hip, f_avg_Pelvis).\r\n return new Profile(withFingers ? \"smpl_x\" : \"smpl\", new[] { \"^m_avg_\", \"^f_avg_\" }, aliases);\r\n }\r\n\r\n // ---------------------------------------------------------------- nvidia soma bvh\r\n\r\n private static Profile BuildSomaBvh()\r\n {\r\n var aliases = new Dictionary<BoneRole, string[]>\r\n {\r\n [BoneRole.Hips] = new[] { \"Hips\" },\r\n [BoneRole.Spine0] = new[] { \"Spine1\" },\r\n [BoneRole.Spine1] = new[] { \"Spine2\" },\r\n [BoneRole.Spine2] = new[] { \"Chest\" },\r\n [BoneRole.Neck] = new[] { \"Neck1\" },\r\n [BoneRole.Head] = new[] { \"Head\" },\r\n };\r\n foreach (var (roleSide, nameSide) in Sides())\r\n {\r\n aliases[Role(\"Clavicle\", roleSide)] = new[] { $\"{nameSide}Shoulder\" };\r\n aliases[Role(\"UpperArm\", roleSide)] = new[] { $\"{nameSide}Arm\" };\r\n aliases[Role(\"LowerArm\", roleSide)] = new[] { $\"{nameSide}ForeArm\" };\r\n aliases[Role(\"Hand\", roleSide)] = new[] { $\"{nameSide}Hand\" };\r\n // SOMA's \"Leg\" is the thigh, \"Shin\" the calf \u2014 the decisive difference from\r\n // mixamo, where \"Leg\" is the calf under \"UpLeg\".\r\n aliases[Role(\"UpperLeg\", roleSide)] = new[] { $\"{nameSide}Leg\" };\r\n aliases[Role(\"LowerLeg\", roleSide)] = new[] { $\"{nameSide}Shin\" };\r\n aliases[Role(\"Foot\", roleSide)] = new[] { $\"{nameSide}Foot\" };\r\n aliases[Role(\"Toe\", roleSide)] = new[] { $\"{nameSide}ToeBase\" };\r\n\r\n // Mixamo-style finger NAMES but not mixamo segmentation: SOMA fingers have four\r\n // segments where segment 1 is a metacarpal (measured on the repro BVH: Index1\r\n // sits 3.2 cm from the wrist at the palm base, then a 6.4 cm metacarpal to the\r\n // Index2 knuckle, then 3.7/2.3 cm phalanges to Index3/Index4) \u2014 so 2/3/4 are the\r\n // phalanges. Mapping 1..3 as Prox/Mid/Dist (mixamo's segmentation) shifted every\r\n // curl one joint outward and dropped the distal curl entirely (frozen fingers).\r\n // The thumb is three segments plus *End, mapped 1..3 like mixamo's; *End tip\r\n // markers carry no role.\r\n aliases[Role(\"ThumbProx\", roleSide)] = new[] { $\"{nameSide}HandThumb1\" };\r\n aliases[Role(\"ThumbMid\", roleSide)] = new[] { $\"{nameSide}HandThumb2\" };\r\n aliases[Role(\"ThumbDist\", roleSide)] = new[] { $\"{nameSide}HandThumb3\" };\r\n foreach (var finger in new[] { \"Index\", \"Middle\", \"Ring\", \"Pinky\" })\r\n {\r\n aliases[Role($\"{finger}Meta\", roleSide)] = new[] { $\"{nameSide}Hand{finger}1\" };\r\n aliases[Role($\"{finger}Prox\", roleSide)] = new[] { $\"{nameSide}Hand{finger}2\" };\r\n aliases[Role($\"{finger}Mid\", roleSide)] = new[] { $\"{nameSide}Hand{finger}3\" };\r\n aliases[Role($\"{finger}Dist\", roleSide)] = new[] { $\"{nameSide}Hand{finger}4\" };\r\n }\r\n }\r\n return new Profile(\"soma_bvh\", new string[0], aliases);\r\n }\r\n\r\n // ---------------------------------------------------------------- classic bvh\r\n\r\n private static Profile BuildClassicBvh()\r\n {\r\n var aliases = new Dictionary<BoneRole, string[]>\r\n {\r\n [BoneRole.Hips] = new[] { \"Hips\" },\r\n [BoneRole.Spine0] = new[] { \"Chest\" },\r\n [BoneRole.Spine1] = new[] { \"Chest2\" },\r\n [BoneRole.Spine2] = new[] { \"Chest3\" },\r\n [BoneRole.Spine3] = new[] { \"Chest4\" },\r\n [BoneRole.Neck] = new[] { \"Neck\" },\r\n [BoneRole.Head] = new[] { \"Head\" },\r\n };\r\n foreach (var (roleSide, nameSide) in Sides())\r\n {\r\n aliases[Role(\"Clavicle\", roleSide)] = new[] { $\"{nameSide}Collar\" };\r\n aliases[Role(\"UpperArm\", roleSide)] = new[] { $\"{nameSide}Shoulder\" };\r\n aliases[Role(\"LowerArm\", roleSide)] = new[] { $\"{nameSide}Elbow\" };\r\n aliases[Role(\"Hand\", roleSide)] = new[] { $\"{nameSide}Wrist\" };\r\n aliases[Role(\"UpperLeg\", roleSide)] = new[] { $\"{nameSide}Hip\" };\r\n aliases[Role(\"LowerLeg\", roleSide)] = new[] { $\"{nameSide}Knee\" };\r\n aliases[Role(\"Foot\", roleSide)] = new[] { $\"{nameSide}Ankle\" };\r\n aliases[Role(\"Toe\", roleSide)] = new[] { $\"{nameSide}Toe\" };\r\n }\r\n return new Profile(\"classic_bvh\", new string[0], aliases);\r\n }\r\n\r\n // ---------------------------------------------------------------- 3ds max biped\r\n\r\n private static Profile BuildBiped()\r\n {\r\n var aliases = new Dictionary<BoneRole, string[]>\r\n {\r\n [BoneRole.Hips] = new[] { \"Pelvis\" },\r\n [BoneRole.Spine0] = new[] { \"Spine\" },\r\n [BoneRole.Spine1] = new[] { \"Spine1\" },\r\n [BoneRole.Spine2] = new[] { \"Spine2\" },\r\n [BoneRole.Spine3] = new[] { \"Spine3\" },\r\n [BoneRole.Neck] = new[] { \"Neck\" },\r\n [BoneRole.Head] = new[] { \"Head\" },\r\n };\r\n foreach (var s in new[] { \"L\", \"R\" })\r\n {\r\n aliases[Role(\"Clavicle\", s)] = new[] { $\"{s} Clavicle\" };\r\n aliases[Role(\"UpperArm\", s)] = new[] { $\"{s} UpperArm\" };\r\n aliases[Role(\"LowerArm\", s)] = new[] { $\"{s} Forearm\" };\r\n aliases[Role(\"Hand\", s)] = new[] { $\"{s} Hand\" };\r\n aliases[Role(\"UpperLeg\", s)] = new[] { $\"{s} Thigh\" };\r\n aliases[Role(\"LowerLeg\", s)] = new[] { $\"{s} Calf\" };\r\n aliases[Role(\"Foot\", s)] = new[] { $\"{s} Foot\" };\r\n aliases[Role(\"Toe\", s)] = new[] { $\"{s} Toe0\" };\r\n\r\n // Numbered finger chains: Finger0 is the thumb; segment names append the\r\n // phalanx digit (Finger0 \u2192 Finger01 \u2192 Finger02, Finger1 \u2192 Finger11 \u2192 ...).\r\n foreach (var (finger, n) in new[]\r\n {\r\n (\"Thumb\", 0), (\"Index\", 1), (\"Middle\", 2), (\"Ring\", 3), (\"Pinky\", 4),\r\n })\r\n {\r\n aliases[Role($\"{finger}Prox\", s)] = new[] { $\"{s} Finger{n}\" };\r\n aliases[Role($\"{finger}Mid\", s)] = new[] { $\"{s} Finger{n}1\" };\r\n aliases[Role($\"{finger}Dist\", s)] = new[] { $\"{s} Finger{n}2\" };\r\n }\r\n }\r\n // \"Bip01 \"/\"Bip001 \" biped-name prefix; underscore form covers exporters that\r\n // mangle the spaces (\"Bip01_L_Thigh\"). The bare COM root \"Bip01\" is untouched by\r\n // the pattern (no trailing separator) and has no alias.\r\n return new Profile(\"biped\", new[] { @\"^Bip\\d+[ _]\" }, aliases);\r\n }\r\n\r\n // ---------------------------------------------------------------- daz / poser\r\n\r\n private static Profile BuildDazPoser()\r\n {\r\n var aliases = new Dictionary<BoneRole, string[]>\r\n {\r\n [BoneRole.Hips] = new[] { \"hip\" },\r\n [BoneRole.Spine0] = new[] { \"abdomen\" },\r\n // Poser classic / DAZ Gen4 spine is abdomen\u2192chest; DAZ Genesis 1/2 inserts\r\n // abdomen2. Ordered preference + used-bone exclusion handles both: without\r\n // abdomen2 the chest falls back to Spine1 and Spine2 stays unmapped.\r\n [BoneRole.Spine1] = new[] { \"abdomen2\", \"chest\" },\r\n [BoneRole.Spine2] = new[] { \"chest\" },\r\n [BoneRole.Neck] = new[] { \"neck\" },\r\n [BoneRole.Head] = new[] { \"head\" },\r\n };\r\n foreach (var s in new[] { \"L\", \"R\" })\r\n {\r\n var p = s == \"L\" ? \"l\" : \"r\"; // lower-case side prefix: lShldr, rThigh\r\n aliases[Role(\"Clavicle\", s)] = new[] { $\"{p}Collar\" };\r\n aliases[Role(\"UpperArm\", s)] = new[] { $\"{p}Shldr\" };\r\n aliases[Role(\"LowerArm\", s)] = new[] { $\"{p}ForeArm\" };\r\n aliases[Role(\"Hand\", s)] = new[] { $\"{p}Hand\" };\r\n aliases[Role(\"UpperLeg\", s)] = new[] { $\"{p}Thigh\" };\r\n aliases[Role(\"LowerLeg\", s)] = new[] { $\"{p}Shin\" };\r\n aliases[Role(\"Foot\", s)] = new[] { $\"{p}Foot\" };\r\n aliases[Role(\"Toe\", s)] = new[] { $\"{p}Toe\" };\r\n\r\n foreach (var (role, daz) in new[]\r\n {\r\n (\"Thumb\", \"Thumb\"), (\"Index\", \"Index\"), (\"Middle\", \"Mid\"), (\"Ring\", \"Ring\"), (\"Pinky\", \"Pinky\"),\r\n })\r\n {\r\n aliases[Role($\"{role}Prox\", s)] = new[] { $\"{p}{daz}1\" };\r\n aliases[Role($\"{role}Mid\", s)] = new[] { $\"{p}{daz}2\" };\r\n aliases[Role($\"{role}Dist\", s)] = new[] { $\"{p}{daz}3\" };\r\n }\r\n }\r\n return new Profile(\"daz_poser\", new string[0], aliases);\r\n }\r\n\r\n // ---------------------------------------------------------------- blender rigify\r\n\r\n private static Profile BuildRigify()\r\n {\r\n var aliases = new Dictionary<BoneRole, string[]>\r\n {\r\n // rigify's \"spine\" bone sits AT the pelvis and parents both thighs \u2014 it is\r\n // the hips, not a spine link (rigify/metarigs/human.py).\r\n [BoneRole.Hips] = new[] { \"spine\" },\r\n [BoneRole.Spine0] = new[] { \"spine.001\" },\r\n [BoneRole.Spine1] = new[] { \"spine.002\" },\r\n [BoneRole.Spine2] = new[] { \"spine.003\" },\r\n // spine.004 + spine.005 are the two neck bones, spine.006 the head;\r\n // spine.005 stays unmapped (same policy as ActorCore's NeckTwist02).\r\n [BoneRole.Neck] = new[] { \"spine.004\" },\r\n [BoneRole.Head] = new[] { \"spine.006\" },\r\n };\r\n foreach (var s in new[] { \"L\", \"R\" })\r\n {\r\n aliases[Role(\"Clavicle\", s)] = new[] { $\"shoulder.{s}\" };\r\n aliases[Role(\"UpperArm\", s)] = new[] { $\"upper_arm.{s}\" };\r\n aliases[Role(\"LowerArm\", s)] = new[] { $\"forearm.{s}\" };\r\n aliases[Role(\"Hand\", s)] = new[] { $\"hand.{s}\" };\r\n aliases[Role(\"UpperLeg\", s)] = new[] { $\"thigh.{s}\" };\r\n aliases[Role(\"LowerLeg\", s)] = new[] { $\"shin.{s}\" };\r\n aliases[Role(\"Foot\", s)] = new[] { $\"foot.{s}\" };\r\n aliases[Role(\"Toe\", s)] = new[] { $\"toe.{s}\" };\r\n\r\n foreach (var (role, rigify) in new[]\r\n {\r\n (\"Thumb\", \"thumb\"), (\"Index\", \"f_index\"), (\"Middle\", \"f_middle\"),\r\n (\"Ring\", \"f_ring\"), (\"Pinky\", \"f_pinky\"),\r\n })\r\n {\r\n aliases[Role($\"{role}Prox\", s)] = new[] { $\"{rigify}.01.{s}\" };\r\n aliases[Role($\"{role}Mid\", s)] = new[] { $\"{rigify}.02.{s}\" };\r\n aliases[Role($\"{role}Dist\", s)] = new[] { $\"{rigify}.03.{s}\" };\r\n }\r\n }\r\n // The generated deform skeleton prefixes every deform bone with \"DEF-\"; its\r\n // segmented limb twins (\"DEF-upper_arm.L.001\") keep their numeric suffix after\r\n // stripping and therefore never collide with the whole-bone aliases.\r\n return new Profile(\"rigify\", new[] { \"^DEF-\" }, aliases);\r\n }\r\n\r\n // ---------------------------------------------------------------- vroid / vrm\r\n\r\n private static Profile BuildVrm()\r\n {\r\n var aliases = new Dictionary<BoneRole, string[]>\r\n {\r\n [BoneRole.Hips] = new[] { \"J_Bip_C_Hips\" },\r\n [BoneRole.Spine0] = new[] { \"J_Bip_C_Spine\" },\r\n [BoneRole.Spine1] = new[] { \"J_Bip_C_Chest\" },\r\n [BoneRole.Spine2] = new[] { \"J_Bip_C_UpperChest\" },\r\n [BoneRole.Neck] = new[] { \"J_Bip_C_Neck\" },\r\n [BoneRole.Head] = new[] { \"J_Bip_C_Head\" },\r\n };\r\n foreach (var s in new[] { \"L\", \"R\" })\r\n {\r\n aliases[Role(\"Clavicle\", s)] = new[] { $\"J_Bip_{s}_Shoulder\" };\r\n aliases[Role(\"UpperArm\", s)] = new[] { $\"J_Bip_{s}_UpperArm\" };\r\n aliases[Role(\"LowerArm\", s)] = new[] { $\"J_Bip_{s}_LowerArm\" };\r\n aliases[Role(\"Hand\", s)] = new[] { $\"J_Bip_{s}_Hand\" };\r\n aliases[Role(\"UpperLeg\", s)] = new[] { $\"J_Bip_{s}_UpperLeg\" };\r\n aliases[Role(\"LowerLeg\", s)] = new[] { $\"J_Bip_{s}_LowerLeg\" };\r\n aliases[Role(\"Foot\", s)] = new[] { $\"J_Bip_{s}_Foot\" };\r\n aliases[Role(\"Toe\", s)] = new[] { $\"J_Bip_{s}_ToeBase\" };\r\n\r\n foreach (var (role, vrm) in new[]\r\n {\r\n (\"Thumb\", \"Thumb\"), (\"Index\", \"Index\"), (\"Middle\", \"Middle\"),\r\n (\"Ring\", \"Ring\"), (\"Pinky\", \"Little\"),\r\n })\r\n {\r\n aliases[Role($\"{role}Prox\", s)] = new[] { $\"J_Bip_{s}_{vrm}1\" };\r\n aliases[Role($\"{role}Mid\", s)] = new[] { $\"J_Bip_{s}_{vrm}2\" };\r\n aliases[Role($\"{role}Dist\", s)] = new[] { $\"J_Bip_{s}_{vrm}3\" };\r\n }\r\n }\r\n return new Profile(\"vrm\", new string[0], aliases);\r\n }\r\n\r\n // ---------------------------------------------------------------- auto-rig pro\r\n\r\n private static Profile BuildAutoRigPro()\r\n {\r\n var aliases = new Dictionary<BoneRole, string[]>\r\n {\r\n [BoneRole.Hips] = new[] { \"root.x\" },\r\n [BoneRole.Spine0] = new[] { \"spine_01.x\" },\r\n [BoneRole.Spine1] = new[] { \"spine_02.x\" },\r\n [BoneRole.Spine2] = new[] { \"spine_03.x\" },\r\n [BoneRole.Neck] = new[] { \"neck.x\" },\r\n [BoneRole.Head] = new[] { \"head.x\" },\r\n };\r\n foreach (var s in new[] { \"L\", \"R\" })\r\n {\r\n var p = s == \"L\" ? \"l\" : \"r\";\r\n aliases[Role(\"Clavicle\", s)] = new[] { $\"shoulder.{p}\" };\r\n aliases[Role(\"UpperArm\", s)] = new[] { $\"arm_stretch.{p}\" };\r\n aliases[Role(\"LowerArm\", s)] = new[] { $\"forearm_stretch.{p}\" };\r\n aliases[Role(\"Hand\", s)] = new[] { $\"hand.{p}\" };\r\n aliases[Role(\"UpperLeg\", s)] = new[] { $\"thigh_stretch.{p}\" };\r\n aliases[Role(\"LowerLeg\", s)] = new[] { $\"leg_stretch.{p}\" };\r\n aliases[Role(\"Foot\", s)] = new[] { $\"foot.{p}\" };\r\n aliases[Role(\"Toe\", s)] = new[] { $\"toes_01.{p}\" };\r\n\r\n // Exported finger deform bones keep ARP's c_ control prefix (Defenses.fbx).\r\n foreach (var finger in new[] { \"thumb\", \"index\", \"middle\", \"ring\", \"pinky\" })\r\n {\r\n var role = char.ToUpperInvariant(finger[0]) + finger[1..];\r\n aliases[Role($\"{role}Prox\", s)] = new[] { $\"c_{finger}1.{p}\" };\r\n aliases[Role($\"{role}Mid\", s)] = new[] { $\"c_{finger}2.{p}\" };\r\n aliases[Role($\"{role}Dist\", s)] = new[] { $\"c_{finger}3.{p}\" };\r\n }\r\n }\r\n return new Profile(\"auto_rig_pro\", new string[0], aliases);\r\n }\r\n\r\n // ---------------------------------------------------------------- helpers\r\n\r\n private static IEnumerable<(string RoleSide, string NameSide)> Sides()\r\n {\r\n yield return (\"L\", \"Left\");\r\n yield return (\"R\", \"Right\");\r\n }\r\n\r\n private static BoneRole Role(string baseName, string side)\r\n => System.Enum.Parse<BoneRole>(baseName + side);\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_retargeter",
"Path": "Code/HumanoidRetargeter/Solve/ClipMirror.cs",
"FileName": "ClipMirror.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 311783,
"Code": "#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Numerics;\r\nusing HumanoidRetargeter.Mapping;\r\nusing HumanoidRetargeter.Maths;\r\nusing HumanoidRetargeter.Target;\r\n\r\nnamespace HumanoidRetargeter.Solve;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)\r\n\r\n/// <summary>\r\n/// Mirrors a solved TARGET-space clip across the target character's sagittal plane,\r\n/// producing the left/right-swapped twin of an animation (e.g. a right-foot-lead walk from a\r\n/// left-foot-lead one).\r\n/// </summary>\r\n/// <remarks>\r\n/// <para><b>Mirror plane.</b> The plane through the rig-space origin spanned by the target\r\n/// character's up and forward directions; its normal is the character's LATERAL axis,\r\n/// computed from the target rig's rest geometry via <see cref=\"CharacterFrame\"/> (never\r\n/// hardcoded \u2014 an arbitrary target may be authored in any axis convention). When the\r\n/// computed lateral lies on a coordinate axis up to float dirt (< 1e-3 on the other two\r\n/// components \u2014 true for every axis-aligned authored rig, including the s&box citizen\r\n/// rigs), it is snapped to that exact axis, which makes every reflection below an EXACT\r\n/// sign-flip in IEEE arithmetic and therefore the whole mirror a bit-exact involution\r\n/// (mirror \u2218 mirror == identity, verified by test).</para>\r\n/// <para><b>Math.</b> Let M = I \u2212 2n\u0302n\u0302\u1d40 be the reflection across the plane with unit normal\r\n/// n\u0302. A world transform W = (R, t) maps to its mirror image by conjugation:\r\n/// W\u2032 = M\u0302 \u2218 W \u2218 M\u0302 (M\u0302 is its own inverse), giving rotation R\u2032 = M\u00b7R\u00b7M and translation\r\n/// t\u2032 = M\u00b7t. For a quaternion q = (v, w), M\u00b7R\u00b7M is the rotation by the SAME angle about the\r\n/// REFLECTED axis with REVERSED sense (a reflection flips orientation), i.e.\r\n/// q\u2032 = (2(n\u0302\u00b7v)n\u0302 \u2212 v, w); with n\u0302 = +X that is exactly q\u2032 = (x, \u2212y, \u2212z, w), and positions\r\n/// reflect as p\u2032 = p \u2212 2(n\u0302\u00b7p)n\u0302 = (\u2212p\u2093, p_y, p_z).</para>\r\n/// <para><b>Locals, not worlds.</b> Because conjugation is a homomorphism\r\n/// (M\u0302(AB)M\u0302 = (M\u0302AM\u0302)(M\u0302BM\u0302)) and world transforms are products of locals down the\r\n/// hierarchy, mirroring every LOCAL transform and permuting bones by their L\u2194R partner is\r\n/// exactly equivalent to mirroring the FK worlds \u2014 provided the partner permutation is\r\n/// hierarchy-consistent (the partner's parent is the parent's partner), which is validated\r\n/// and holds on structurally symmetric humanoid rigs. This avoids FK\u2192inverse-FK float drift\r\n/// entirely, which is what makes the double-mirror identity bit-exact.</para>\r\n/// <para><b>Pairing.</b> Left/right bones are paired by the rig's canonical role annotations\r\n/// first (UpperArmL \u2194 UpperArmR, \u2026); role-less bones (twist helpers, IK bones) fall back to\r\n/// <c>_L</c>/<c>_R</c> name-token pairing (<c>arm_upper_L_twist0</c> \u2194\r\n/// <c>arm_upper_R_twist0</c>, <c>foot_L_IK_target</c> \u2194 <c>foot_R_IK_target</c>); anything\r\n/// unpaired (center bones: pelvis, spine, neck, head) mirrors in place, which reflects its\r\n/// rotation across the sagittal plane and negates its lateral translation. IK-baked helper\r\n/// bones are NOT re-baked after mirroring: conjugation is a homomorphism, so the mirrored\r\n/// copies of the primary clip's final helper channels already hang in exactly the mirrored\r\n/// relationship over the mirrored body (re-baking encoded a divergent convention, the Gate 3\r\n/// review's mechanism 2 of the _M render defect; southpaw project, gate3_review.md 3.4).\r\n/// Channel exclusions on mirrored clips go through <see cref=\"MirrorSafeExclusions\"/>.</para>\r\n/// </remarks>\r\npublic static class ClipMirror\r\n{\r\n /// <summary>Maximum off-axis component magnitude below which the computed lateral axis is\r\n /// snapped to the exact coordinate axis (authored rigs are axis-aligned; the tiny rest\r\n /// asymmetries of a real mesh stay far below this).</summary>\r\n private const float AxisSnapTolerance = 1e-3f;\r\n\r\n /// <summary>\r\n /// Returns the mirrored copy of <paramref name=\"frames\"/> (one new list, inputs\r\n /// untouched): per frame, bone i takes the conjugated local transform of its L\u2194R partner\r\n /// \u03c3(i). See the class remarks for the math and pairing rules.\r\n /// </summary>\r\n /// <param name=\"frames\">Solved per-frame local transforms (target skeleton bone order).</param>\r\n /// <param name=\"rig\">The target rig (skeleton + roles) the frames belong to.</param>\r\n /// <exception cref=\"ArgumentException\">Thrown when the rig maps a sided role without its\r\n /// counterpart, the pairing is not hierarchy-consistent, or the character frame is not\r\n /// computable \u2014 mirroring would silently produce garbage in those cases.</exception>\r\n public static List<XForm[]> Mirror(List<XForm[]> frames, TargetRig rig)\r\n {\r\n ArgumentNullException.ThrowIfNull(frames);\r\n ArgumentNullException.ThrowIfNull(rig);\r\n\r\n var skeleton = rig.Skeleton;\r\n var lateral = LateralAxis(rig);\r\n var pair = BuildPairing(rig);\r\n var fkFix = HierarchyInconsistentBones(rig, pair);\r\n\r\n var result = new List<XForm[]>(frames.Count);\r\n var baseWorld = fkFix.Count > 0 ? new XForm[skeleton.Count] : null;\r\n var mirrorWorld = fkFix.Count > 0 ? new XForm[skeleton.Count] : null;\r\n foreach (var locals in frames)\r\n {\r\n if (locals.Length != skeleton.Count)\r\n throw new ArgumentException(\r\n $\"Frame has {locals.Length} bones but the target skeleton has {skeleton.Count}.\",\r\n nameof(frames));\r\n\r\n var mirrored = new XForm[locals.Length];\r\n for (var i = 0; i < locals.Length; i++)\r\n {\r\n var source = locals[pair[i]];\r\n mirrored[i] = new XForm(\r\n ReflectPoint(source.Pos, lateral),\r\n ReflectRotation(source.Rot, lateral));\r\n }\r\n\r\n // Hierarchy-inconsistent pairs (the citizen parents arm_elbow_helper_R under\r\n // arm_lower_R_twist0 while _L hangs under arm_lower_L, the W3a-documented\r\n // rig quirk): the partner's conjugated LOCAL under a non-mirrored parent\r\n // chain misplaces the bone by the parent-chain difference (measured ~1 in on\r\n // the elbow/knee helpers). Solve their locals by FK so the mirrored WORLD is\r\n // the exact reflection of the partner's world (southpaw G8 mirror fix).\r\n if (fkFix.Count > 0)\r\n {\r\n for (var i = 0; i < skeleton.Count; i++)\r\n {\r\n var parent = skeleton[i].ParentIndex;\r\n baseWorld![i] = parent < 0\r\n ? locals[i]\r\n : XForm.Compose(baseWorld[parent], locals[i]);\r\n }\r\n for (var i = 0; i < skeleton.Count; i++)\r\n {\r\n var parent = skeleton[i].ParentIndex;\r\n if (fkFix.Contains(i))\r\n {\r\n var desired = new XForm(\r\n ReflectPoint(baseWorld![pair[i]].Pos, lateral),\r\n ReflectRotation(baseWorld[pair[i]].Rot, lateral));\r\n mirrored[i] = parent < 0\r\n ? desired\r\n : XForm.ToLocal(mirrorWorld![parent], desired);\r\n }\r\n mirrorWorld![i] = parent < 0\r\n ? mirrored[i]\r\n : XForm.Compose(mirrorWorld[parent], mirrored[i]);\r\n }\r\n }\r\n result.Add(mirrored);\r\n }\r\n return result;\r\n }\r\n\r\n /// <summary>\r\n /// Bones whose L/R pairing is NOT hierarchy-consistent (the partner hangs under a\r\n /// non-mirrored parent). Only constraint-driven helpers can reach this state\r\n /// (<see cref=\"BuildPairing\"/> fails hard for any other bone); their mirrored locals\r\n /// need the FK solve in <see cref=\"Mirror\"/> and their DMX channels must be written\r\n /// (<see cref=\"MirrorSafeExclusions\"/>).\r\n /// </summary>\r\n private static HashSet<int> HierarchyInconsistentBones(TargetRig rig, int[] pair)\r\n {\r\n var skeleton = rig.Skeleton;\r\n var result = new HashSet<int>();\r\n for (var i = 0; i < pair.Length; i++)\r\n {\r\n var parent = skeleton[i].ParentIndex;\r\n var partnerParent = skeleton[pair[i]].ParentIndex;\r\n var consistent = parent < 0\r\n ? partnerParent < 0\r\n : partnerParent == pair[parent];\r\n if (!consistent)\r\n result.Add(i);\r\n }\r\n return result;\r\n }\r\n\r\n /// <summary>\r\n /// Filters a channel-exclusion set for a MIRRORED clip: returns the subset of\r\n /// <paramref name=\"excluded\"/> that is still safe to leave channel-less after mirroring.\r\n /// A channel-less bone is rendered/baked at its own REST local under its (mirrored)\r\n /// parent; the mirrored frames instead carry the conjugated rest local of the bone's\r\n /// L/R partner. Those agree only when the rig's rest locals are mirror conjugates\r\n /// (restLocal(i) == conjugate(restLocal(partner(i)))). Bones breaking that symmetry\r\n /// (measured on the citizen rig: leg/arm *_twist1 chains and neck_clothing, 19 to 37 cm\r\n /// off) MUST keep explicit mirrored channels or every data consumer (model compiler\r\n /// sequence bake, render-side helper evaluation) places them wrong: the Gate 3 review's\r\n /// MECHANISM 1 of the _M render defect (southpaw project, gate3_review.md 3.4).\r\n /// Truly symmetric helpers stay excluded exactly as on primary clips.\r\n /// </summary>\r\n /// <param name=\"rig\">The target rig the exclusion set belongs to.</param>\r\n /// <param name=\"excluded\">The primary-clip exclusion set (constraint-driven bones).</param>\r\n /// <returns>The mirror-safe subset, or null when nothing remains excluded.</returns>\r\n public static IReadOnlySet<int>? MirrorSafeExclusions(TargetRig rig, IReadOnlySet<int>? excluded)\r\n {\r\n if (excluded is null || excluded.Count == 0)\r\n return excluded;\r\n\r\n var skeleton = rig.Skeleton;\r\n var lateral = LateralAxis(rig);\r\n var pair = BuildPairing(rig);\r\n var inconsistent = HierarchyInconsistentBones(rig, pair);\r\n\r\n const float posTolCm = 0.1f;\r\n const float rotTolDeg = 0.5f;\r\n var cosTol = MathF.Cos(rotTolDeg * MathF.PI / 360f); // half-angle for quat dot\r\n\r\n var safe = new HashSet<int>();\r\n foreach (var i in excluded)\r\n {\r\n // Hierarchy-inconsistent pairs always need explicit channels: their mirrored\r\n // locals are FK-solved (see Mirror) and no rest local can stand in for them.\r\n if (inconsistent.Contains(i))\r\n continue;\r\n\r\n var own = skeleton[i].RestLocal;\r\n var partnerRest = skeleton[pair[i]].RestLocal;\r\n var needed = new XForm(\r\n ReflectPoint(partnerRest.Pos, lateral),\r\n ReflectRotation(partnerRest.Rot, lateral));\r\n\r\n var posOk = (own.Pos - needed.Pos).Length() <= posTolCm;\r\n var dot = MathF.Abs(\r\n own.Rot.X * needed.Rot.X + own.Rot.Y * needed.Rot.Y\r\n + own.Rot.Z * needed.Rot.Z + own.Rot.W * needed.Rot.W);\r\n var rotOk = dot >= cosTol;\r\n if (posOk && rotOk)\r\n safe.Add(i);\r\n }\r\n return safe.Count > 0 ? safe : null;\r\n }\r\n\r\n // ================================================================ mirror plane\r\n\r\n /// <summary>The unit mirror normal: the target character's lateral axis from rest\r\n /// geometry, snapped to an exact coordinate axis when within tolerance (bit-exact\r\n /// reflections, see class remarks).</summary>\r\n private static Vector3 LateralAxis(TargetRig rig)\r\n {\r\n Vector3 lateral;\r\n try\r\n {\r\n lateral = CharacterFrame.Compute(\r\n rig.Skeleton, rig.ToMappingResult(), rig.Skeleton.RestWorld).Lateral;\r\n }\r\n catch (ArgumentException e)\r\n {\r\n throw new ArgumentException(\r\n $\"Cannot mirror: target character frame not computable ({e.Message}).\", e);\r\n }\r\n\r\n var a = Vector3.Abs(lateral);\r\n if (a.Y <= AxisSnapTolerance && a.Z <= AxisSnapTolerance)\r\n return Vector3.UnitX;\r\n if (a.X <= AxisSnapTolerance && a.Z <= AxisSnapTolerance)\r\n return Vector3.UnitY;\r\n if (a.X <= AxisSnapTolerance && a.Y <= AxisSnapTolerance)\r\n return Vector3.UnitZ;\r\n return lateral; // general (non-axis-aligned) rig: exact involution is lost, math is not\r\n }\r\n\r\n /// <summary>p\u2032 = p \u2212 2(n\u0302\u00b7p)n\u0302. With a snapped axis this is an exact sign flip of one\r\n /// component (IEEE subtraction of representable values is exact).</summary>\r\n private static Vector3 ReflectPoint(Vector3 p, Vector3 n)\r\n => p - 2f * Vector3.Dot(p, n) * n;\r\n\r\n /// <summary>q\u2032 = (2(n\u0302\u00b7v)n\u0302 \u2212 v, w): the conjugated rotation M\u00b7R\u00b7M \u2014 same angle, axis\r\n /// reflected, sense reversed. With n\u0302 = +X this is (x, \u2212y, \u2212z, w). Components are\r\n /// preserved exactly (no renormalization), keeping the double mirror bit-exact.</summary>\r\n private static Quaternion ReflectRotation(Quaternion q, Vector3 n)\r\n {\r\n var v = new Vector3(q.X, q.Y, q.Z);\r\n var reflected = 2f * Vector3.Dot(v, n) * n - v;\r\n return new Quaternion(reflected.X, reflected.Y, reflected.Z, q.W);\r\n }\r\n\r\n // ================================================================ L\u2194R pairing\r\n\r\n /// <summary>\r\n /// \u03c3: bone \u2192 mirror partner (identity for center/unpaired bones). Roles pair first;\r\n /// role-less bones pair by <c>_L</c>/<c>_R</c> name tokens. Validated to be an involution\r\n /// consistent with the hierarchy (\u03c3(parent(i)) == parent(\u03c3(i))).\r\n /// </summary>\r\n private static int[] BuildPairing(TargetRig rig)\r\n {\r\n var skeleton = rig.Skeleton;\r\n var pair = new int[skeleton.Count];\r\n for (var i = 0; i < pair.Length; i++)\r\n pair[i] = i;\r\n\r\n for (var i = 0; i < skeleton.Count; i++)\r\n {\r\n if (rig.RoleOf(i) is { } role)\r\n {\r\n if (MirrorRole(role) is not { } mirroredRole)\r\n continue; // center role: mirrors in place\r\n pair[i] = rig.BoneForRole(mirroredRole)\r\n ?? throw new ArgumentException(\r\n $\"Cannot mirror: target rig maps role {role} ('{skeleton[i].Name}') \"\r\n + $\"but not its counterpart {mirroredRole}.\");\r\n }\r\n else\r\n {\r\n var partnerName = SwapSideTokens(skeleton[i].Name);\r\n if (partnerName is null)\r\n continue; // no side token: center bone\r\n var partner = skeleton.IndexOf(partnerName);\r\n if (partner >= 0)\r\n pair[i] = partner;\r\n // No partner bone: leave in place (e.g. an asymmetric prop bone) \u2014 its\r\n // rotation still mirrors across the sagittal plane.\r\n }\r\n }\r\n\r\n for (var i = 0; i < pair.Length; i++)\r\n {\r\n // Constraint-driven helper bones are excluded from the output DMX (the model's\r\n // AnimConstraintList re-drives them at runtime, see Retargeter.EmitClip\r\n // ChannelExcludedBones), so their mirrored channels are never written. The shipped\r\n // s&box citizen rig parents these asymmetrically (arm_elbow_helper_R hangs under\r\n // arm_lower_R_twist0 while arm_elbow_helper_L hangs under arm_lower_L), a benign\r\n // data quirk that must not fail the whole mirror. Skip the strict L/R\r\n // hierarchy-consistency requirement for them: their pairing does not affect any\r\n // written channel. (W3a fix, southpaw project.)\r\n if (rig.HelpersAreConstraintDriven && rig.ClassOf(i) == BoneClass.ConstraintDriven)\r\n continue;\r\n\r\n if (pair[pair[i]] != i)\r\n throw new ArgumentException(\r\n $\"Cannot mirror: bone pairing is not symmetric ('{skeleton[i].Name}' \u2192 \"\r\n + $\"'{skeleton[pair[i]].Name}' \u2192 '{skeleton[pair[pair[i]]].Name}').\");\r\n\r\n var parent = skeleton[i].ParentIndex;\r\n var partnerParent = skeleton[pair[i]].ParentIndex;\r\n var consistent = parent < 0\r\n ? partnerParent < 0\r\n : partnerParent == pair[parent];\r\n if (!consistent)\r\n throw new ArgumentException(\r\n $\"Cannot mirror: left/right pairing is not hierarchy-consistent \u2014 \"\r\n + $\"'{skeleton[i].Name}' and partner '{skeleton[pair[i]].Name}' hang under \"\r\n + \"non-mirrored parents.\");\r\n }\r\n\r\n return pair;\r\n }\r\n\r\n /// <summary>UpperArmL \u2192 UpperArmR (and back); null for center roles. Every sided\r\n /// <see cref=\"BoneRole\"/> ends in <c>L</c>/<c>R</c>; no center role does.</summary>\r\n private static BoneRole? MirrorRole(BoneRole role)\r\n {\r\n var name = role.ToString();\r\n var mirroredName = name[^1] switch\r\n {\r\n 'L' => name[..^1] + \"R\",\r\n 'R' => name[..^1] + \"L\",\r\n _ => null,\r\n };\r\n return mirroredName is not null && Enum.TryParse<BoneRole>(mirroredName, out var mirrored)\r\n ? mirrored\r\n : null;\r\n }\r\n\r\n /// <summary>Swaps <c>L</c>/<c>R</c> underscore-delimited name tokens\r\n /// (<c>foot_L_IK_target</c> \u2192 <c>foot_R_IK_target</c>); null when the name carries no\r\n /// side token.</summary>\r\n private static string? SwapSideTokens(string name)\r\n {\r\n var tokens = name.Split('_');\r\n for (var i = 0; i < tokens.Length; i++)\r\n {\r\n tokens[i] = tokens[i] switch\r\n {\r\n \"L\" => \"R\",\r\n \"R\" => \"L\",\r\n \"l\" => \"r\",\r\n \"r\" => \"l\",\r\n _ => tokens[i],\r\n };\r\n }\r\n var result = string.Join('_', tokens);\r\n return string.Equals(result, name, StringComparison.Ordinal) ? null : result;\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_retargeter",
"Path": "Code/HumanoidRetargeter/Solve/FingerSolver.cs",
"FileName": "FingerSolver.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 311783,
"Code": "#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Numerics;\r\nusing HumanoidRetargeter.Mapping;\r\nusing HumanoidRetargeter.Maths;\r\n\r\nnamespace HumanoidRetargeter.Solve;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)\r\n\r\n/// <summary>\r\n/// Finger retargeting. Picks one of three strategies per finger chain:\r\n/// <list type=\"number\">\r\n/// <item><b>1:1 absolute copy</b> (via the <c>transferOneToOne</c> callback into\r\n/// <see cref=\"GeometricSolver\"/>'s body path) when the source and target chains are\r\n/// <i>geometrically identical</i> \u2014 same mapped role set, same canonical frames, same\r\n/// normalized rest rotations. This is the same-rig round-trip case and is lossless (exact\r\n/// identity, twist included).</item>\r\n/// <item><b>Direction matching</b> when the phalanx counts match ordinally but the rigs\r\n/// differ (the common cross-rig case, e.g. Mixamo Prox/Mid/Dist onto the s&box finger\r\n/// with its extra metacarpal \u2014 which keeps its rest local; a source metacarpal's rotation is\r\n/// implicit in the proximal's absolute direction). Each target phalanx is swung \u2014 shortest\r\n/// arc, rotation axis \u22a5 the finger axis, hence <b>zero twist by construction</b> \u2014 so that its\r\n/// segment direction matches the source phalanx's direction in character-frame coordinates\r\n/// exactly. Curl and splay are both captured by the direction; the source's axial twist is\r\n/// dropped (hinge-joint noise; copying it absolutely would read as roll through the\r\n/// inter-phalanx canonical mismatch between rigs, measured up to ~12\u00b0 on thumbs).</item>\r\n/// <item><b>Proportional redistribution</b> when phalanx counts differ (e.g. a two-phalanx\r\n/// source finger): per-phalanx local curls \u2014 swing-twist about the canonical hinge Y of\r\n/// <c>\u03bb_b = C_b\u207b\u00b9\u00b7(\u0394R_prev\u207b\u00b9\u00b7\u0394R_b)\u00b7C_b</c> \u2014 are summed over the source chain (metacarpal\r\n/// included) and redistributed over the target phalanges proportional to rest segment\r\n/// lengths; splay (metacarpal + proximal, swing-twist about canonical Z) goes 100% to the\r\n/// target proximal; the X-twist residual is dropped.</item>\r\n/// </list>\r\n/// In every mode target world deltas rebuild hierarchically from the solved target hand:\r\n/// <c>\u0394R_i = \u0394R_{i-1} \u00b7 (C_i \u00b7 \u03bb_i \u00b7 C_i\u207b\u00b9)</c>, then <c>W_i = \u0394R_i \u00b7 R_tgtNormRest,i</c>.\r\n/// Instances are per-solve and not thread-safe.\r\n/// </summary>\r\ninternal sealed class FingerSolver\r\n{\r\n /// <summary>Two canonical frames / rest rotations within this angle count as identical\r\n /// (same-rig detection for the lossless 1:1 path); cross-rig differences are degrees.</summary>\r\n private const float SameRigToleranceRad = 1e-3f;\r\n\r\n private enum ChainMode\r\n {\r\n DirectionMatch,\r\n Proportional,\r\n }\r\n\r\n private readonly struct SourcePhalanx\r\n {\r\n public required int Slot { get; init; }\r\n public required Quaternion C { get; init; }\r\n public required Quaternion CInv { get; init; }\r\n public required bool TakesSplay { get; init; }\r\n }\r\n\r\n private readonly struct Recipient\r\n {\r\n public required int TgtBone { get; init; }\r\n public required Quaternion C { get; init; }\r\n public required Quaternion CInv { get; init; }\r\n public required Quaternion RestRot { get; init; }\r\n public required float Weight { get; init; }\r\n public required bool Splay { get; init; }\r\n }\r\n\r\n private sealed class Chain\r\n {\r\n public required ChainMode Mode { get; init; }\r\n public required int SrcHandSlot { get; init; }\r\n public required int TgtHandBone { get; init; }\r\n public required Quaternion TgtHandNormRestRotInv { get; init; }\r\n public required SourcePhalanx[] Sources { get; init; }\r\n public required Recipient[] Recipients { get; init; }\r\n }\r\n\r\n private readonly List<Chain> _chains;\r\n private readonly Quaternion _chrSrcInv;\r\n private readonly Quaternion _chrTgt;\r\n\r\n private FingerSolver(List<Chain> chains, Quaternion chrSrcInv, Quaternion chrTgt)\r\n {\r\n _chains = chains;\r\n _chrSrcInv = chrSrcInv;\r\n _chrTgt = chrTgt;\r\n }\r\n\r\n // ---------------------------------------------------------------- role tables\r\n\r\n private static readonly BoneRole[][] ChainRoles = BuildChainRoles();\r\n private static readonly HashSet<BoneRole> FingerRoleSet = ChainRoles.SelectMany(c => c.Skip(1)).ToHashSet();\r\n\r\n private static BoneRole[][] BuildChainRoles()\r\n {\r\n var chains = new List<BoneRole[]>();\r\n foreach (var side in new[] { \"L\", \"R\" })\r\n {\r\n foreach (var finger in new[] { \"Thumb\", \"Index\", \"Middle\", \"Ring\", \"Pinky\" })\r\n {\r\n // Element 0 is the hand the chain hangs off; 1.. are Meta/Prox/Mid/Dist.\r\n chains.Add(new[]\r\n {\r\n Enum.Parse<BoneRole>(\"Hand\" + side),\r\n Enum.Parse<BoneRole>(finger + \"Meta\" + side),\r\n Enum.Parse<BoneRole>(finger + \"Prox\" + side),\r\n Enum.Parse<BoneRole>(finger + \"Mid\" + side),\r\n Enum.Parse<BoneRole>(finger + \"Dist\" + side),\r\n });\r\n }\r\n }\r\n return chains.ToArray();\r\n }\r\n\r\n /// <summary>True for the 40 per-finger segment roles (Meta/Prox/Mid/Dist \u00d7 finger \u00d7 side).</summary>\r\n public static bool IsFingerRole(BoneRole role) => FingerRoleSet.Contains(role);\r\n\r\n // ---------------------------------------------------------------- build\r\n\r\n /// <summary>\r\n /// Builds the per-chain plans. Geometrically identical chains are reported through\r\n /// <paramref name=\"transferOneToOne\"/> instead of being planned here. Returns null when\r\n /// every mapped chain took that path (or none is mapped).\r\n /// </summary>\r\n public static FingerSolver? Build(\r\n MappingResult sourceMap,\r\n CanonicalFrames srcCanon,\r\n IReadOnlyList<XForm> srcNormRest,\r\n Func<BoneRole, int?> tgtBoneForRole,\r\n CanonicalFrames tgtCanon,\r\n IReadOnlyList<XForm> tgtNormRest,\r\n Quaternion chrSrcInv,\r\n Quaternion chrTgt,\r\n Func<int, int> registerSlot,\r\n Action<BoneRole> transferOneToOne)\r\n {\r\n var chains = new List<Chain>();\r\n foreach (var chainRoles in ChainRoles)\r\n {\r\n var handRole = chainRoles[0];\r\n var metaRole = chainRoles[1];\r\n var proxRole = chainRoles[2];\r\n var segments = chainRoles.Skip(1).ToArray();\r\n\r\n var srcRoles = segments\r\n .Where(r => sourceMap.RoleToBone.ContainsKey(r) && srcCanon.Has(r))\r\n .ToArray();\r\n var tgtRoles = segments\r\n .Where(r => tgtBoneForRole(r) is not null && tgtCanon.Has(r))\r\n .ToArray();\r\n if (srcRoles.Length == 0 || tgtRoles.Length == 0)\r\n continue;\r\n\r\n if (srcRoles.SequenceEqual(tgtRoles) && ChainsCoincide(\r\n srcRoles, sourceMap, srcCanon, srcNormRest, tgtBoneForRole, tgtCanon, tgtNormRest))\r\n {\r\n foreach (var role in srcRoles)\r\n transferOneToOne(role);\r\n continue;\r\n }\r\n\r\n var srcPhalanges = srcRoles.Where(r => r != metaRole).ToArray();\r\n var tgtPhalanges = tgtRoles.Where(r => r != metaRole).ToArray();\r\n var recipientRoles = tgtPhalanges.Length > 0 ? tgtPhalanges : tgtRoles;\r\n var mode = srcPhalanges.Length == recipientRoles.Length && srcPhalanges.Length > 0\r\n ? ChainMode.DirectionMatch\r\n : ChainMode.Proportional;\r\n\r\n // Direction matching consumes only the non-meta phalanges (the metacarpal's\r\n // motion is implicit in the proximal's absolute direction); redistribution\r\n // decomposes every mapped source segment including the metacarpal.\r\n var sourceRolesUsed = mode == ChainMode.DirectionMatch ? srcPhalanges : srcRoles;\r\n var sources = sourceRolesUsed.Select(r =>\r\n {\r\n var c = srcCanon.WorldFrameOf(r);\r\n return new SourcePhalanx\r\n {\r\n Slot = registerSlot(sourceMap.RoleToBone[r]),\r\n C = c,\r\n CInv = Quaternion.Conjugate(c),\r\n TakesSplay = r == metaRole || r == proxRole,\r\n };\r\n }).ToArray();\r\n\r\n var weights = SegmentWeights(tgtRoles, recipientRoles, tgtBoneForRole, tgtNormRest);\r\n var recipients = recipientRoles.Select((r, i) =>\r\n {\r\n var bone = tgtBoneForRole(r)!.Value;\r\n var c = tgtCanon.WorldFrameOf(r);\r\n return new Recipient\r\n {\r\n TgtBone = bone,\r\n C = c,\r\n CInv = Quaternion.Conjugate(c),\r\n RestRot = tgtNormRest[bone].Rot,\r\n Weight = weights[i],\r\n Splay = i == 0,\r\n };\r\n }).ToArray();\r\n\r\n var tgtHand = tgtBoneForRole(handRole);\r\n chains.Add(new Chain\r\n {\r\n Mode = mode,\r\n SrcHandSlot = sourceMap.RoleToBone.TryGetValue(handRole, out var srcHand)\r\n ? registerSlot(srcHand)\r\n : -1,\r\n TgtHandBone = tgtHand ?? -1,\r\n TgtHandNormRestRotInv = tgtHand is int h\r\n ? Quaternion.Conjugate(tgtNormRest[h].Rot)\r\n : Quaternion.Identity,\r\n Sources = sources,\r\n Recipients = recipients,\r\n });\r\n }\r\n\r\n return chains.Count > 0 ? new FingerSolver(chains, chrSrcInv, chrTgt) : null;\r\n }\r\n\r\n /// <summary>Same-rig detection: every chain member's canonical frame and normalized rest\r\n /// rotation agree between source and target (within float noise). Only then is the 1:1\r\n /// absolute copy lossless.</summary>\r\n private static bool ChainsCoincide(\r\n BoneRole[] roles, MappingResult sourceMap, CanonicalFrames srcCanon,\r\n IReadOnlyList<XForm> srcNormRest, Func<BoneRole, int?> tgtBoneForRole,\r\n CanonicalFrames tgtCanon, IReadOnlyList<XForm> tgtNormRest)\r\n {\r\n foreach (var role in roles)\r\n {\r\n var srcBone = sourceMap.RoleToBone[role];\r\n var tgtBone = tgtBoneForRole(role)!.Value;\r\n if (MathQ.AngleBetween(srcCanon.WorldFrameOf(role), tgtCanon.WorldFrameOf(role)) > SameRigToleranceRad\r\n || MathQ.AngleBetween(srcNormRest[srcBone].Rot, tgtNormRest[tgtBone].Rot) > SameRigToleranceRad)\r\n {\r\n return false;\r\n }\r\n }\r\n return true;\r\n }\r\n\r\n /// <summary>Normalized rest segment lengths of the recipient phalanges (the proportional\r\n /// curl weights). The distal segment, having no chain child, is estimated as 0.8\u00d7 its\r\n /// preceding segment.</summary>\r\n private static float[] SegmentWeights(\r\n BoneRole[] tgtRoles, BoneRole[] recipientRoles,\r\n Func<BoneRole, int?> tgtBoneForRole, IReadOnlyList<XForm> tgtNormRest)\r\n {\r\n var positions = tgtRoles.Select(r => tgtNormRest[tgtBoneForRole(r)!.Value].Pos).ToArray();\r\n var weights = new float[recipientRoles.Length];\r\n for (var i = 0; i < recipientRoles.Length; i++)\r\n {\r\n var j = Array.IndexOf(tgtRoles, recipientRoles[i]);\r\n weights[i] = j + 1 < positions.Length\r\n ? (positions[j + 1] - positions[j]).Length()\r\n : j > 0 ? 0.8f * (positions[j] - positions[j - 1]).Length() : 1f;\r\n }\r\n\r\n var sum = weights.Sum();\r\n if (sum <= 1e-6f)\r\n return Enumerable.Repeat(1f / weights.Length, weights.Length).ToArray();\r\n for (var i = 0; i < weights.Length; i++)\r\n weights[i] /= sum;\r\n return weights;\r\n }\r\n\r\n // ---------------------------------------------------------------- per frame\r\n\r\n /// <summary>\r\n /// Solves the planned chains for one frame. <paramref name=\"srcDeltas\"/> holds the\r\n /// registered source world rotation deltas (from normalized rest); solved target world\r\n /// rotations are written into <paramref name=\"rot\"/>/<paramref name=\"solved\"/>. The target\r\n /// hands must already be solved (body pass runs first).\r\n /// </summary>\r\n public void Apply(Quaternion[] srcDeltas, bool[] solved, Quaternion[] rot)\r\n {\r\n foreach (var chain in _chains)\r\n {\r\n var acc = chain.TgtHandBone >= 0 && solved[chain.TgtHandBone]\r\n ? MathQ.Normalize(rot[chain.TgtHandBone] * chain.TgtHandNormRestRotInv)\r\n : Quaternion.Identity;\r\n\r\n if (chain.Mode == ChainMode.DirectionMatch)\r\n ApplyDirectionMatch(chain, srcDeltas, acc, solved, rot);\r\n else\r\n ApplyProportional(chain, srcDeltas, acc, solved, rot);\r\n }\r\n }\r\n\r\n private void ApplyDirectionMatch(\r\n Chain chain, Quaternion[] srcDeltas, Quaternion acc, bool[] solved, Quaternion[] rot)\r\n {\r\n for (var i = 0; i < chain.Recipients.Length; i++)\r\n {\r\n var sp = chain.Sources[i];\r\n var rc = chain.Recipients[i];\r\n\r\n // Source phalanx direction in character coords; re-expressed in the target world,\r\n // then relative to the already-reconstructed parent delta, then in the phalanx's\r\n // canonical frame \u2014 where the rest direction is unit X.\r\n var srcAbs = MathQ.Normalize(_chrSrcInv * srcDeltas[sp.Slot] * sp.C);\r\n var dirChr = Vector3.Transform(Vector3.UnitX, srcAbs);\r\n var dirTgtWorld = Vector3.Transform(dirChr, _chrTgt);\r\n var dirLocal = Vector3.Transform(dirTgtWorld, Quaternion.Conjugate(acc));\r\n var dirCanon = Vector3.Transform(dirLocal, rc.CInv);\r\n\r\n // Shortest-arc swing X -> dir: rotation axis \u22a5 X, so it carries zero finger-axis\r\n // twist by construction.\r\n var swing = MathQ.FromTo(Vector3.UnitX, dirCanon);\r\n\r\n acc = MathQ.Normalize(acc * (rc.C * swing * rc.CInv));\r\n rot[rc.TgtBone] = MathQ.Normalize(acc * rc.RestRot);\r\n solved[rc.TgtBone] = true;\r\n }\r\n }\r\n\r\n private static void ApplyProportional(\r\n Chain chain, Quaternion[] srcDeltas, Quaternion acc, bool[] solved, Quaternion[] rot)\r\n {\r\n // Decompose: total local curl over the chain, splay from metacarpal + proximal.\r\n var prev = chain.SrcHandSlot >= 0 ? srcDeltas[chain.SrcHandSlot] : Quaternion.Identity;\r\n float totalCurl = 0f, splay = 0f;\r\n foreach (var sp in chain.Sources)\r\n {\r\n var dr = srcDeltas[sp.Slot];\r\n var local = MathQ.Normalize(Quaternion.Conjugate(prev) * dr);\r\n var canon = MathQ.Normalize(sp.CInv * local * sp.C);\r\n\r\n MathQ.SwingTwist(canon, Vector3.UnitY, out var swing, out var curlQ);\r\n totalCurl += SignedAngle(curlQ, Vector3.UnitY);\r\n\r\n if (sp.TakesSplay)\r\n {\r\n MathQ.SwingTwist(swing, Vector3.UnitZ, out _, out var splayQ);\r\n splay += SignedAngle(splayQ, Vector3.UnitZ);\r\n }\r\n\r\n prev = dr;\r\n }\r\n\r\n foreach (var rc in chain.Recipients)\r\n {\r\n var mu = Quaternion.CreateFromAxisAngle(Vector3.UnitY, totalCurl * rc.Weight);\r\n if (rc.Splay)\r\n mu = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, splay) * mu;\r\n\r\n acc = MathQ.Normalize(acc * (rc.C * mu * rc.CInv));\r\n rot[rc.TgtBone] = MathQ.Normalize(acc * rc.RestRot);\r\n solved[rc.TgtBone] = true;\r\n }\r\n }\r\n\r\n /// <summary>Signed rotation angle of an axis-aligned twist quaternion about\r\n /// <paramref name=\"axis\"/>, wrapped to (\u2212\u03c0, \u03c0].</summary>\r\n private static float SignedAngle(Quaternion twist, Vector3 axis)\r\n {\r\n var s = twist.X * axis.X + twist.Y * axis.Y + twist.Z * axis.Z;\r\n var angle = 2f * MathF.Atan2(s, twist.W);\r\n if (angle > MathF.PI)\r\n angle -= 2f * MathF.PI;\r\n else if (angle < -MathF.PI)\r\n angle += 2f * MathF.PI;\r\n return angle;\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_retargeter",
"Path": "Code/HumanoidRetargeter/Solve/SolveOptions.cs",
"FileName": "SolveOptions.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 311783,
"Code": "#nullable enable annotations\r\n\r\nusing System.Collections.Generic;\r\nusing HumanoidRetargeter.Mapping;\r\n\r\nnamespace HumanoidRetargeter.Solve;\r\n\r\n/// <summary>How a mapped role's rotation is transferred by the <see cref=\"GeometricSolver\"/>.</summary>\r\npublic enum RoleTransferMode\r\n{\r\n /// <summary>\r\n /// Absolute canonical-orientation matching: the target's animated chain direction is\r\n /// driven to <b>equal</b> the source's (in character-frame coordinates). Right for limbs\r\n /// and the spine \u2014 the pose IS the direction \u2014 but it also imposes the source rig's rest\r\n /// proportions/posture on roles whose rest directions legitimately differ between rigs.\r\n /// </summary>\r\n AbsoluteDirection,\r\n\r\n /// <summary>\r\n /// Rest-relative delta: the source's canonical-space rotation <i>delta from its own\r\n /// normalized rest</i> is replayed onto the <b>target's</b> normalized rest\r\n /// (<c>W_t(f) = C_t\u00b7\u0394C(f)\u00b7C_t\u207b\u00b9\u00b7R_tgtNormRest</c> with\r\n /// <c>\u0394C(f) = C_s\u207b\u00b9\u00b7\u0394R(f)\u00b7C_s</c>). The target keeps its own rest carriage (shoulder\r\n /// line height, neck-base angle) and moves with the source. Identical to\r\n /// <see cref=\"AbsoluteDirection\"/> when source and target rigs coincide.\r\n /// </summary>\r\n DeltaFromRest,\r\n\r\n /// <summary>\r\n /// Character-space delta: the source's world-rotation delta from its normalized rest is\r\n /// re-expressed in character coordinates and applied to the <b>target's</b> normalized\r\n /// rest (<c>W_t(f) = M\u00b7\u0394R(f)\u00b7M\u207b\u00b9\u00b7R_tgtNormRest</c> with <c>M = Q_tgt\u00b7Q_src\u207b\u00b9</c>, the\r\n /// same character basis change <see cref=\"AbsoluteDirection\"/> premultiplies). Like\r\n /// <see cref=\"DeltaFromRest\"/> the target keeps its own rest carriage, but the delta\r\n /// keeps its <i>world</i> rotation axes instead of being remapped through the per-role\r\n /// canonical frames \u2014 the faithful replay when the rigs' rest chain directions diverge\r\n /// so far that canonical-axis remapping would tilt every rotation axis by that\r\n /// divergence (measured 23\u201344\u00b0 on feet: CMU/ARP ankle anatomy vs the s&box rig's\r\n /// steep ankle, where canonical remapping mis-pitched planted feet by up to 47\u00b0).\r\n /// Identical to the other modes when source and target rigs coincide.\r\n /// </summary>\r\n CharacterDeltaFromRest,\r\n}\r\n\r\n/// <summary>Options controlling a single retarget solve (one clip \u2192 one output clip).</summary>\r\npublic sealed class SolveOptions\r\n{\r\n /// <summary>\r\n /// Default per-role transfer modes: shoulder girdle and neck carriage are\r\n /// <see cref=\"RoleTransferMode.DeltaFromRest\"/> (each rig's clavicle line / neck-base\r\n /// direction is rig anatomy, not pose \u2014 absolute matching was measured to drag the\r\n /// s&box shoulders 6\u201328\u00b0 toward the source's flatter/lower clavicle line and is the\r\n /// \"low shoulders, hunched neck\" artifact), and feet are\r\n /// <see cref=\"RoleTransferMode.CharacterDeltaFromRest\"/> (a rest foot\u2192toe direction is\r\n /// ankle anatomy too \u2014 rigs diverge 11\u201344\u00b0 from the s&box rig's steep ankle, so\r\n /// absolute matching pitched planted feet up to 25\u00b0 off flat, the \"feet bent\r\n /// upward/inward\" artifact; the character-space delta keeps the rotation's world axes,\r\n /// which canonical-frame remapping would tilt by that same divergence). The head is\r\n /// <see cref=\"RoleTransferMode.CharacterDeltaFromRest\"/> for the same reason: the rest\r\n /// neck\u2192head direction is head-joint-placement anatomy (measured 0\u201327\u00b0 forward lean\r\n /// across neutral-rest rigs vs the s&box rig's 25.5\u00b0), so the target keeps its own\r\n /// neutral skull attitude and replays the source's attitude <i>changes</i> \u2014 for the\r\n /// head this computes exactly what the previous virtual-frame absolute matching did.\r\n /// Two solver fallbacks adjust these defaults per rig pair: on a toe-less source the\r\n /// foot entries become <see cref=\"RoleTransferMode.DeltaFromRest\"/> (virtual-foot\r\n /// fallback), and a source whose normalized rest head attitude is implausible as a\r\n /// neutral carriage (a posed bind \u2014 e.g. a chin-down/tilted fighting-stance rest,\r\n /// measured 40.7\u00b0 forward / 16.9\u00b0 lateral on such a rig where the delta replay read\r\n /// ~12\u00b0 \"looking up at an angle\") switches the head to\r\n /// <see cref=\"RoleTransferMode.AbsoluteDirection\"/> so the gaze follows the source\r\n /// absolutely instead of replaying deltas from a posed reference (see the\r\n /// <see cref=\"GeometricSolver\"/> remarks for both). Everything else (limbs, spine,\r\n /// toes, fingers) stays absolute: there the worldspace direction IS the pose.\r\n /// </summary>\r\n public static IReadOnlyDictionary<BoneRole, RoleTransferMode> DefaultTransferModes { get; } =\r\n new Dictionary<BoneRole, RoleTransferMode>\r\n {\r\n [BoneRole.ClavicleL] = RoleTransferMode.DeltaFromRest,\r\n [BoneRole.ClavicleR] = RoleTransferMode.DeltaFromRest,\r\n [BoneRole.Neck] = RoleTransferMode.DeltaFromRest,\r\n [BoneRole.Head] = RoleTransferMode.CharacterDeltaFromRest,\r\n [BoneRole.FootL] = RoleTransferMode.CharacterDeltaFromRest,\r\n [BoneRole.FootR] = RoleTransferMode.CharacterDeltaFromRest,\r\n };\r\n\r\n /// <summary>\r\n /// Per-role transfer modes. Null (default) = <see cref=\"DefaultTransferModes\"/> plus the\r\n /// solver's fallback heuristics (a toe-less source's virtual foot direction overrides\r\n /// the foot default to <see cref=\"RoleTransferMode.DeltaFromRest\"/>, and a posed-rest\r\n /// source head overrides the head default to\r\n /// <see cref=\"RoleTransferMode.AbsoluteDirection\"/> \u2014 see the\r\n /// <see cref=\"GeometricSolver\"/> remarks). A non-null map REPLACES the defaults entirely\r\n /// and disables every fallback heuristic: each role uses exactly the mode in the map, and\r\n /// roles absent from it are <see cref=\"RoleTransferMode.AbsoluteDirection\"/>. Pass an\r\n /// empty dictionary for fully absolute (legacy) behavior \u2014 API callers supplying a map\r\n /// opt out of all heuristics.\r\n /// </summary>\r\n public IReadOnlyDictionary<BoneRole, RoleTransferMode>? TransferModes { get; init; }\r\n\r\n /// <summary>\r\n /// Scale applied to the pelvis translation components perpendicular to the character up\r\n /// direction. Null (default) = automatic: target hip height / source hip height, both\r\n /// measured on the normalized rests.\r\n /// </summary>\r\n public float? HipScaleHorizontal { get; init; }\r\n\r\n /// <summary>\r\n /// Scale applied to the pelvis translation component along the character up direction.\r\n /// Null (default) = the same automatic hip-height ratio as <see cref=\"HipScaleHorizontal\"/>.\r\n /// </summary>\r\n public float? HipScaleVertical { get; init; }\r\n\r\n /// <summary>Whether finger roles are transferred; when false, target finger bones keep\r\n /// their rest locals.</summary>\r\n public bool TransferFingers { get; init; } = true;\r\n\r\n /// <summary>Output clip name; null = the source clip's name.</summary>\r\n public string? ClipName { get; init; }\r\n\r\n /// <summary>Index of the source clip to retarget (<c>SourceScene.Clips</c>).</summary>\r\n public int ClipIndex { get; init; }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_retargeter",
"Path": "HumanoidRetargeter/Cleanup/FootGroundAlign.cs",
"FileName": "FootGroundAlign.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 311783,
"Code": "#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Numerics;\r\nusing HumanoidRetargeter.Maths;\r\nusing SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;\r\n\r\nnamespace HumanoidRetargeter.Cleanup;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)\r\n\r\n/// <summary>Tunables for the grounded-foot stance recalibration pass.</summary>\r\npublic sealed class FootGroundAlignOptions\r\n{\r\n /// <summary>\r\n /// Dead zone (degrees): measured stance offsets at or below this are genuine planted\r\n /// articulation (heel-roll bias, natural lean \u2014 measured 2\u20134\u00b0 on well-rested rigs and\r\n /// on citizen clips) and are left untouched, keeping the transfer byte-faithful there.\r\n /// Only offsets beyond it are clearly rest-pose artifacts (measured 12\u201325\u00b0 on the\r\n /// repro rig) and get recalibrated.\r\n /// </summary>\r\n public float MinCorrectionDeg { get; set; } = 8f;\r\n\r\n /// <summary>\r\n /// Maximum mean sole deviation (degrees) a plant may show and still count as a STANCE\r\n /// for the offset measurement. Plants beyond this are not standing on the sole (crawls,\r\n /// kneels, prone contact \u2014 measured 60\u201390\u00b0 there) and are excluded; genuine rest-pose\r\n /// stance artifacts measure well below it (largest seen: 27\u00b0).\r\n /// </summary>\r\n public float MaxStanceDeviationDeg { get; set; } = 35f;\r\n}\r\n\r\n/// <summary>Per-foot results of a <see cref=\"FootGroundAlign.Apply\"/> run.</summary>\r\npublic sealed class FootGroundAlignFootReport\r\n{\r\n /// <summary>Plants that contributed to the stance measurement.</summary>\r\n public int StancePlants { get; set; }\r\n\r\n /// <summary>Plants excluded as non-stance (mean sole deviation beyond\r\n /// <see cref=\"FootGroundAlignOptions.MaxStanceDeviationDeg\"/>).</summary>\r\n public int SkippedPlants { get; set; }\r\n\r\n /// <summary>Measured planted sole offset from the ground plane, degrees (0 when no\r\n /// stance plants exist).</summary>\r\n public float MeasuredOffsetDeg { get; set; }\r\n\r\n /// <summary>Foot correction applied to every frame, degrees (0 = inside the dead zone,\r\n /// nothing changed).</summary>\r\n public float AppliedFootDeg { get; set; }\r\n\r\n /// <summary>Toe correction applied to every frame, degrees.</summary>\r\n public float AppliedToeDeg { get; set; }\r\n}\r\n\r\n/// <summary>Results of a <see cref=\"FootGroundAlign.Apply\"/> run.</summary>\r\npublic sealed class FootGroundAlignReport\r\n{\r\n /// <summary>Left-foot results.</summary>\r\n public required FootGroundAlignFootReport Left { get; init; }\r\n\r\n /// <summary>Right-foot results.</summary>\r\n public required FootGroundAlignFootReport Right { get; init; }\r\n}\r\n\r\n/// <summary>\r\n/// Grounded-foot stance recalibration: measures how far the foot's SOLE sits from the ground\r\n/// plane while planted, and \u2014 when that offset is clearly a rest-pose artifact \u2014 rotates it\r\n/// out with one constant per foot, applied to every frame of the clip.\r\n/// </summary>\r\n/// <remarks>\r\n/// <para><b>Why a cleanup pass.</b> The solver transfers feet as rest-relative deltas\r\n/// (<see cref=\"Solve.RoleTransferMode.CharacterDeltaFromRest\"/>), so the target keeps its own\r\n/// ankle anatomy \u2014 correct whenever the source's rest pose is a flat-footed stance (the delta\r\n/// is then \"deviation from standing\"). Some rigs ship a NON-stance rest (measured: an\r\n/// Auto-Rig-Pro export whose rest foot sits 12\u201325\u00b0 from its planted stance), and that constant\r\n/// offset rides into every frame of the replay \u2014 planted feet hover toe-down/heel-up. What a\r\n/// stance actually looks like is animation evidence (planted phases), which a per-frame\r\n/// solver cannot see, so the recalibration lives here.</para>\r\n/// <para><b>Measurement.</b> Per foot: over every planted frame, the sole normal = rest up\r\n/// carried by the foot's world delta from the target bind rest (whose feet stand on the\r\n/// ground by construction); plants whose own mean normal sits beyond\r\n/// <see cref=\"FootGroundAlignOptions.MaxStanceDeviationDeg\"/> are excluded (crawl/kneel/prone\r\n/// contact is not a stance). The pooled mean normal's deviation from up is the stance\r\n/// offset.</para>\r\n/// <para><b>Correction.</b> Offsets inside <see cref=\"FootGroundAlignOptions.MinCorrectionDeg\"/>\r\n/// are genuine articulation \u2014 nothing is changed (well-rested rigs and same-rig round trips\r\n/// stay byte-identical through this pass). Beyond it, the shortest-arc rotation taking the\r\n/// pooled normal back to up (pitch+roll only \u2014 yaw/toe-out is pose and follows the source)\r\n/// premultiplies the foot's world rotation on EVERY frame: a rest artifact is constant, so\r\n/// the fix is too \u2014 within-plant heel-roll, swing styling and frame-to-frame continuity are\r\n/// preserved exactly, and no blending is needed. The toe then receives its own residual\r\n/// constant measured on top of the corrected foot (it neither double-rotates with the foot\r\n/// fix nor inherits the source toe's own rest artifact). Corrections rotate bones about\r\n/// their own joints: ankle positions are untouched, so the pass composes freely with the\r\n/// <see cref=\"FootPlant\"/> position pinning (which preserves foot world rotations).</para>\r\n/// <para><b>Plant intervals come from the caller</b> (the pipeline detects them on the\r\n/// SOURCE clip via <see cref=\"FootPlant.DetectPlantIntervals\"/> \u2014 ground truth, immune to\r\n/// the hip-height rescaling that can push target-side trajectories outside the cm-tuned\r\n/// Kovar thresholds). So does the decision to run at all: the pipeline invokes this pass\r\n/// only when the source's normalized rest is implausible as a flat stance (toe at/above\r\n/// ankle level or asymmetric feet \u2014 see <c>Retargeter.GroundAlignFeet</c>); on plausible\r\n/// stance rests the solver's rest-relative transfer is already faithful and planted-sole\r\n/// deviations are genuine articulation (boxing stances, heel rolls) that must not be\r\n/// flattened.</para>\r\n/// </remarks>\r\npublic static class FootGroundAlign\r\n{\r\n /// <summary>Measures planted stance offsets and recalibrates feet whose offset is a\r\n /// rest-pose artifact; returns what was measured and done.</summary>\r\n /// <param name=\"frames\">Per-frame local transforms (skeleton bone order); modified in place.</param>\r\n /// <param name=\"skeleton\">Bone hierarchy the frames are expressed against; its bind rest\r\n /// is the flat-stance reference.</param>\r\n /// <param name=\"left\">Left leg chain bone indices.</param>\r\n /// <param name=\"right\">Right leg chain bone indices.</param>\r\n /// <param name=\"up\">World up direction of the clip's space.</param>\r\n /// <param name=\"leftPlants\">Left-foot plant intervals (frame indices into\r\n /// <paramref name=\"frames\"/>; out-of-range parts are clamped/ignored).</param>\r\n /// <param name=\"rightPlants\">Right-foot plant intervals.</param>\r\n /// <param name=\"options\">Tunables; defaults used when null.</param>\r\n public static FootGroundAlignReport Apply(\r\n List<XForm[]> frames,\r\n SkeletonModel skeleton,\r\n FootChain left,\r\n FootChain right,\r\n Vector3 up,\r\n IReadOnlyList<FrameRange> leftPlants,\r\n IReadOnlyList<FrameRange> rightPlants,\r\n FootGroundAlignOptions? options = null)\r\n {\r\n ArgumentNullException.ThrowIfNull(frames);\r\n ArgumentNullException.ThrowIfNull(skeleton);\r\n ArgumentNullException.ThrowIfNull(left);\r\n ArgumentNullException.ThrowIfNull(right);\r\n ArgumentNullException.ThrowIfNull(leftPlants);\r\n ArgumentNullException.ThrowIfNull(rightPlants);\r\n\r\n options ??= new FootGroundAlignOptions();\r\n var report = new FootGroundAlignReport\r\n {\r\n Left = new FootGroundAlignFootReport(),\r\n Right = new FootGroundAlignFootReport(),\r\n };\r\n if (frames.Count == 0 || up.LengthSquared() < 1e-12f)\r\n return report;\r\n up = Vector3.Normalize(up);\r\n\r\n RecalibrateFoot(frames, skeleton, left, up, leftPlants, options, report.Left);\r\n RecalibrateFoot(frames, skeleton, right, up, rightPlants, options, report.Right);\r\n return report;\r\n }\r\n\r\n private static void RecalibrateFoot(\r\n List<XForm[]> frames, SkeletonModel skeleton, FootChain chain, Vector3 up,\r\n IReadOnlyList<FrameRange> plants, FootGroundAlignOptions options,\r\n FootGroundAlignFootReport report)\r\n {\r\n int n = frames.Count;\r\n var foot = chain.Ankle;\r\n var restFootRotInv = Quaternion.Conjugate(skeleton.RestWorld[foot].Rot);\r\n var maxStanceCos = MathF.Cos(options.MaxStanceDeviationDeg * MathF.PI / 180f);\r\n\r\n // ---- measurement: pooled planted sole normal over the stance plants ----\r\n var pooled = Vector3.Zero;\r\n foreach (var plant in plants)\r\n {\r\n int start = Math.Max(plant.Start, 0);\r\n int end = Math.Min(plant.End, n - 1);\r\n if (start > end)\r\n continue;\r\n\r\n var plantSum = Vector3.Zero;\r\n for (int f = start; f <= end; f++)\r\n {\r\n var footRot = FkUtil.BoneWorld(frames[f], skeleton, foot).Rot;\r\n plantSum += Vector3.Transform(up, MathQ.Normalize(footRot * restFootRotInv));\r\n }\r\n if (plantSum.LengthSquared() < 1e-8f\r\n || Vector3.Dot(Vector3.Normalize(plantSum), up) < maxStanceCos)\r\n {\r\n report.SkippedPlants++; // not standing on the sole \u2014 crawl/kneel/toe contact\r\n continue;\r\n }\r\n report.StancePlants++;\r\n pooled += plantSum; // frame-count-weighted: longer stances dominate\r\n }\r\n if (pooled.LengthSquared() < 1e-8f)\r\n return;\r\n pooled = Vector3.Normalize(pooled);\r\n\r\n var offsetDeg = MathQ.AngleBetween(pooled, up) * (180f / MathF.PI);\r\n report.MeasuredOffsetDeg = offsetDeg;\r\n if (offsetDeg <= options.MinCorrectionDeg)\r\n return; // genuine planted articulation \u2014 leave the transfer byte-faithful\r\n\r\n // ---- correction: one constant per foot, every frame ----\r\n var footFix = MathQ.FromTo(pooled, up);\r\n report.AppliedFootDeg = offsetDeg;\r\n\r\n // Toe residual measured on top of the corrected foot, same dead zone.\r\n var toeFix = Quaternion.Identity;\r\n if (chain.Toe is { } toe && skeleton[toe].ParentIndex == foot)\r\n {\r\n var restToeRotInv = Quaternion.Conjugate(skeleton.RestWorld[toe].Rot);\r\n var toePooled = Vector3.Zero;\r\n foreach (var plant in plants)\r\n {\r\n int start = Math.Max(plant.Start, 0);\r\n int end = Math.Min(plant.End, n - 1);\r\n for (int f = start; f <= end && f >= 0; f++)\r\n {\r\n var toeRot = FkUtil.BoneWorld(frames[f], skeleton, toe).Rot;\r\n toePooled += Vector3.Transform(\r\n up, MathQ.Normalize(footFix * toeRot * restToeRotInv));\r\n }\r\n }\r\n if (toePooled.LengthSquared() > 1e-8f)\r\n {\r\n toePooled = Vector3.Normalize(toePooled);\r\n var toeDeg = MathQ.AngleBetween(toePooled, up) * (180f / MathF.PI);\r\n if (toeDeg > options.MinCorrectionDeg && Vector3.Dot(toePooled, up) >= maxStanceCos)\r\n {\r\n toeFix = MathQ.FromTo(toePooled, up);\r\n report.AppliedToeDeg = toeDeg;\r\n }\r\n }\r\n }\r\n\r\n for (int f = 0; f < n; f++)\r\n CorrectFrame(frames[f], skeleton, chain, footFix, toeFix);\r\n }\r\n\r\n /// <summary>Premultiplies the foot's world rotation by the constant fix (the joint\r\n /// position is untouched \u2014 the rotation pivots the foot about its own head), then gives\r\n /// the toe its own residual on top of the corrected foot.</summary>\r\n private static void CorrectFrame(\r\n XForm[] locals, SkeletonModel skeleton, FootChain chain,\r\n Quaternion footFix, Quaternion toeFix)\r\n {\r\n var foot = chain.Ankle;\r\n var parent = skeleton[foot].ParentIndex;\r\n var parentRot = parent < 0\r\n ? Quaternion.Identity\r\n : FkUtil.BoneWorld(locals, skeleton, parent).Rot;\r\n\r\n var footWorld = MathQ.Normalize(parentRot * locals[foot].Rot);\r\n var newFootWorld = MathQ.Normalize(footFix * footWorld);\r\n locals[foot] = new XForm(\r\n locals[foot].Pos, MathQ.Normalize(Quaternion.Conjugate(parentRot) * newFootWorld));\r\n\r\n if (chain.Toe is { } toe && skeleton[toe].ParentIndex == foot)\r\n {\r\n // Desired toe world = toeFix \u2218 footFix \u2218 original world; re-derive its local\r\n // against the corrected foot so it does not double-rotate with the foot fix.\r\n var toeWorldOld = MathQ.Normalize(footWorld * locals[toe].Rot);\r\n var desired = MathQ.Normalize(toeFix * footFix * toeWorldOld);\r\n locals[toe] = new XForm(\r\n locals[toe].Pos, MathQ.Normalize(Quaternion.Conjugate(newFootWorld) * desired));\r\n }\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_retargeter",
"Path": "HumanoidRetargeter/Formats/Bvh/BvhImporter.cs",
"FileName": "BvhImporter.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 311783,
"Code": "#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing System.Numerics;\r\nusing System.Text;\r\nusing HumanoidRetargeter.Maths;\r\nusing HumanoidRetargeter.Skeleton;\r\n\r\nnamespace HumanoidRetargeter.Formats.Bvh;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)\r\n\r\n/// <summary>Options for <see cref=\"BvhImporter.Import\"/>.</summary>\r\npublic sealed class BvhImportOptions\r\n{\r\n /// <summary>Fixed resampling rate for the motion data, frames per second.</summary>\r\n public float SampleFps { get; init; } = 30f;\r\n}\r\n\r\n/// <summary>\r\n/// BVH (Biovision Hierarchy) \u2192 <see cref=\"SourceScene\"/> importer.\r\n/// </summary>\r\n/// <remarks>\r\n/// <para><b>Format conventions implemented</b> (verified against Blender's\r\n/// <c>io_anim_bvh</c> importer, which is the project's ground-truth extractor):</para>\r\n/// <list type=\"bullet\">\r\n/// <item><b>Rest pose:</b> each joint's rest local translation is its <c>OFFSET</c>; rest\r\n/// rotation is identity (BVH stores no rest orientation).</item>\r\n/// <item><b>Rotation channels:</b> the channel list order IS the rotation order. The listed\r\n/// rotations apply left-to-right as intrinsic rotations, which in this library's\r\n/// column-vector convention (<c>a * b</c> applies <c>b</c> first) is the product\r\n/// <c>R = R_chan1 * R_chan2 * R_chan3</c> \u2014 e.g. <c>Zrotation Yrotation Xrotation</c> gives\r\n/// <c>R = Rz * Ry * Rx</c>. This matches Blender, which builds\r\n/// <c>Euler((x,y,z), reversed(channelOrder))</c> for the same matrix. Angles are degrees.</item>\r\n/// <item><b>Position channels:</b> when a joint has any position channel, the channel values\r\n/// REPLACE the joint's local translation (missing components are 0) \u2014 they are not added to\r\n/// the <c>OFFSET</c>. This is Blender's behavior; in practice roots have OFFSET 0 so the two\r\n/// readings only diverge on non-root position channels (e.g. Bandai-Namco exports).</item>\r\n/// <item><b>End Sites:</b> synthesized as a channel-less leaf bone named\r\n/// <c>\"<parent>_end\"</c> so chain tips keep their direction information (Blender instead\r\n/// folds them into the parent bone's tail).</item>\r\n/// </list>\r\n/// <para><b>Units</b>: BVH files carry no unit declaration. Heuristic: compute the rest\r\n/// skeleton height (max\u2212min world Y over all joints); if it is < 10 the file is assumed\r\n/// to be in meters and all translations (offsets AND position channels, root included) are\r\n/// scaled \u00d7100 to centimeters, otherwise it is assumed to already be centimeters (\u00d71).\r\n/// Millimeter-scale files (height > 400) are not special-cased \u2014 they are rare and\r\n/// ambiguous against cm mocap of long ranges; <see cref=\"SourceScene.UnitScaleCm\"/> records\r\n/// whichever factor was applied for diagnostics.</para>\r\n/// <para><b>Calibration rest-frame trim</b>: mocap exports (CMU asf/amc conversions among\r\n/// them) often prepend a skeleton-calibration segment \u2014 the rest pose itself (all rotation\r\n/// channels \u2248 0), hard-cut (or blend-ramped over 2\u20133 frames) into the real motion. Played\r\n/// back it reads as a T-pose flash at t = 0. The importer drops such a segment from either\r\n/// clip end when ALL of (measured margins in parentheses, over the corpus + repro files):\r\n/// every segment frame is rest-like (max joint rotation vs the identity rest \u2264 40\u00b0;\r\n/// calibration frames/ramps measure \u2264 24\u00b0, real clip edges \u2265 80\u00b0); the segment is short\r\n/// (\u2264 4 rest-like frames \u2014 hard cuts measure 1, blend ramps 2; longer rest-like leads are\r\n/// content); the clip beyond it is NOT rest-like; and the discontinuity where the segment\r\n/// exits into the motion is both large in absolute terms (\u2265 20\u00b0; measured 25\u2013177\u00b0) and\r\n/// large versus the clip's own typical inter-frame delta (\u2265 4\u00d7 the median; real clip edges\r\n/// measure \u2264 8\u00b0 at \u2264 ~1\u00d7 the median). A qualifying segment that exits through a multi-frame\r\n/// blend RAMP (measured 22\u201325\u00b0/frame for 3 frames on a makehuman-retarget export) has the\r\n/// ramp trimmed too, until the motion settles \u2014 8 frames total per end at most. A clip that\r\n/// legitimately starts near rest (an idle) is continuous into the motion and never trips\r\n/// the discontinuity gates. Note the trim can never remove\r\n/// the reference frame a non-anatomical stick bind needs for its rest rebuild (see\r\n/// <c>RestNormalizer</c>): it only removes frames that MATCH the identity-rotation bind,\r\n/// and a frame matching a stick bind carries no rest information the bind itself lacks \u2014\r\n/// the next (real) frame is then strictly the better reference.</para>\r\n/// <para><b>Resampling</b>: motion frames are resampled from the file's <c>Frame Time</c>\r\n/// grid onto <see cref=\"BvhImportOptions.SampleFps\"/>. Each native frame's euler channels are\r\n/// converted to a quaternion FIRST and bracketing frames are then slerped (positions lerped).\r\n/// Interpolating raw euler angles across frames would mostly work at mocap densities\r\n/// (30\u2013120 fps, small per-frame deltas) but breaks down when an angle wraps \u00b1180\u00b0 between\r\n/// frames; per-frame quaternion + slerp has no such failure mode, so that is what we do.</para>\r\n/// <para><b>Axes</b>: BVH is conventionally Y-up / Z-forward / X-right. Native axes are\r\n/// preserved (no conversion), matching the FBX importer's policy; the conventional axes are\r\n/// recorded on the <see cref=\"SourceScene\"/> (up = Y, front = Z, coord = X).</para>\r\n/// </remarks>\r\npublic static class BvhImporter\r\n{\r\n private const float MeterHeightThreshold = 10f;\r\n\r\n /// <summary>Parses BVH bytes and builds the source scene.</summary>\r\n /// <exception cref=\"FormatException\">Malformed or truncated BVH.</exception>\r\n public static SourceScene Import(byte[] data, BvhImportOptions? options = null)\r\n {\r\n ArgumentNullException.ThrowIfNull(data);\r\n options ??= new BvhImportOptions();\r\n if (!(options.SampleFps > 0f) || !float.IsFinite(options.SampleFps))\r\n throw new ArgumentOutOfRangeException(nameof(options), \"SampleFps must be positive.\");\r\n\r\n var cursor = new TokenCursor(Encoding.UTF8.GetString(data));\r\n\r\n // ---- HIERARCHY -----------------------------------------------------------------\r\n cursor.ExpectKeyword(\"HIERARCHY\");\r\n var joints = new List<Joint>();\r\n int channelCount = 0;\r\n if (!cursor.PeekIs(\"ROOT\"))\r\n throw new FormatException(\"BVH: expected ROOT after HIERARCHY.\");\r\n while (cursor.PeekIs(\"ROOT\")) // multiple roots are out of spec but harmless to accept\r\n {\r\n cursor.Next();\r\n ParseJoint(cursor, joints, parent: -1, ref channelCount);\r\n }\r\n\r\n // ---- MOTION ---------------------------------------------------------------------\r\n cursor.ExpectKeyword(\"MOTION\");\r\n cursor.ExpectKeyword(\"FRAMES:\");\r\n int frameCount = cursor.NextInt();\r\n if (frameCount < 0)\r\n throw new FormatException($\"BVH: negative frame count {frameCount}.\");\r\n cursor.ExpectKeyword(\"FRAME\");\r\n cursor.ExpectKeyword(\"TIME:\");\r\n float frameTime = cursor.NextFloat();\r\n if (!(frameTime > 0f) || !float.IsFinite(frameTime))\r\n throw new FormatException($\"BVH: invalid Frame Time {frameTime}.\");\r\n\r\n var motion = new float[frameCount][];\r\n for (int f = 0; f < frameCount; f++)\r\n {\r\n var row = new float[channelCount];\r\n for (int c = 0; c < channelCount; c++)\r\n row[c] = cursor.NextFloat();\r\n motion[f] = row;\r\n }\r\n\r\n // ---- units heuristic --------------------------------------------------------------\r\n float unitScale = HeuristicUnitScale(joints);\r\n\r\n // ---- skeleton ----------------------------------------------------------------------\r\n var defs = new List<BoneDefinition>(joints.Count);\r\n foreach (var j in joints)\r\n {\r\n defs.Add(new BoneDefinition(\r\n j.Name,\r\n j.Parent < 0 ? null : joints[j.Parent].Name,\r\n new XForm(j.Offset * unitScale, Quaternion.Identity)));\r\n }\r\n var skeleton = Skeleton.Skeleton.Create(defs);\r\n\r\n // ---- clip ----------------------------------------------------------------------------\r\n var clips = new List<Clip>();\r\n if (frameCount > 0)\r\n clips.Add(ResampleClip(joints, skeleton, motion, frameTime, unitScale, options.SampleFps));\r\n\r\n // BVH conventional axes: Y-up (1), Z-front (2), X-coord (0) \u2014 recorded, not converted.\r\n // RestPlacementAuthored = false: the BVH rest skeleton is OFFSETs only (root at the\r\n // file origin, no authored world placement), while MOTION root positions live in\r\n // absolute capture-volume coordinates \u2014 the two share no common ground/origin, so\r\n // the solver must normalize clip placement against the rest skeleton\r\n // (see SourceScene.RestPlacementAuthored and GeometricSolver remarks).\r\n return new SourceScene(\r\n skeleton, clips, unitScale,\r\n upAxis: 1, upAxisSign: 1,\r\n frontAxis: 2, frontAxisSign: 1,\r\n coordAxis: 0, coordAxisSign: 1,\r\n originalUpAxis: -1)\r\n {\r\n RestPlacementAuthored = false,\r\n };\r\n }\r\n\r\n // =====================================================================================\r\n // hierarchy parsing\r\n // =====================================================================================\r\n\r\n private sealed class Joint\r\n {\r\n public required string Name;\r\n public required int Parent; // index into the joint list, -1 for roots\r\n public Vector3 Offset; // raw file units\r\n public int PosX = -1, PosY = -1, PosZ = -1; // motion column per position axis\r\n public List<(int Axis, int Column)> Rot = new(); // rotation channels in file order\r\n public bool HasPos => PosX >= 0 || PosY >= 0 || PosZ >= 0;\r\n }\r\n\r\n private static void ParseJoint(TokenCursor cursor, List<Joint> joints, int parent, ref int channelCount)\r\n {\r\n // Joint name: tokens up to '{', joined with '_' (mirrors Blender's handling of\r\n // names containing spaces).\r\n var nameParts = new List<string>();\r\n while (!cursor.PeekIs(\"{\"))\r\n {\r\n if (cursor.AtEnd)\r\n throw new FormatException(\"BVH: unexpected end of file in joint name.\");\r\n nameParts.Add(cursor.Next());\r\n }\r\n if (nameParts.Count == 0)\r\n throw new FormatException(\"BVH: joint with no name.\");\r\n string name = UniqueName(string.Join('_', nameParts), joints);\r\n\r\n cursor.ExpectKeyword(\"{\");\r\n cursor.ExpectKeyword(\"OFFSET\");\r\n var joint = new Joint { Name = name, Parent = parent };\r\n joint.Offset = new Vector3(cursor.NextFloat(), cursor.NextFloat(), cursor.NextFloat());\r\n int index = joints.Count;\r\n joints.Add(joint);\r\n\r\n if (cursor.PeekIs(\"CHANNELS\"))\r\n {\r\n cursor.Next();\r\n int n = cursor.NextInt();\r\n if (n < 0 || n > 6)\r\n throw new FormatException($\"BVH: joint '{name}' has invalid channel count {n}.\");\r\n for (int i = 0; i < n; i++)\r\n {\r\n string channel = cursor.Next();\r\n int column = channelCount++;\r\n switch (channel.ToUpperInvariant())\r\n {\r\n case \"XPOSITION\": joint.PosX = column; break;\r\n case \"YPOSITION\": joint.PosY = column; break;\r\n case \"ZPOSITION\": joint.PosZ = column; break;\r\n case \"XROTATION\": joint.Rot.Add((0, column)); break;\r\n case \"YROTATION\": joint.Rot.Add((1, column)); break;\r\n case \"ZROTATION\": joint.Rot.Add((2, column)); break;\r\n default:\r\n throw new FormatException($\"BVH: unknown channel '{channel}' on joint '{name}'.\");\r\n }\r\n }\r\n }\r\n\r\n while (!cursor.PeekIs(\"}\"))\r\n {\r\n if (cursor.AtEnd)\r\n throw new FormatException($\"BVH: unexpected end of file inside joint '{name}'.\");\r\n if (cursor.PeekIs(\"JOINT\"))\r\n {\r\n cursor.Next();\r\n ParseJoint(cursor, joints, index, ref channelCount);\r\n }\r\n else if (cursor.PeekIs(\"END\"))\r\n {\r\n cursor.Next();\r\n cursor.ExpectKeyword(\"SITE\");\r\n while (!cursor.PeekIs(\"{\")) // a name after \"End Site\" is out of spec; skip it\r\n {\r\n if (cursor.AtEnd)\r\n throw new FormatException(\"BVH: unexpected end of file in End Site.\");\r\n cursor.Next();\r\n }\r\n cursor.ExpectKeyword(\"{\");\r\n cursor.ExpectKeyword(\"OFFSET\");\r\n var endOffset = new Vector3(cursor.NextFloat(), cursor.NextFloat(), cursor.NextFloat());\r\n cursor.ExpectKeyword(\"}\");\r\n\r\n // Synthesize a channel-less leaf so the chain tip's direction is kept.\r\n joints.Add(new Joint\r\n {\r\n Name = UniqueName(name + \"_end\", joints),\r\n Parent = index,\r\n Offset = endOffset,\r\n });\r\n }\r\n else\r\n {\r\n throw new FormatException(\r\n $\"BVH: unexpected token '{cursor.Next()}' inside joint '{name}'.\");\r\n }\r\n }\r\n cursor.ExpectKeyword(\"}\");\r\n }\r\n\r\n private static string UniqueName(string name, List<Joint> joints)\r\n {\r\n bool Taken(string candidate)\r\n {\r\n foreach (var j in joints)\r\n if (string.Equals(j.Name, candidate, StringComparison.Ordinal))\r\n return true;\r\n return false;\r\n }\r\n\r\n if (!Taken(name))\r\n return name;\r\n for (int i = 1; ; i++)\r\n {\r\n string candidate = $\"{name}#{i}\";\r\n if (!Taken(candidate))\r\n return candidate;\r\n }\r\n }\r\n\r\n // =====================================================================================\r\n // units\r\n // =====================================================================================\r\n\r\n /// <summary>\r\n /// Meters-vs-centimeters heuristic: rest skeleton height (max\u2212min world Y over all\r\n /// joints, end sites included) < 10 \u2192 meters \u2192 \u00d7100; otherwise centimeters \u2192 \u00d71.\r\n /// </summary>\r\n private static float HeuristicUnitScale(List<Joint> joints)\r\n {\r\n Span<float> worldY = joints.Count <= 256 ? stackalloc float[joints.Count] : new float[joints.Count];\r\n float min = float.MaxValue, max = float.MinValue;\r\n for (int i = 0; i < joints.Count; i++)\r\n {\r\n worldY[i] = (joints[i].Parent < 0 ? 0f : worldY[joints[i].Parent]) + joints[i].Offset.Y;\r\n min = MathF.Min(min, worldY[i]);\r\n max = MathF.Max(max, worldY[i]);\r\n }\r\n float height = max - min;\r\n return height > 0f && height < MeterHeightThreshold ? 100f : 1f;\r\n }\r\n\r\n // =====================================================================================\r\n // motion sampling\r\n // =====================================================================================\r\n\r\n /// <summary>\r\n /// Decodes every native frame to per-joint local transforms (quaternions built per frame\r\n /// from the joint's channel order), drops leading/trailing calibration rest frames (see\r\n /// class remarks), then resamples onto the <paramref name=\"fps\"/> grid \u2014 positions\r\n /// lerped, rotations slerped between the bracketing native frames.\r\n /// </summary>\r\n private static Clip ResampleClip(\r\n List<Joint> joints, Skeleton.Skeleton skeleton, float[][] motion,\r\n float frameTime, float unitScale, float fps)\r\n {\r\n int jointCount = joints.Count;\r\n int nativeCount = motion.Length;\r\n\r\n // Joint order may differ from skeleton bone order (topological sort) \u2014 map.\r\n var toSkeleton = new int[jointCount];\r\n for (int i = 0; i < jointCount; i++)\r\n toSkeleton[i] = skeleton.IndexOf(joints[i].Name);\r\n\r\n // Native-frame locals.\r\n var native = new XForm[nativeCount][];\r\n for (int f = 0; f < nativeCount; f++)\r\n {\r\n var row = motion[f];\r\n var locals = new XForm[jointCount];\r\n for (int i = 0; i < jointCount; i++)\r\n locals[i] = EvaluateLocal(joints[i], row, unitScale);\r\n native[f] = locals;\r\n }\r\n\r\n // Calibration rest-frame trim: a short rest-like segment per clip end (class remarks).\r\n int first = 0;\r\n int last = nativeCount - 1;\r\n if (nativeCount >= 3)\r\n {\r\n float typicalDeltaDeg = TypicalNeighborRotDeltaDeg(native);\r\n first += CalibrationSegmentLength(native, first, last, step: +1, typicalDeltaDeg);\r\n last -= CalibrationSegmentLength(native, last, first, step: -1, typicalDeltaDeg);\r\n }\r\n int trimmedCount = last - first + 1;\r\n\r\n double duration = (trimmedCount - 1) * (double)frameTime;\r\n int outCount = Math.Max(1, (int)Math.Round(duration * fps) + 1);\r\n\r\n var frames = new List<XForm[]>(outCount);\r\n for (int f = 0; f < outCount; f++)\r\n {\r\n double s = f / (double)fps / frameTime; // position on the trimmed native frame grid\r\n int i0 = first + Math.Clamp((int)Math.Floor(s), 0, trimmedCount - 1);\r\n int i1 = Math.Min(i0 + 1, last);\r\n float u = Math.Clamp((float)(s - (i0 - first)), 0f, 1f);\r\n\r\n var frame = new XForm[skeleton.Count];\r\n var a = native[i0];\r\n var b = native[i1];\r\n for (int i = 0; i < jointCount; i++)\r\n {\r\n frame[toSkeleton[i]] = new XForm(\r\n Vector3.Lerp(a[i].Pos, b[i].Pos, u),\r\n MathQ.Normalize(Quaternion.Slerp(a[i].Rot, b[i].Rot, u)));\r\n }\r\n frames.Add(frame);\r\n }\r\n\r\n // NativeFps records the file's authored frame rate (1 / FrameTime): external frame\r\n // ranges (Unity .meta clipAnimations) are expressed in it.\r\n float nativeFps = frameTime > 0f ? (float)(1.0 / frameTime) : fps;\r\n return new Clip(\"motion\", fps, looping: false, frames, nativeFps);\r\n }\r\n\r\n // ---------------------------------------------------------------- calibration trim\r\n\r\n /// <summary>A frame counts as rest-like only below this max-joint rotation angle vs the\r\n /// identity-rotation bind (measured: calibration frames/ramps \u2264 24\u00b0, real edges \u2265 80\u00b0).</summary>\r\n private const float CalibrationRestMaxDeg = 40f;\r\n\r\n /// <summary>Absolute floor on the discontinuity out of the rest-like segment (measured:\r\n /// calibration exits 25\u2013177\u00b0, continuous real clip edges \u2264 8\u00b0).</summary>\r\n private const float CalibrationJumpMinDeg = 20f;\r\n\r\n /// <summary>The segment-exit discontinuity must also exceed this multiple of the clip's\r\n /// median inter-frame delta \u2014 a clip idling near rest never trips this.</summary>\r\n private const float CalibrationJumpTypicalRatio = 4f;\r\n\r\n /// <summary>Longest rest-like calibration segment trimmed per clip end. Hard cuts are\r\n /// 1 frame (CMU asf/amc exports); rest\u2192motion blend ramps measure 2 rest-like frames\r\n /// (a makehuman-retarget export). Longer rest-like leads are content, left alone.</summary>\r\n private const int CalibrationMaxSegmentFrames = 4;\r\n\r\n /// <summary>Absolute floor on a blend-ramp frame's delta for the ramp extension\r\n /// (measured ramp deltas 15\u201325\u00b0/frame; settled motion \u2264 6\u00b0).</summary>\r\n private const float CalibrationRampMinDeg = 10f;\r\n\r\n /// <summary>Hard cap on the total trim per clip end (rest-like segment + blend ramp;\r\n /// measured worst case 5 frames on the makehuman-retarget export).</summary>\r\n private const int CalibrationMaxTrimFrames = 8;\r\n\r\n /// <summary>\r\n /// Length of the prepended (<paramref name=\"step\"/> = +1, scanning from\r\n /// <paramref name=\"edge\"/> toward <paramref name=\"stop\"/>) or appended (\u22121)\r\n /// skeleton-calibration segment, 0 when there is none. The segment is a short run of\r\n /// rest-like frames (\u2264 <see cref=\"CalibrationMaxSegmentFrames\"/>) that exits into\r\n /// NON-rest-like motion (\u2265 2 frames of which must remain) through a discontinuity that\r\n /// is large both absolutely and against the clip's typical inter-frame delta. When the\r\n /// exit is a multi-frame blend RAMP rather than a hard cut (measured: 2 rest-like frames\r\n /// then 22\u201325\u00b0/frame for 3 more), the ramp frames are consumed too \u2014 until the motion\r\n /// settles to ordinary deltas \u2014 bounded by <see cref=\"CalibrationMaxTrimFrames\"/> total.\r\n /// See class remarks for the measured margins.\r\n /// </summary>\r\n private static int CalibrationSegmentLength(\r\n XForm[][] native, int edge, int stop, int step, float typicalDeltaDeg)\r\n {\r\n int length = 0;\r\n int f = edge;\r\n while (f != stop && length <= CalibrationMaxSegmentFrames\r\n && MaxRotDeltaDeg(native[f], null) <= CalibrationRestMaxDeg)\r\n {\r\n length++;\r\n f += step;\r\n }\r\n if (length is 0 or > CalibrationMaxSegmentFrames)\r\n return 0;\r\n\r\n // f = first frame past the rest-like segment; require a real (non-rest-like) clip\r\n // of \u2265 2 frames beyond it and a calibration-grade cut between segment and motion.\r\n if (f == stop || MaxRotDeltaDeg(native[f], null) <= CalibrationRestMaxDeg)\r\n return 0;\r\n float jump = MaxRotDeltaDeg(native[f - step], native[f]);\r\n if (jump < CalibrationJumpMinDeg || jump < CalibrationJumpTypicalRatio * typicalDeltaDeg)\r\n return 0;\r\n\r\n // Blend-ramp extension: consume frames still moving at calibration-ramp speed until\r\n // the motion settles (leaving \u2265 2 frames past the trim).\r\n float rampFloor = MathF.Max(\r\n CalibrationRampMinDeg, CalibrationJumpTypicalRatio * typicalDeltaDeg);\r\n while (length < CalibrationMaxTrimFrames\r\n && f != stop && f + step != stop\r\n && MaxRotDeltaDeg(native[f], native[f + step]) >= rampFloor)\r\n {\r\n length++;\r\n f += step;\r\n }\r\n return length;\r\n }\r\n\r\n /// <summary>Max joint rotation angle (degrees) between two decoded frames, or \u2014 when\r\n /// <paramref name=\"b\"/> is null \u2014 against the identity-rotation BVH bind rest.</summary>\r\n private static float MaxRotDeltaDeg(XForm[] a, XForm[]? b)\r\n {\r\n float max = 0f;\r\n for (int i = 0; i < a.Length; i++)\r\n {\r\n float angle = MathQ.AngleBetween(a[i].Rot, b is null ? Quaternion.Identity : b[i].Rot);\r\n max = MathF.Max(max, angle);\r\n }\r\n return max * (180f / MathF.PI);\r\n }\r\n\r\n /// <summary>Median of the per-pair max joint rotation deltas over the clip's INTERIOR\r\n /// consecutive frame pairs (both edge pairs excluded \u2014 they are the trim candidates).</summary>\r\n private static float TypicalNeighborRotDeltaDeg(XForm[][] native)\r\n {\r\n int pairCount = native.Length - 3; // pairs (1,2) \u2026 (n-3, n-2)\r\n if (pairCount <= 0)\r\n return 0f;\r\n var deltas = new float[pairCount];\r\n for (int f = 0; f < pairCount; f++)\r\n deltas[f] = MaxRotDeltaDeg(native[f + 1], native[f + 2]);\r\n Array.Sort(deltas);\r\n return deltas[pairCount / 2];\r\n }\r\n\r\n /// <summary>One joint's local transform from one motion row (see class remarks).</summary>\r\n private static XForm EvaluateLocal(Joint joint, float[] row, float unitScale)\r\n {\r\n // Position channels replace the OFFSET; absent channels (or no position channels at\r\n // all) fall back per Blender's semantics described in the class remarks.\r\n Vector3 pos = joint.HasPos\r\n ? new Vector3(\r\n joint.PosX >= 0 ? row[joint.PosX] : 0f,\r\n joint.PosY >= 0 ? row[joint.PosY] : 0f,\r\n joint.PosZ >= 0 ? row[joint.PosZ] : 0f)\r\n : joint.Offset;\r\n\r\n // R = R_chan1 * R_chan2 * R_chan3 (column-vector convention; degrees in the file).\r\n var rot = Quaternion.Identity;\r\n foreach (var (axis, column) in joint.Rot)\r\n {\r\n float radians = row[column] * (MathF.PI / 180f);\r\n var axisVector = axis switch\r\n {\r\n 0 => Vector3.UnitX,\r\n 1 => Vector3.UnitY,\r\n _ => Vector3.UnitZ,\r\n };\r\n rot *= Quaternion.CreateFromAxisAngle(axisVector, radians);\r\n }\r\n\r\n return new XForm(pos * unitScale, MathQ.Normalize(rot));\r\n }\r\n\r\n // =====================================================================================\r\n // tokenizer\r\n // =====================================================================================\r\n\r\n /// <summary>Whitespace token stream over the BVH text (BVH is line-format agnostic).</summary>\r\n private sealed class TokenCursor\r\n {\r\n private readonly string[] _tokens;\r\n private int _pos;\r\n\r\n public TokenCursor(string text)\r\n => _tokens = text.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);\r\n\r\n public bool AtEnd => _pos >= _tokens.Length;\r\n\r\n public bool PeekIs(string keywordUpper)\r\n => _pos < _tokens.Length &&\r\n string.Equals(_tokens[_pos], keywordUpper, StringComparison.OrdinalIgnoreCase);\r\n\r\n public string Next()\r\n {\r\n if (AtEnd)\r\n throw new FormatException(\"BVH: unexpected end of file.\");\r\n return _tokens[_pos++];\r\n }\r\n\r\n public void ExpectKeyword(string keywordUpper)\r\n {\r\n string token = Next();\r\n if (!string.Equals(token, keywordUpper, StringComparison.OrdinalIgnoreCase))\r\n throw new FormatException($\"BVH: expected '{keywordUpper}', found '{token}'.\");\r\n }\r\n\r\n public int NextInt()\r\n {\r\n string token = Next();\r\n if (!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value))\r\n throw new FormatException($\"BVH: expected an integer, found '{token}'.\");\r\n return value;\r\n }\r\n\r\n public float NextFloat()\r\n {\r\n string token = Next();\r\n if (!float.TryParse(token, NumberStyles.Float, CultureInfo.InvariantCulture, out float value) ||\r\n !float.IsFinite(value))\r\n throw new FormatException($\"BVH: expected a number, found '{token}'.\");\r\n return value;\r\n }\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_retargeter",
"Path": "HumanoidRetargeter/Formats/Dmx/DmxWriter.cs",
"FileName": "DmxWriter.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 311783,
"Code": "#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing System.Security.Cryptography;\r\nusing System.Text;\r\nusing HumanoidRetargeter.Skeleton;\r\nusing SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;\r\n\r\nnamespace HumanoidRetargeter.Formats.Dmx;\r\n\r\n/// <summary>Options for <see cref=\"DmxWriter.Write\"/>.</summary>\r\npublic sealed class DmxWriteOptions\r\n{\r\n /// <summary>Model/clip name written into the DmeModel element (e.g. the sequence name).</summary>\r\n public string Name { get; set; } = \"\";\r\n\r\n /// <summary>Free-form provenance note written as the DmeDCCMakefile source name\r\n /// (fbx2dmx writes the source .fbx path here).</summary>\r\n public string SourceNote { get; set; } = \"\";\r\n\r\n /// <summary>When true (default, matching fbx2dmx output) the file declares a Y-up axis\r\n /// system; when false it declares Z-up. Data is written as-is either way.</summary>\r\n public bool UpAxisY { get; set; } = true;\r\n\r\n /// <summary>\r\n /// Skeleton bone indices that get NO DmeChannel pair: the bones keep their DmeJoint and\r\n /// bind (rest) transform, but no animation channels are written for them \u2014 the engine then\r\n /// drives them itself (e.g. ConstraintDriven twist/helper bones, design \u00a73). Null (default)\r\n /// writes channels for every bone.\r\n /// </summary>\r\n public IReadOnlySet<int>? ChannelExcludedBones { get; set; }\r\n}\r\n\r\n/// <summary>\r\n/// Writes an animation DMX in <c>keyvalues2_noids</c> text encoding, replicating the exact\r\n/// element/attribute shape of fbx2dmx output (authoritative reference:\r\n/// <c>dev/m0/ref_idlepose.dmx</c>): a root DmElement holding an inline DmeModel (joint GUID\r\n/// refs + bind base state), a top-level DmeAnimationList with one DmeChannelsClip carrying a\r\n/// position and an orientation channel per bone, and top-level DmeTransform/DmeJoint elements\r\n/// the channels and joint lists reference by GUID. Output is fully deterministic: GUIDs are\r\n/// MD5-derived from the options name and an element path, and export tags use fixed\r\n/// placeholder strings.\r\n/// </summary>\r\npublic static class DmxWriter\r\n{\r\n private const string Header = \"<!-- dmx encoding keyvalues2_noids 4 format model 22 -->\";\r\n\r\n /// <summary>\r\n /// Serializes <paramref name=\"clip\"/> on <paramref name=\"skeleton\"/> to DMX text.\r\n /// Frames must contain one local transform per bone in skeleton order.\r\n /// </summary>\r\n /// <exception cref=\"ArgumentException\">Thrown when the clip is empty or a frame's bone\r\n /// count does not match the skeleton.</exception>\r\n public static string Write(SkeletonModel skeleton, Clip clip, DmxWriteOptions options)\r\n {\r\n ArgumentNullException.ThrowIfNull(skeleton);\r\n ArgumentNullException.ThrowIfNull(clip);\r\n ArgumentNullException.ThrowIfNull(options);\r\n\r\n if (clip.FrameCount == 0)\r\n throw new ArgumentException(\"Clip has no frames.\", nameof(clip));\r\n for (var f = 0; f < clip.FrameCount; f++)\r\n {\r\n if (clip.Frames[f].Length != skeleton.Count)\r\n throw new ArgumentException(\r\n $\"Frame {f} has {clip.Frames[f].Length} bone transforms, skeleton has {skeleton.Count}.\",\r\n nameof(clip));\r\n }\r\n\r\n var w = new Emitter();\r\n var animListGuid = GuidString(options.Name, \"animationList\");\r\n var jointGuids = new string[skeleton.Count];\r\n var transformGuids = new string[skeleton.Count];\r\n for (var i = 0; i < skeleton.Count; i++)\r\n {\r\n jointGuids[i] = GuidString(options.Name, \"joint:\" + skeleton[i].Name);\r\n transformGuids[i] = GuidString(options.Name, \"transform:\" + skeleton[i].Name);\r\n }\r\n\r\n w.Raw(Header);\r\n\r\n // ---- root DmElement -------------------------------------------------\r\n w.BeginTopLevel(\"DmElement\");\r\n w.Attr(\"name\", \"string\", \"root\");\r\n\r\n w.BeginInlineAttr(\"skeleton\", \"DmeModel\");\r\n w.Attr(\"name\", \"string\", options.Name);\r\n w.BeginInlineAttr(\"transform\", \"DmeTransform\");\r\n w.Attr(\"position\", \"vector3\", \"0 0 0\");\r\n w.Attr(\"orientation\", \"quaternion\", \"0 0 0 1\");\r\n w.EndInlineAttr();\r\n w.Attr(\"shape\", \"element\", \"\");\r\n w.Attr(\"visible\", \"bool\", \"1\");\r\n\r\n w.BeginArray(\"children\");\r\n var roots = new List<int>();\r\n for (var i = 0; i < skeleton.Count; i++)\r\n {\r\n if (skeleton[i].ParentIndex < 0)\r\n roots.Add(i);\r\n }\r\n for (var r = 0; r < roots.Count; r++)\r\n w.ElementRef(jointGuids[roots[r]], last: r == roots.Count - 1);\r\n w.EndArray();\r\n\r\n w.BeginArray(\"jointList\");\r\n for (var i = 0; i < skeleton.Count; i++)\r\n w.ElementRef(jointGuids[i], last: i == skeleton.Count - 1);\r\n w.EndArray();\r\n\r\n w.BeginArray(\"baseStates\");\r\n w.BeginArrayElement(\"DmeTransformList\");\r\n w.Attr(\"name\", \"string\", \"bind\");\r\n w.BeginArray(\"transforms\");\r\n for (var i = 0; i < skeleton.Count; i++)\r\n {\r\n w.BeginArrayElement(\"DmeTransform\");\r\n w.Attr(\"name\", \"string\", skeleton[i].Name);\r\n w.Attr(\"position\", \"vector3\", Vec(skeleton[i].RestLocal));\r\n w.Attr(\"orientation\", \"quaternion\", Quat(skeleton[i].RestLocal));\r\n w.EndArrayElement(last: i == skeleton.Count - 1);\r\n }\r\n w.EndArray();\r\n w.EndArrayElement(last: true);\r\n w.EndArray();\r\n\r\n w.Attr(\"upAxis\", \"string\", options.UpAxisY ? \"Y\" : \"Z\");\r\n w.BeginInlineAttr(\"axisSystem\", \"DmeAxisSystem\");\r\n w.Attr(\"upAxis\", \"int\", options.UpAxisY ? \"2\" : \"3\");\r\n w.Attr(\"forwardParity\", \"int\", \"2\");\r\n w.Attr(\"coordSys\", \"int\", \"0\");\r\n w.EndInlineAttr();\r\n w.Attr(\"animationList\", \"element\", animListGuid);\r\n w.EndInlineAttr(); // skeleton DmeModel\r\n\r\n w.BeginInlineAttr(\"makefile\", \"DmeDCCMakefile\");\r\n w.Attr(\"name\", \"string\", \"makefile\");\r\n w.BeginArray(\"sources\");\r\n w.BeginArrayElement(\"DmeSource\");\r\n w.Attr(\"name\", \"string\", options.SourceNote);\r\n w.EndArrayElement(last: true);\r\n w.EndArray();\r\n w.EndInlineAttr();\r\n\r\n // Deterministic placeholders \u2014 never wall-clock/user data, so output is reproducible.\r\n w.BeginInlineAttr(\"exportTags\", \"DmeExportTags\");\r\n w.Attr(\"name\", \"string\", \"exportTags\");\r\n w.Attr(\"date\", \"string\", \"2026/01/01\");\r\n w.Attr(\"time\", \"string\", \"12:00:00 am\");\r\n w.Attr(\"user\", \"string\", \"retargeter\");\r\n w.Attr(\"machine\", \"string\", \"retargeter\");\r\n w.Attr(\"app\", \"string\", \"humanoid-retargeter\");\r\n w.Attr(\"appVersion\", \"string\", \"1.0\");\r\n w.Attr(\"cmdLine\", \"string\", \"humanoid-retargeter\");\r\n w.Attr(\"pwd\", \"string\", \"\");\r\n w.EndInlineAttr();\r\n\r\n w.Attr(\"animationList\", \"element\", animListGuid);\r\n w.EndTopLevel();\r\n\r\n // ---- DmeAnimationList ----------------------------------------------\r\n w.BeginTopLevel(\"DmeAnimationList\");\r\n w.Attr(\"id\", \"elementid\", animListGuid);\r\n w.Attr(\"name\", \"string\", \"anim\");\r\n w.BeginArray(\"animations\");\r\n w.BeginArrayElement(\"DmeChannelsClip\");\r\n w.Attr(\"name\", \"string\", \"anim\");\r\n\r\n w.BeginInlineAttr(\"timeFrame\", \"DmeTimeFrame\");\r\n w.Attr(\"start\", \"time\", Time(0.0));\r\n w.Attr(\"duration\", \"time\", Time((clip.FrameCount - 1) / (double)clip.Fps));\r\n w.Attr(\"offset\", \"time\", Time(0.0));\r\n w.Attr(\"scale\", \"float\", \"1\");\r\n w.EndInlineAttr();\r\n\r\n w.Attr(\"color\", \"color\", \"0 0 0 0\");\r\n w.Attr(\"text\", \"string\", \"\");\r\n w.Attr(\"mute\", \"bool\", \"0\");\r\n w.BeginArray(\"trackGroups\");\r\n w.EndArray();\r\n w.Attr(\"displayScale\", \"float\", \"1\");\r\n\r\n var channelBones = new List<int>(skeleton.Count);\r\n for (var i = 0; i < skeleton.Count; i++)\r\n {\r\n if (options.ChannelExcludedBones is null || !options.ChannelExcludedBones.Contains(i))\r\n channelBones.Add(i);\r\n }\r\n\r\n w.BeginArray(\"channels\");\r\n for (var n = 0; n < channelBones.Count; n++)\r\n {\r\n var i = channelBones[n];\r\n WriteChannel(w, skeleton, clip, i, transformGuids[i], position: true, last: false);\r\n WriteChannel(w, skeleton, clip, i, transformGuids[i], position: false,\r\n last: n == channelBones.Count - 1);\r\n }\r\n w.EndArray();\r\n\r\n w.Attr(\"frameRate\", \"int\",\r\n ((int)MathF.Round(clip.Fps)).ToString(CultureInfo.InvariantCulture));\r\n w.EndArrayElement(last: true);\r\n w.EndArray();\r\n w.EndTopLevel();\r\n\r\n // ---- top-level channel-target DmeTransforms (rest values) -----------\r\n for (var i = 0; i < skeleton.Count; i++)\r\n {\r\n w.BeginTopLevel(\"DmeTransform\");\r\n w.Attr(\"id\", \"elementid\", transformGuids[i]);\r\n w.Attr(\"name\", \"string\", skeleton[i].Name);\r\n w.Attr(\"position\", \"vector3\", Vec(skeleton[i].RestLocal));\r\n w.Attr(\"orientation\", \"quaternion\", Quat(skeleton[i].RestLocal));\r\n w.EndTopLevel();\r\n }\r\n\r\n // ---- top-level DmeJoints --------------------------------------------\r\n for (var i = 0; i < skeleton.Count; i++)\r\n {\r\n w.BeginTopLevel(\"DmeJoint\");\r\n w.Attr(\"id\", \"elementid\", jointGuids[i]);\r\n w.Attr(\"name\", \"string\", skeleton[i].Name);\r\n w.Attr(\"transform\", \"element\", transformGuids[i]);\r\n w.Attr(\"shape\", \"element\", \"\");\r\n w.Attr(\"visible\", \"bool\", \"1\");\r\n w.BeginArray(\"children\");\r\n var children = new List<int>();\r\n for (var c = 0; c < skeleton.Count; c++)\r\n {\r\n if (skeleton[c].ParentIndex == i)\r\n children.Add(c);\r\n }\r\n for (var c = 0; c < children.Count; c++)\r\n w.ElementRef(jointGuids[children[c]], last: c == children.Count - 1);\r\n w.EndArray();\r\n w.EndTopLevel();\r\n }\r\n\r\n return w.ToString();\r\n }\r\n\r\n /// <summary>\r\n /// Deterministic element GUID: MD5 over <c>\"<name>\\n<path>\"</c> (UTF-8)\r\n /// interpreted as <see cref=\"Guid\"/> bytes. Exposed so tests can verify the scheme.\r\n /// </summary>\r\n public static Guid ElementGuid(string name, string path)\r\n => new(MD5.HashData(Encoding.UTF8.GetBytes(name + \"\\n\" + path)));\r\n\r\n private static string GuidString(string name, string path)\r\n => ElementGuid(name, path).ToString(\"D\", CultureInfo.InvariantCulture);\r\n\r\n // ---------------------------------------------------------------- channels\r\n\r\n private static void WriteChannel(Emitter w, SkeletonModel skeleton, Clip clip, int bone,\r\n string transformGuid, bool position, bool last)\r\n {\r\n var logClass = position ? \"DmeVector3Log\" : \"DmeQuaternionLog\";\r\n var layerClass = position ? \"DmeVector3LogLayer\" : \"DmeQuaternionLogLayer\";\r\n var logName = position ? \"vector3 log\" : \"quaternion log\";\r\n\r\n w.BeginArrayElement(\"DmeChannel\");\r\n w.Attr(\"name\", \"string\", skeleton[bone].Name + (position ? \"_p\" : \"_o\"));\r\n w.Attr(\"fromElement\", \"element\", \"\");\r\n w.Attr(\"fromAttribute\", \"string\", \"\");\r\n w.Attr(\"fromIndex\", \"int\", \"0\");\r\n w.Attr(\"toElement\", \"element\", transformGuid);\r\n w.Attr(\"toAttribute\", \"string\", position ? \"position\" : \"orientation\");\r\n w.Attr(\"toIndex\", \"int\", \"0\");\r\n w.Attr(\"mode\", \"int\", \"3\");\r\n\r\n w.BeginInlineAttr(\"log\", logClass);\r\n w.Attr(\"name\", \"string\", logName);\r\n w.BeginArray(\"layers\");\r\n w.BeginArrayElement(layerClass);\r\n w.Attr(\"name\", \"string\", logName);\r\n\r\n w.BeginArray(\"times\", \"time_array\");\r\n for (var f = 0; f < clip.FrameCount; f++)\r\n w.ArrayValue(Time(f / (double)clip.Fps), last: f == clip.FrameCount - 1);\r\n w.EndArray();\r\n\r\n w.BeginArray(\"curvetypes\", \"int_array\");\r\n w.EndArray();\r\n\r\n w.BeginArray(\"values\", position ? \"vector3_array\" : \"quaternion_array\");\r\n // Orientation values are hemisphere-aligned on the fly (q and -q are the same\r\n // rotation, but the engine interpolates between DMX samples numerically \u2014 see\r\n // QuaternionContinuity). The clip itself is never mutated.\r\n var prev = System.Numerics.Quaternion.Identity;\r\n for (var f = 0; f < clip.FrameCount; f++)\r\n {\r\n var x = clip.Frames[f][bone];\r\n string value;\r\n if (position)\r\n {\r\n value = Vec(x);\r\n }\r\n else\r\n {\r\n var q = x.Rot;\r\n if (f > 0 && System.Numerics.Quaternion.Dot(prev, q) < 0f)\r\n q = System.Numerics.Quaternion.Negate(q);\r\n prev = q;\r\n value = Quat(q);\r\n }\r\n w.ArrayValue(value, last: f == clip.FrameCount - 1);\r\n }\r\n w.EndArray();\r\n\r\n w.EmptyBinaryAttr(\"compressed\");\r\n w.EndArrayElement(last: true);\r\n w.EndArray(); // layers\r\n\r\n w.Attr(\"curveinfo\", \"element\", \"\");\r\n w.Attr(\"usedefaultvalue\", \"bool\", \"0\");\r\n w.Attr(\"defaultvalue\", position ? \"vector3\" : \"quaternion\", position ? \"0 0 0\" : \"0 0 0 1\");\r\n w.BeginArray(\"bookmarksX\", \"time_array\");\r\n w.EndArray();\r\n w.BeginArray(\"bookmarksY\", \"time_array\");\r\n w.EndArray();\r\n w.BeginArray(\"bookmarksZ\", \"time_array\");\r\n w.EndArray();\r\n w.EndInlineAttr(); // log\r\n\r\n w.EndArrayElement(last);\r\n }\r\n\r\n // ---------------------------------------------------------------- formatting\r\n\r\n /// <summary>fbx2dmx float style: up to 10 decimal places, trailing zeros stripped,\r\n /// invariant culture, negative zero normalized.</summary>\r\n private static string F(float value)\r\n {\r\n if (value == 0f)\r\n return \"0\";\r\n return ((double)value).ToString(\"0.##########\", CultureInfo.InvariantCulture);\r\n }\r\n\r\n private static string Time(double seconds)\r\n => seconds.ToString(\"0.0000\", CultureInfo.InvariantCulture);\r\n\r\n private static string Vec(in Maths.XForm x)\r\n => $\"{F(x.Pos.X)} {F(x.Pos.Y)} {F(x.Pos.Z)}\";\r\n\r\n private static string Quat(in Maths.XForm x) => Quat(x.Rot);\r\n\r\n private static string Quat(in System.Numerics.Quaternion q)\r\n => $\"{F(q.X)} {F(q.Y)} {F(q.Z)} {F(q.W)}\";\r\n\r\n // ---------------------------------------------------------------- emitter\r\n\r\n /// <summary>\r\n /// Low-level keyvalues2 text emitter reproducing fbx2dmx layout quirks: CRLF endings,\r\n /// tab indentation, a trailing space after array-typed attribute names, and an\r\n /// indentation-only line after every inline element attribute closes.\r\n /// </summary>\r\n private sealed class Emitter\r\n {\r\n private readonly StringBuilder _sb = new();\r\n private int _indent;\r\n\r\n public void Raw(string text)\r\n {\r\n _sb.Append(text).Append(\"\\r\\n\");\r\n }\r\n\r\n private void Line(string text)\r\n {\r\n _sb.Append('\\t', _indent).Append(text).Append(\"\\r\\n\");\r\n }\r\n\r\n public void Attr(string name, string type, string value)\r\n => Line($\"\\\"{name}\\\" \\\"{type}\\\" \\\"{value}\\\"\");\r\n\r\n public void BeginTopLevel(string className)\r\n {\r\n Line($\"\\\"{className}\\\"\");\r\n Line(\"{\");\r\n _indent++;\r\n }\r\n\r\n public void EndTopLevel()\r\n {\r\n _indent--;\r\n Line(\"}\");\r\n _sb.Append(\"\\r\\n\"); // blank separator after every top-level element (incl. the last)\r\n }\r\n\r\n public void BeginInlineAttr(string name, string className)\r\n {\r\n Line($\"\\\"{name}\\\" \\\"{className}\\\"\");\r\n Line(\"{\");\r\n _indent++;\r\n }\r\n\r\n public void EndInlineAttr()\r\n {\r\n _indent--;\r\n Line(\"}\");\r\n Line(\"\"); // indentation-only line, as fbx2dmx emits\r\n }\r\n\r\n public void BeginArrayElement(string className)\r\n {\r\n Line($\"\\\"{className}\\\"\");\r\n Line(\"{\");\r\n _indent++;\r\n }\r\n\r\n public void EndArrayElement(bool last)\r\n {\r\n _indent--;\r\n Line(last ? \"}\" : \"},\");\r\n }\r\n\r\n public void BeginArray(string name, string type = \"element_array\")\r\n {\r\n Line($\"\\\"{name}\\\" \\\"{type}\\\" \");\r\n Line(\"[\");\r\n _indent++;\r\n }\r\n\r\n public void EndArray()\r\n {\r\n _indent--;\r\n Line(\"]\");\r\n }\r\n\r\n public void ElementRef(string guid, bool last)\r\n => Line($\"\\\"element\\\" \\\"{guid}\\\"\" + (last ? \"\" : \",\"));\r\n\r\n public void ArrayValue(string value, bool last)\r\n => Line($\"\\\"{value}\\\"\" + (last ? \"\" : \",\"));\r\n\r\n public void EmptyBinaryAttr(string name)\r\n {\r\n Line($\"\\\"{name}\\\" \\\"binary\\\" \");\r\n Line(\"\\\"\");\r\n Line(\"\\\"\");\r\n }\r\n\r\n public override string ToString() => _sb.ToString();\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_retargeter",
"Path": "HumanoidRetargeter/Formats/Fbx/FbxBindPoseFixer.cs",
"FileName": "FbxBindPoseFixer.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 311783,
"Code": "#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Numerics;\r\nusing HumanoidRetargeter.Maths;\r\n\r\nnamespace HumanoidRetargeter.Formats.Fbx;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)\r\n\r\n/// <summary>\r\n/// Repairs FBX files that were exported MID-POSE: their node transforms (Lcl\r\n/// Translation/Rotation) hold an animation snapshot while the true skeleton bind lives in\r\n/// the file's Pose/BindPose section. Engines that build the skeleton from node transforms\r\n/// (s&box does; the FBX SDK's own samples do) then import a posed \"bind\" \u2014 the skin\r\n/// stays self-consistent so the model LOOKS fine at rest, but every anatomical assumption\r\n/// about the skeleton (leg chains point down, hands mirror) is silently wrong and\r\n/// retargeted motion comes out mangled on exactly the posed bones. Found in the wild on\r\n/// Auto-Rig Pro exports whose IK'd hands/feet were left posed (one leg at hip height).\r\n/// </summary>\r\npublic static class FbxBindPoseFixer\r\n{\r\n /// <summary>Bones whose node transform is further than this (native units, as a\r\n /// fraction of skeleton height) from their BindPose matrix count as posed.</summary>\r\n private const float PositionToleranceOfHeight = 0.01f;\r\n\r\n /// <summary>Rotation disagreement (degrees) that counts as posed.</summary>\r\n private const float RotationToleranceDeg = 2.0f;\r\n\r\n /// <summary>\r\n /// Detects the mid-pose condition and rewrites node transforms to the BindPose. Returns\r\n /// null when the file needs no repair (no BindPose section, or node transforms already\r\n /// agree with it); otherwise the repaired file bytes.\r\n /// <paramref name=\"report\"/> always describes what was found.\r\n /// </summary>\r\n public static byte[]? TryFix(byte[] fbx, out string report)\r\n {\r\n ArgumentNullException.ThrowIfNull(fbx);\r\n\r\n FbxNode root;\r\n FbxScene scene;\r\n try\r\n {\r\n root = FbxTokenizer.Parse(fbx);\r\n scene = FbxScene.Build(root);\r\n }\r\n catch (FormatException e)\r\n {\r\n report = $\"not parseable ({e.Message})\";\r\n return null;\r\n }\r\n\r\n if (scene.BindPose.Count == 0)\r\n {\r\n report = \"no BindPose section\";\r\n return null;\r\n }\r\n\r\n // Evaluate the ORIGINAL node-transform FK, roots first.\r\n var originalWorld = new Dictionary<long, Matrix4x4>();\r\n var order = new List<FbxObject>();\r\n foreach (var model in scene.Models)\r\n VisitModel(model, scene, originalWorld, order);\r\n\r\n // Skeleton height (native units) for the position tolerance.\r\n float minUp = float.MaxValue, maxUp = float.MinValue;\r\n foreach (var w in originalWorld.Values)\r\n {\r\n var t = w.Translation;\r\n float up = MathF.Max(MathF.Abs(t.Y), MathF.Abs(t.Z));\r\n minUp = MathF.Min(minUp, up);\r\n maxUp = MathF.Max(maxUp, up);\r\n }\r\n float posTolerance = MathF.Max(0.0001f, (maxUp - minUp) * PositionToleranceOfHeight);\r\n\r\n // Any bone posed away from its bind?\r\n int posedCount = 0;\r\n foreach (var model in order)\r\n {\r\n if (!scene.BindPose.TryGetValue(model.Id, out var bind))\r\n continue;\r\n var fk = FbxTransform.ToRigid(originalWorld[model.Id]);\r\n var target = FbxTransform.ToRigid(bind);\r\n if ((fk.Pos - target.Pos).Length() > posTolerance\r\n || MathQ.AngleBetween(fk.Rot, target.Rot) > RotationToleranceDeg * MathF.PI / 180f)\r\n {\r\n posedCount++;\r\n }\r\n }\r\n if (posedCount == 0)\r\n {\r\n report = $\"node transforms match the BindPose ({scene.BindPose.Count} entries)\";\r\n return null;\r\n }\r\n\r\n // Rewrite every BindPose-backed model's local transform so FK lands on the bind.\r\n // correctedWorld carries the repair down the hierarchy for bones WITHOUT a\r\n // BindPose entry (helpers keep their original locals under corrected parents).\r\n var correctedWorld = new Dictionary<long, Matrix4x4>();\r\n int patched = 0, skipped = 0;\r\n foreach (var model in order)\r\n {\r\n var parent = model.ModelParent;\r\n Matrix4x4 parentWorld = parent is not null && correctedWorld.TryGetValue(parent.Id, out var pw)\r\n ? pw\r\n : Matrix4x4.Identity;\r\n\r\n if (!scene.BindPose.TryGetValue(model.Id, out var bindWorld))\r\n {\r\n // No bind info: keep the original local under the (possibly corrected) parent.\r\n var transform = FbxTransform.FromModel(scene, model);\r\n correctedWorld[model.Id] = transform.LocalMatrixDefault() * parentWorld;\r\n continue;\r\n }\r\n\r\n if (!Matrix4x4.Invert(parentWorld, out var invParent))\r\n {\r\n correctedWorld[model.Id] = bindWorld;\r\n skipped++;\r\n continue;\r\n }\r\n\r\n // Row-vector: World = Local \u00b7 ParentWorld \u21d2 Local = World \u00b7 ParentWorld\u207b\u00b9.\r\n var desiredLocal = FbxTransform.ToRigid(bindWorld * invParent);\r\n if (PatchModelLocal(scene, model, desiredLocal))\r\n patched++;\r\n else\r\n skipped++;\r\n\r\n // Children FK from the ACTUAL bind either way (unpatchable bones are rare and\r\n // their children still deserve correct parent frames).\r\n correctedWorld[model.Id] = bindWorld;\r\n }\r\n\r\n report = $\"{posedCount} bones were exported mid-pose; repaired {patched}\"\r\n + (skipped > 0 ? $\", {skipped} left as-is (pivots/scale beyond the safe rewrite)\" : \"\");\r\n if (patched == 0)\r\n return null;\r\n return FbxBinaryWriter.Write(root);\r\n }\r\n\r\n private static void VisitModel(\r\n FbxObject model, FbxScene scene,\r\n Dictionary<long, Matrix4x4> world, List<FbxObject> order)\r\n {\r\n if (world.ContainsKey(model.Id))\r\n return;\r\n Matrix4x4 parentWorld = Matrix4x4.Identity;\r\n if (model.ModelParent is { } parent)\r\n {\r\n VisitModel(parent, scene, world, order);\r\n parentWorld = world[parent.Id];\r\n }\r\n var transform = FbxTransform.FromModel(scene, model);\r\n world[model.Id] = transform.LocalMatrixDefault() * parentWorld;\r\n order.Add(model);\r\n }\r\n\r\n // ------------------------------------------------------------------ patching\r\n\r\n /// <summary>\r\n /// Rewrites one Model's Lcl Translation/Rotation so its local evaluates to\r\n /// <paramref name=\"desiredLocal\"/>. Verified by re-evaluating through the full FBX\r\n /// transform formula \u2014 models using pivots/offsets/scale that the rewrite cannot\r\n /// express are left untouched (returns false).\r\n /// </summary>\r\n private static bool PatchModelLocal(FbxScene scene, FbxObject model, XForm desiredLocal)\r\n {\r\n var transform = FbxTransform.FromModel(scene, model);\r\n\r\n // R_total = Pre \u00b7 R \u00b7 Post\u207b\u00b9 \u21d2 R = Pre\u207b\u00b9 \u00b7 R_total \u00b7 Post\r\n var r = MathQ.Normalize(\r\n Quaternion.Conjugate(transform.PreRotation)\r\n * desiredLocal.Rot\r\n * transform.PostRotation);\r\n\r\n var eulerDeg = QuaternionToEulerDegrees(r, transform.RotationOrder);\r\n\r\n // Full-formula verification (catches pivots, scale, decomposition branches).\r\n var check = new FbxTransform\r\n {\r\n LclTranslation = desiredLocal.Pos,\r\n LclRotationDeg = eulerDeg,\r\n LclScaling = transform.LclScaling,\r\n PreRotation = transform.PreRotation,\r\n PostRotation = transform.PostRotation,\r\n RotationOffset = transform.RotationOffset,\r\n RotationPivot = transform.RotationPivot,\r\n ScalingOffset = transform.ScalingOffset,\r\n ScalingPivot = transform.ScalingPivot,\r\n RotationOrder = transform.RotationOrder,\r\n };\r\n var evaluated = FbxTransform.ToRigid(check.LocalMatrixDefault());\r\n float posScale = MathF.Max(1f, desiredLocal.Pos.Length());\r\n if ((evaluated.Pos - desiredLocal.Pos).Length() > 0.001f * posScale\r\n || MathQ.AngleBetween(evaluated.Rot, desiredLocal.Rot) > 0.1f * MathF.PI / 180f)\r\n {\r\n return false;\r\n }\r\n\r\n SetProperty70(model.Node, \"Lcl Translation\", \"Lcl Translation\", \"A\",\r\n desiredLocal.Pos.X, desiredLocal.Pos.Y, desiredLocal.Pos.Z);\r\n SetProperty70(model.Node, \"Lcl Rotation\", \"Lcl Rotation\", \"A\",\r\n eulerDeg.X, eulerDeg.Y, eulerDeg.Z);\r\n return true;\r\n }\r\n\r\n /// <summary>Sets (or adds) a 3-double P entry in the node's Properties70 block.</summary>\r\n private static void SetProperty70(\r\n FbxNode modelNode, string name, string type, string flags,\r\n double x, double y, double z)\r\n {\r\n var block = modelNode.Child(\"Properties70\");\r\n if (block is null)\r\n {\r\n block = new FbxNode(\"Properties70\");\r\n modelNode.Children.Insert(0, block);\r\n }\r\n\r\n foreach (var p in block.ChildrenNamed(\"P\"))\r\n {\r\n if (p.Properties.Count >= 1 && p.Properties[0] is string n && n == name)\r\n {\r\n // Values live at indices 4.. \u2014 replace, extending if the entry was short.\r\n while (p.Properties.Count < 7)\r\n p.Properties.Add(0.0);\r\n p.Properties[4] = x;\r\n p.Properties[5] = y;\r\n p.Properties[6] = z;\r\n return;\r\n }\r\n }\r\n\r\n var entry = new FbxNode(\"P\");\r\n entry.Properties.Add(name);\r\n entry.Properties.Add(type);\r\n entry.Properties.Add(\"\");\r\n entry.Properties.Add(flags);\r\n entry.Properties.Add(x);\r\n entry.Properties.Add(y);\r\n entry.Properties.Add(z);\r\n block.Children.Add(entry);\r\n }\r\n\r\n // ------------------------------------------------------------------ euler decomposition\r\n\r\n /// <summary>\r\n /// Decomposes a quaternion into FBX euler degrees for the given RotationOrder, the\r\n /// exact inverse of <see cref=\"FbxTransform.EulerDegreesToQuaternion\"/>. Tait-Bryan\r\n /// extraction on the column-convention rotation matrix.\r\n /// </summary>\r\n public static Vector3 QuaternionToEulerDegrees(Quaternion q, int order)\r\n {\r\n // Column-convention matrix C (v' = C\u00b7v): C = transpose of System.Numerics' row form.\r\n var m = Matrix4x4.CreateFromQuaternion(q);\r\n // C[r,c]: row r, column c.\r\n float c00 = m.M11, c01 = m.M21, c02 = m.M31;\r\n float c10 = m.M12, c11 = m.M22, c12 = m.M32;\r\n float c20 = m.M13, c21 = m.M23, c22 = m.M33;\r\n\r\n const float radToDeg = 180f / MathF.PI;\r\n float a, b, c;\r\n switch (order)\r\n {\r\n case 0: // XYZ: C = Rz\u00b7Ry\u00b7Rx\r\n b = MathF.Asin(Math.Clamp(-c20, -1f, 1f));\r\n a = MathF.Atan2(c21, c22);\r\n c = MathF.Atan2(c10, c00);\r\n return new Vector3(a * radToDeg, b * radToDeg, c * radToDeg);\r\n case 1: // XZY: C = Ry\u00b7Rz\u00b7Rx\r\n b = MathF.Asin(Math.Clamp(c10, -1f, 1f));\r\n a = MathF.Atan2(-c12, c11);\r\n c = MathF.Atan2(-c20, c00);\r\n return new Vector3(a * radToDeg, c * radToDeg, b * radToDeg);\r\n case 2: // YZX: C = Rx\u00b7Rz\u00b7Ry\r\n b = MathF.Asin(Math.Clamp(-c01, -1f, 1f));\r\n a = MathF.Atan2(c02, c00);\r\n c = MathF.Atan2(c21, c11);\r\n return new Vector3(c * radToDeg, a * radToDeg, b * radToDeg);\r\n case 3: // YXZ: C = Rz\u00b7Rx\u00b7Ry\r\n b = MathF.Asin(Math.Clamp(c21, -1f, 1f));\r\n a = MathF.Atan2(-c20, c22);\r\n c = MathF.Atan2(-c01, c11);\r\n return new Vector3(b * radToDeg, a * radToDeg, c * radToDeg);\r\n case 4: // ZXY: C = Ry\u00b7Rx\u00b7Rz\r\n b = MathF.Asin(Math.Clamp(-c12, -1f, 1f));\r\n a = MathF.Atan2(c02, c22);\r\n c = MathF.Atan2(c10, c11);\r\n return new Vector3(b * radToDeg, a * radToDeg, c * radToDeg);\r\n case 5: // ZYX: C = Rx\u00b7Ry\u00b7Rz\r\n case 6: // eSphericXYZ treated as XYZ on read; mirror that here\r\n default:\r\n if (order == 5)\r\n {\r\n b = MathF.Asin(Math.Clamp(c02, -1f, 1f));\r\n a = MathF.Atan2(-c01, c00);\r\n c = MathF.Atan2(-c12, c22);\r\n return new Vector3(c * radToDeg, b * radToDeg, a * radToDeg);\r\n }\r\n goto case 0;\r\n }\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_retargeter",
"Path": "HumanoidRetargeter/RetargetRequest.cs",
"FileName": "RetargetRequest.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 311783,
"Code": "#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing HumanoidRetargeter.Cleanup;\r\nusing HumanoidRetargeter.Formats;\r\nusing HumanoidRetargeter.Mapping;\r\nusing HumanoidRetargeter.Solve;\r\nusing HumanoidRetargeter.Target;\r\n\r\nnamespace HumanoidRetargeter;\r\n\r\n/// <summary>Which solver retargets a request's clips (design \u00a710).</summary>\r\npublic enum SolverKind\r\n{\r\n /// <summary>The deterministic <see cref=\"Solve.GeometricSolver\"/> (default; better\r\n /// wherever a role mapping exists).</summary>\r\n Geometric,\r\n\r\n /// <summary>The experimental skeleton-agnostic deep-learning solver\r\n /// (<see cref=\"Dl.DlSolver\"/>, SAME pretrained checkpoint) \u2014 the no-profile fallback.\r\n /// Requires <see cref=\"RetargetTargetSpec.DlWeights\"/>; ignores per-role mapping\r\n /// (only hips/alignment heuristics consult it) and leaves fingers at rest.</summary>\r\n DeepLearning,\r\n}\r\n\r\n/// <summary>\r\n/// One source animation file to retarget (engine-agnostic: bytes in, no file IO). Every\r\n/// request runs its OWN profile detection, so a single batch may mix Mixamo + ActorCore +\r\n/// BVH sources \u2014 unless <see cref=\"MappingOverride\"/> supplies a mapping explicitly.\r\n/// </summary>\r\npublic sealed class RetargetRequest\r\n{\r\n /// <summary>Solver choice for this request's clips. <see cref=\"SolverKind.DeepLearning\"/>\r\n /// requires the batch's <see cref=\"RetargetTargetSpec.DlWeights\"/> to be set; the\r\n /// conversion fails per-clip with a clear error otherwise.</summary>\r\n public SolverKind Solver { get; init; } = SolverKind.Geometric;\r\n\r\n /// <summary>Raw bytes of the source file (.fbx, .bvh, .glb, .gltf, .vrm, .anm or .an5).</summary>\r\n public required byte[] SourceData { get; init; }\r\n\r\n /// <summary>\r\n /// Source file name (used for the report and DMX provenance). The extension drives the\r\n /// format choice (<c>.fbx</c> / <c>.bvh</c> / <c>.glb</c> / <c>.gltf</c> / <c>.vrm</c> \u2014\r\n /// a VRM is a glTF container whose authored humanoid bone map becomes the mapping \u2014 /\r\n /// <c>.anm</c> / <c>.an5</c> RenderWare animations, which additionally need\r\n /// <see cref=\"SkeletonData\"/>); when the extension is unknown the content is sniffed\r\n /// (FBX binary magic / \"FBXHeaderExtension\" / BVH \"HIERARCHY\" / GLB 'glTF' magic /\r\n /// glTF JSON / RenderWare 0x1B animation chunk).\r\n /// </summary>\r\n public required string SourceFileName { get; init; }\r\n\r\n /// <summary>\r\n /// Raw bytes of a companion SKELETON file for formats whose animation files carry no\r\n /// skeleton of their own: RenderWare <c>.anm</c>/<c>.an5</c> sources require the\r\n /// character model's <c>.dff</c> here (callers resolve the file \u2014 e.g. a .dff sitting\r\n /// next to the animation; the facade does no file IO). Ignored by self-contained\r\n /// formats. A RenderWare request without it fails with an instructive error.\r\n /// </summary>\r\n public byte[]? SkeletonData { get; init; }\r\n\r\n /// <summary>\r\n /// Caller-supplied identity of this request, echoed verbatim on every produced\r\n /// <see cref=\"ClipResult.SourceId\"/> so callers can join results back to their own\r\n /// entries unambiguously (e.g. the editor window passes the FULL file path here, since\r\n /// two files in different folders may share the same <see cref=\"SourceFileName\"/>).\r\n /// Null = <see cref=\"SourceFileName\"/>.\r\n /// </summary>\r\n public string? SourceId { get; init; }\r\n\r\n /// <summary>\r\n /// Import sample rate the source clips are resampled to (BVH native frames / FBX curves\r\n /// are evaluated on this grid). Null = the importer default (30 fps).\r\n /// </summary>\r\n public float? SampleFps { get; init; }\r\n\r\n /// <summary>\r\n /// Restricts the conversion to ONE take of the source file (0-based index into the\r\n /// imported scene's clips). Null = convert all takes. Out of range fails the request's\r\n /// clip result with a clear error (the batch continues). UI listings that expand a\r\n /// multi-take file into one entry per take submit one request per selected take.\r\n /// When <see cref=\"ClipDefinitions\"/> is set this index addresses the DEFINITIONS\r\n /// instead (each definition is what a UI row represents then).\r\n /// </summary>\r\n public int? TakeIndex { get; init; }\r\n\r\n /// <summary>\r\n /// Optional external clip definitions, parsed from a Unity <c><file>.fbx.meta</c>\r\n /// sidecar (<see cref=\"UnityMeta.ParseClipAnimations\"/>): Unity animation packs ship FBX\r\n /// files whose clips are sub-ranges of ONE source timeline. When set (non-empty), the\r\n /// conversion produces one output clip per definition instead of one per take: the\r\n /// definition's take (matched by <see cref=\"ExternalClipDef.TakeName\"/>, falling back to\r\n /// the file's first take) is sliced to the definition's native-frame range\r\n /// (<see cref=\"UnityMeta.Slice\"/>), named <see cref=\"ExternalClipDef.Name\"/> (sanitized\r\n /// like take names, collision-suffixed across the batch) and looped per\r\n /// <see cref=\"ExternalClipDef.Loop\"/> unless <see cref=\"LoopingOverride\"/> is set.\r\n /// <see cref=\"TakeIndex\"/> then indexes INTO this list. Null = no definitions.\r\n /// </summary>\r\n public IReadOnlyList<ExternalClipDef>? ClipDefinitions { get; init; }\r\n\r\n /// <summary>\r\n /// UI-supplied mapping (manual mapping table or a user preset loaded Editor-side).\r\n /// Null = auto-detect per request: preset profiles via <see cref=\"ProfileDetector\"/>,\r\n /// then the <see cref=\"AutoMapper\"/> as best-effort fallback.\r\n /// </summary>\r\n public MappingResult? MappingOverride { get; init; }\r\n\r\n /// <summary>Solver tunables (hip scales, finger transfer). ClipIndex/ClipName are managed\r\n /// by the pipeline per take and ignored here. Null = defaults.</summary>\r\n public SolveOptions? Solve { get; init; }\r\n\r\n /// <summary>\r\n /// Root-motion handling. <see cref=\"RootMotionMode.Extract\"/> on a target without a\r\n /// dedicated animated root bone (the s&box rig: pelvis is parentless, root_IK is\r\n /// IkBaked) leaves the frames untouched and instead sets the ExtractMotion flag on the\r\n /// clip's vmdl AnimFile entry \u2014 Source 2's compile-time extraction replaces the missing\r\n /// bone-level extraction. <see cref=\"RootMotionMode.InPlace\"/> always operates on the\r\n /// hips directly.\r\n /// </summary>\r\n public RootMotionMode RootMotion { get; init; } = RootMotionMode.Off;\r\n\r\n /// <summary>Run the Kovar foot-plant cleanup pass on the solved frames (default on).</summary>\r\n public bool FootPlantCleanup { get; init; } = true;\r\n\r\n /// <summary>\r\n /// Copy the source clip's per-frame LOCAL translations onto same-named target bones\r\n /// (hips and its ancestors excluded \u2014 trajectory stays solver-owned). For SAME-RIG\r\n /// conversions of authored takes (a target FBX's own embedded animations): the solver\r\n /// pins every non-hips bone to its rest translation, silently dropping a Biped take's\r\n /// animated spine/thigh translations (~19cm of authored body sway on a death fall).\r\n /// Meaningless across different rigs \u2014 leave off (default) for real retargets.\r\n /// </summary>\r\n public bool PreserveSourceTranslations { get; init; }\r\n\r\n /// <summary>\r\n /// Optional arm end-effector IK pass pulling the wrists onto limb-length-normalized\r\n /// source hand positions. Default OFF: the geometric solver already matches anatomical\r\n /// directions, so arm IK only helps reach-critical work (props, contact poses) and can\r\n /// otherwise disturb elbow styling.\r\n /// </summary>\r\n public bool ArmEffectorIk { get; init; }\r\n\r\n /// <summary>\r\n /// Generate <c>AE_FOOTSTEP</c> AnimEvent nodes on each produced clip's vmdl AnimFile\r\n /// entry (default OFF). After solving and cleanup, foot-plant intervals are detected on\r\n /// the SOLVED target clip (<see cref=\"Cleanup.FootPlant.DetectPlantIntervals\"/>); each\r\n /// plant's start frame is a touchdown and becomes one footstep event, in the exact node\r\n /// shape the shipped citizen data uses (see <see cref=\"Target.FootstepEvents\"/>).\r\n /// Skipped (with a report note) when the target rig lacks complete leg chains.\r\n /// </summary>\r\n public bool GenerateFootstepEvents { get; init; }\r\n\r\n /// <summary>\r\n /// Additionally produce a mirrored twin of every converted clip (default OFF), named\r\n /// <c><clip>_M</c> (collision-suffixed across the batch as usual). Mirroring runs\r\n /// in TARGET space on the solved clip (<see cref=\"Solve.ClipMirror\"/>): left/right role\r\n /// bone channels swap and everything is reflected across the target character's sagittal\r\n /// plane; IK-baked helper bones are re-baked from the mirrored body afterwards.\r\n /// </summary>\r\n public bool CreateMirroredVariant { get; init; }\r\n\r\n /// <summary>\r\n /// Additionally register an additive (delta) twin of every converted clip in the\r\n /// generated/augmented vmdl (default OFF), named <c><clip>_delta</c> (the shipped\r\n /// citizen naming; collision-suffixed across the batch as usual). The twin is a second\r\n /// AnimFile entry REUSING the clip's DMX with an <c>AnimSubtract</c> child\r\n /// (<c>anim_name</c> = the base sequence, <c>frame</c> = 0) \u2014 exactly the shipped\r\n /// <c>IdleLayer_01</c>/<c>IdleLayer_01_delta</c> pattern, where resourcecompiler\r\n /// subtracts the reference frame at compile time (no frame math happens here). The\r\n /// resulting <c>_delta</c> sequence is what s&box layered animation additively\r\n /// blends on top of a base pose.\r\n /// </summary>\r\n public bool CreateAdditiveVariant { get; init; }\r\n\r\n /// <summary>Output clip name override; with multiple takes an index suffix is appended.\r\n /// Null = the source take name.</summary>\r\n public string? ClipNameOverride { get; init; }\r\n\r\n /// <summary>Force the looping flag on the output sequence(s); null = the source clip's flag.</summary>\r\n public bool? LoopingOverride { get; init; }\r\n}\r\n\r\n/// <summary>\r\n/// Axis/unit convention of a <see cref=\"RetargetTargetSpec\"/>'s rig data \u2014 drives the DMX\r\n/// axis-system declaration, foot-plant threshold units, and the editor preview's\r\n/// rig-space \u2192 engine-space conversion.\r\n/// </summary>\r\npublic enum TargetUpAxis\r\n{\r\n /// <summary>\r\n /// The s&box source convention: rig authored in centimeters, Y-up (the shipped\r\n /// citizen rig, FBX targets). The vmdl's ScaleAndMirror 0.3937 + resourcecompiler's\r\n /// Y-up\u2192Z-up conversion take it to engine space at compile time. Default.\r\n /// </summary>\r\n YUpCm,\r\n\r\n /// <summary>\r\n /// Engine space already: rig read from a compiled model's <c>Model.Bones</c>\r\n /// (inches, Z-up). The DMX declares a Z-up axis system so the compiler performs no\r\n /// further axis conversion.\r\n /// </summary>\r\n ZUpEngine,\r\n\r\n /// <summary>\r\n /// A Z-up rig authored in centimeters: FBX targets whose GlobalSettings declare a Z\r\n /// up-axis (UE and 3ds Max exports; Maya/Blender exports are Y-up). The DMX declares\r\n /// Z-up (no compile-time rotation \u2014 the mesh source is in the same Z-up space) while\r\n /// the vmdl's ScaleAndMirror 0.3937 still converts cm\u2192inches. Without this, a Z-up\r\n /// FBX target compiles lying on its back.\r\n /// </summary>\r\n ZUpCm,\r\n}\r\n\r\n/// <summary>\r\n/// The conversion target shared by all requests of one <see cref=\"Retargeter.Convert\"/> /\r\n/// <see cref=\"Retargeter.ConvertBatch\"/> call: the rig plus the vmdl generation parameters.\r\n/// </summary>\r\npublic sealed class RetargetTargetSpec\r\n{\r\n /// <summary>The s&box-source \u2192 engine-units vmdl scale (cm rigs like the citizen).</summary>\r\n public const float SboxSourceScale = 0.3937f;\r\n\r\n /// <summary>The committed asset path of the s&box human male model.</summary>\r\n public const string SboxHumanMalePath = \"models/citizen_human/citizen_human_male.vmdl\";\r\n\r\n /// <summary>The committed asset path of the classic (4-finger) s&box citizen model.</summary>\r\n public const string SboxCitizenPath = \"models/citizen/citizen.vmdl\";\r\n\r\n /// <summary>Target rig (skeleton + bone classes + roles).</summary>\r\n public required TargetRig Rig { get; set; }\r\n\r\n /// <summary>ModelModifier_ScaleAndMirror scale written into standalone vmdls:\r\n /// <c>0.3937</c> for cm-authored s&box-source rigs, <c>1.0</c> for engine-unit rigs\r\n /// (the modifier node is omitted at 1.0).</summary>\r\n public required float VmdlScale { get; init; }\r\n\r\n /// <summary>base_model_name of generated standalone vmdls (the model that owns the mesh).</summary>\r\n public string BaseModelPath { get; init; } = \"\";\r\n\r\n /// <summary>\r\n /// Assets-relative mesh source file (e.g. an <c>.fbx</c>) embedded in generated\r\n /// standalone vmdls as a <c>RenderMeshList/RenderMeshFile</c> node. Custom FBX targets\r\n /// have no compiled base model to point <see cref=\"BaseModelPath\"/> at \u2014 without a mesh\r\n /// source their standalone vmdl compiles into an EMPTY model (0 bones, 0 sequences) and\r\n /// playing it does nothing. Callers own copying the file into the project (this type\r\n /// does no IO); settable so the editor can fill it at convert time once the output\r\n /// folder is known. Empty (default) = no mesh node.\r\n /// </summary>\r\n public string MeshFilePath { get; set; } = \"\";\r\n\r\n /// <summary>\r\n /// Import scale of <see cref=\"MeshFilePath\"/> (raw mesh-file units \u2192 the target\r\n /// skeleton's units). resourcecompiler reads mesh files' raw values ignoring their unit\r\n /// metadata, while the importer normalizes the target skeleton to centimeters \u2014 a\r\n /// meters-authored FBX therefore needs 100 here (the importer's recorded\r\n /// source-unit\u2192cm factor) for the mesh to match the animation skeleton.\r\n /// </summary>\r\n public float MeshImportScale { get; set; } = 1.0f;\r\n\r\n /// <summary>\r\n /// Material remaps written into generated standalone vmdls as a\r\n /// MaterialGroupList/DefaultMaterialGroup (bare mesh material reference \u2192 assets-relative\r\n /// vmat path, e.g. <c>\"mi_dante_head.vmat\" \u2192 \"animations/retargeted/mi_dante_head.vmat\"</c>).\r\n /// FBX materials carry bare names the compiler cannot resolve as resource paths\r\n /// (\"Trying to load an illegal resource name X.vmat\"); this remap table \u2014 the same\r\n /// mechanism the shipped citizen vmdl uses \u2014 points them at real files. Null/empty =\r\n /// no material group node (default).\r\n /// </summary>\r\n public IReadOnlyDictionary<string, string>? MaterialRemaps { get; set; }\r\n\r\n /// <summary>\r\n /// Additional AnimFile entries appended to generated/augmented vmdls verbatim \u2014\r\n /// the target FBX's OWN embedded animations (an FBX with an animation on it must keep\r\n /// that animation when new ones are retargeted onto it; the AnimFile references the\r\n /// FBX directly, exactly like the shipped citizen animation list references its\r\n /// Citizen@*.fbx files, so the import is lossless). Augmentation skips entries the\r\n /// existing vmdl already carries (idempotent re-runs). Null/empty = none (default).\r\n /// </summary>\r\n public IReadOnlyList<Target.AnimEntry>? ExtraAnimFiles { get; set; }\r\n\r\n /// <summary>default_root_bone_name of the generated AnimationList (also the bone vmdl\r\n /// ExtractMotion nodes operate on).</summary>\r\n public string DefaultRootBone { get; set; } = \"pelvis\";\r\n\r\n /// <summary>\r\n /// Axis/unit convention of <see cref=\"Rig\"/>. <see cref=\"TargetUpAxis.YUpCm\"/> (default)\r\n /// for cm Y-up source-space rigs (DMX declares Y-up, compiler converts);\r\n /// <see cref=\"TargetUpAxis.ZUpEngine\"/> for rigs read from compiled engine models\r\n /// (DMX declares Z-up so no double conversion happens at compile, and cm-tuned cleanup\r\n /// thresholds are rescaled to inches).\r\n /// </summary>\r\n public TargetUpAxis UpAxis { get; init; } = TargetUpAxis.YUpCm;\r\n\r\n /// <summary>\r\n /// Raw bytes of the committed SAME weight blob\r\n /// (<c>Assets/humanoid_retargeter/dl/same_v1.weights</c>; callers do the file IO).\r\n /// Required only when a request selects <see cref=\"SolverKind.DeepLearning\"/>; the\r\n /// solver instance is built once per batch from these bytes.\r\n /// </summary>\r\n public byte[]? DlWeights { get; init; }\r\n\r\n /// <summary>\r\n /// The shipped s&box default target: rig parsed from the committed\r\n /// <c>Assets/humanoid_retargeter/target_rig_sbox.json</c> text (callers do the file IO),\r\n /// 0.3937 vmdl scale, citizen human male base model, pelvis root. Pass the committed\r\n /// SAME weight bytes as <paramref name=\"dlWeights\"/> to enable the deep-learning solver.\r\n /// </summary>\r\n public static RetargetTargetSpec SboxDefault(string targetRigJson, byte[]? dlWeights = null) => new()\r\n {\r\n Rig = TargetRig.SboxDefault(targetRigJson),\r\n VmdlScale = SboxSourceScale,\r\n BaseModelPath = SboxHumanMalePath,\r\n DefaultRootBone = \"pelvis\",\r\n DlWeights = dlWeights,\r\n };\r\n\r\n /// <summary>\r\n /// The classic (4-finger) s&box citizen target: rig parsed from the committed\r\n /// <c>Assets/humanoid_retargeter/target_rig_sbox_citizen.json</c> text (callers do the\r\n /// file IO), 0.3937 vmdl scale, citizen base model, pelvis root, Y-up cm. The rig has no\r\n /// pinky bones, so pinky roles stay unassigned \u2014 the engine's own constraints handle the\r\n /// pinky at runtime for models that have one. Pass the committed SAME weight bytes as\r\n /// <paramref name=\"dlWeights\"/> to enable the deep-learning solver.\r\n /// </summary>\r\n public static RetargetTargetSpec SboxCitizen(string targetRigJson, byte[]? dlWeights = null) => new()\r\n {\r\n Rig = TargetRig.Load(targetRigJson),\r\n VmdlScale = SboxSourceScale,\r\n BaseModelPath = SboxCitizenPath,\r\n DefaultRootBone = \"pelvis\",\r\n UpAxis = TargetUpAxis.YUpCm,\r\n DlWeights = dlWeights,\r\n };\r\n}\r\n\r\n/// <summary>Options for <see cref=\"Retargeter.ConvertBatch\"/> output assembly.</summary>\r\npublic sealed class BatchOptions\r\n{\r\n /// <summary>\r\n /// When set, the batch additionally augments this existing vmdl text (all successful\r\n /// clips spliced into its AnimationList via <see cref=\"VmdlAugmenter\"/>) and returns the\r\n /// result in <see cref=\"RetargetBatchResult.AugmentedVmdl\"/>.\r\n /// </summary>\r\n public string? AugmentVmdlText { get; init; }\r\n\r\n /// <summary>Assets-relative folder the DMX files will be written to by the caller; used\r\n /// to build each AnimFile's <c>source_filename</c>.</summary>\r\n public string DmxFolderRelative { get; init; } = \"animations/retargeted\";\r\n\r\n /// <summary>\r\n /// Animation source paths (<c>source_filename</c> values of the augment target's existing\r\n /// AnimFile nodes, assets-relative) that the IO-owning caller has determined NO LONGER\r\n /// EXIST on disk. Stale AnimFile entries referencing them are REMOVED from the augmented\r\n /// vmdl (reported on <see cref=\"RetargetBatchResult.Warnings\"/>) \u2014 one unresolvable\r\n /// source otherwise fails the ENTIRE vmdl recompile (\"Node 'X' resolve failure\"), taking\r\n /// every newly added animation down with it. Entries this batch overwrites (their DMX is\r\n /// about to be written) are never pruned. The facade itself never touches the filesystem:\r\n /// callers probe <see cref=\"Target.VmdlAugmenter.CollectAnimSourcePaths\"/> results against\r\n /// their content roots and pass the missing ones here. Null/empty = keep everything.\r\n /// </summary>\r\n public IReadOnlyCollection<string>? MissingAnimSources { get; init; }\r\n\r\n /// <summary>Auto-suffix colliding clip names (<c>_2</c>, <c>_3</c>, \u2026) across the whole\r\n /// batch (default on). When off, duplicate names are kept as-is.</summary>\r\n public bool AutoSuffixCollisions { get; init; } = true;\r\n\r\n /// <summary>\r\n /// After conversion, scan the batch's successful clip names for directional locomotion\r\n /// families (default OFF): <c>_N</c>/<c>_NE</c>/\u2026/<c>_NW</c> compass suffixes and\r\n /// <c>_Forward</c>/<c>_Backward</c>(/<c>_Back</c>)/<c>_Left</c>/<c>_Right</c> word forms\r\n /// sharing a stem. Each complete family (all four cardinals) is grouped under a Folder\r\n /// node with a <c>2DBlend</c> wired to the citizen <c>move_x</c>/<c>move_y</c> pose\r\n /// parameters, replicating the shipped citizen locomotion layout (see\r\n /// <see cref=\"Target.LocomotionSetDetector\"/>); detection results land on\r\n /// <see cref=\"RetargetBatchResult.LocomotionSets\"/>. Custom (non-citizen) base models\r\n /// must declare <c>move_x</c>/<c>move_y</c> pose parameters themselves for the blends to\r\n /// be drivable.\r\n /// </summary>\r\n public bool DetectLocomotionSets { get; init; }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_retargeter",
"Path": "HumanoidRetargeter/Solve/HandGeometry.cs",
"FileName": "HandGeometry.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 311783,
"Code": "#nullable enable annotations\r\n\r\nusing System.Collections.Generic;\r\nusing System.Numerics;\r\nusing HumanoidRetargeter.Mapping;\r\nusing HumanoidRetargeter.Maths;\r\n\r\nnamespace HumanoidRetargeter.Solve;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)\r\n\r\n/// <summary>\r\n/// Hand rest-geometry helpers shared by <see cref=\"CanonicalFrames\"/> (finger secondary axes)\r\n/// and <see cref=\"RestNormalizer\"/> (palm-down roll correction). Everything derives from joint\r\n/// positions only \u2014 bone local axes carry no anatomical meaning on the s&box rig.\r\n/// </summary>\r\ninternal static class HandGeometry\r\n{\r\n private static readonly BoneRole[] LeftProximals =\r\n {\r\n BoneRole.ThumbProxL, BoneRole.IndexProxL, BoneRole.MiddleProxL, BoneRole.RingProxL, BoneRole.PinkyProxL,\r\n };\r\n\r\n private static readonly BoneRole[] RightProximals =\r\n {\r\n BoneRole.ThumbProxR, BoneRole.IndexProxR, BoneRole.MiddleProxR, BoneRole.RingProxR, BoneRole.PinkyProxR,\r\n };\r\n\r\n // Index \u2192 pinky order; the knuckle line is taken from the first and last mapped of these.\r\n private static readonly BoneRole[] LeftNonThumbProximals =\r\n {\r\n BoneRole.IndexProxL, BoneRole.MiddleProxL, BoneRole.RingProxL, BoneRole.PinkyProxL,\r\n };\r\n\r\n private static readonly BoneRole[] RightNonThumbProximals =\r\n {\r\n BoneRole.IndexProxR, BoneRole.MiddleProxR, BoneRole.RingProxR, BoneRole.PinkyProxR,\r\n };\r\n\r\n /// <summary>\r\n /// Midpoint of all mapped finger proximal heads of one hand (the hand's anatomical\r\n /// \"chain child\" point), or null when no finger proximal is mapped.\r\n /// </summary>\r\n public static Vector3? FingerProximalMidpoint(MappingResult map, IReadOnlyList<XForm> worldRest, bool left)\r\n {\r\n var sum = Vector3.Zero;\r\n var count = 0;\r\n foreach (var role in left ? LeftProximals : RightProximals)\r\n {\r\n if (map.RoleToBone.TryGetValue(role, out var index))\r\n {\r\n sum += worldRest[index].Pos;\r\n count++;\r\n }\r\n }\r\n return count > 0 ? sum / count : null;\r\n }\r\n\r\n /// <summary>\r\n /// Dorsal palm normal of one hand: the unit vector pointing out of the <b>back</b> of the\r\n /// hand (away from the palm), or null when the hand/finger geometry is unmapped or\r\n /// degenerate.\r\n /// </summary>\r\n /// <remarks>\r\n /// Formula (mirror-consistent by construction, verified on the ActorCore fixture by the\r\n /// finger-curl test): <c>dorsal = sideSign \u00b7 cross(knuckle, fingerDir)</c> with\r\n /// <c>sideSign = +1</c> left / <c>\u22121</c> right, <c>knuckle = IndexProx.head \u2212\r\n /// PinkyProx.head</c> (first/last mapped non-thumb proximal), and <c>fingerDir =\r\n /// FingerProximalMidpoint \u2212 Hand.head</c>. On every fixture rig the thumb proximal lies on\r\n /// the \u2212dorsal (palmar) side of the hand plane, grounding the sign anatomically. A positive\r\n /// rotation about a finger frame's hinge axis (frame Y = cross(dorsal, fingerChainDir))\r\n /// curls the fingertip toward the palm on <b>both</b> hands.\r\n /// </remarks>\r\n public static Vector3? Dorsal(MappingResult map, IReadOnlyList<XForm> worldRest, bool left)\r\n {\r\n if (!map.RoleToBone.TryGetValue(left ? BoneRole.HandL : BoneRole.HandR, out var handIndex))\r\n return null;\r\n var hand = worldRest[handIndex].Pos;\r\n\r\n var nonThumb = left ? LeftNonThumbProximals : RightNonThumbProximals;\r\n Vector3? first = null, last = null;\r\n foreach (var role in nonThumb)\r\n {\r\n if (!map.RoleToBone.TryGetValue(role, out var index))\r\n continue;\r\n first ??= worldRest[index].Pos;\r\n last = worldRest[index].Pos;\r\n }\r\n if (first is null || last is null || (first.Value - last.Value).LengthSquared() < 1e-8f)\r\n return null;\r\n\r\n var midpoint = FingerProximalMidpoint(map, worldRest, left);\r\n if (midpoint is null)\r\n return null;\r\n\r\n var knuckle = first.Value - last.Value;\r\n var fingerDir = midpoint.Value - hand;\r\n var raw = Vector3.Cross(knuckle, fingerDir) * (left ? 1f : -1f);\r\n return raw.LengthSquared() < 1e-8f ? null : Vector3.Normalize(raw);\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_retargeter",
"Path": "Code/HumanoidRetargeter/AssemblyInfo.cs",
"FileName": "AssemblyInfo.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 311783,
"Code": "#nullable enable annotations\r\n\r\n// Engine-agnostic retargeting core. No Sandbox/Editor references allowed in this tree:\r\n// these sources also compile in the plain net8.0 dev harness (dev/HumanoidRetargeter.Dev.csproj).\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_retargeter",
"Path": "Code/HumanoidRetargeter/Formats/Gltf/GltfDocument.cs",
"FileName": "GltfDocument.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 311783,
"Code": "#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Numerics;\r\nusing System.Text;\r\nusing System.Text.Json;\r\n\r\nnamespace HumanoidRetargeter.Formats.Gltf;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)\r\n\r\n/// <summary>One glTF node, reduced to what skeleton import needs (TRS rest + hierarchy).</summary>\r\ninternal sealed class GltfNode\r\n{\r\n public string? Name;\r\n public int[] Children = Array.Empty<int>();\r\n public int Parent = -1;\r\n public bool HasMesh;\r\n\r\n // Rest local transform: TRS properties, or the decomposed \"matrix\" property (the spec\r\n // makes them exclusive; animated nodes must use TRS). Shear is not representable.\r\n public Vector3 Translation; // meters\r\n public Quaternion Rotation = Quaternion.Identity; // xyzw\r\n public Vector3 Scale = Vector3.One;\r\n}\r\n\r\n/// <summary>One decoded animation channel: keyframe times + values for one node property.</summary>\r\ninternal sealed class GltfChannel\r\n{\r\n public required int NodeIndex;\r\n public required bool IsRotation; // true = rotation (VEC4 quat), false = translation (VEC3)\r\n public required float[] Times; // seconds, ascending\r\n public required float[] Values; // flattened; 4 (or 3) floats per element\r\n public required string Interpolation; // LINEAR / STEP / CUBICSPLINE\r\n\r\n /// <summary>Floats per element (3 translation / 4 rotation).</summary>\r\n public int Comps => IsRotation ? 4 : 3;\r\n\r\n /// <summary>Elements stored per key: CUBICSPLINE keys carry in-tangent/value/out-tangent.</summary>\r\n public int ElementsPerKey => Interpolation == \"CUBICSPLINE\" ? 3 : 1;\r\n\r\n /// <summary>Number of keys.</summary>\r\n public int KeyCount => Times.Length;\r\n}\r\n\r\n/// <summary>One glTF animation with its decoded rotation/translation channels.</summary>\r\ninternal sealed class GltfAnimation\r\n{\r\n public string? Name;\r\n public List<GltfChannel> Channels { get; } = new();\r\n}\r\n\r\n/// <summary>\r\n/// Container + JSON layer of the glTF importer: parses a GLB binary container or a plain\r\n/// .gltf JSON document, resolves buffers (GLB BIN chunk and base64 <c>data:</c> URIs \u2014 file\r\n/// IO is banned in Code/, so external file URIs throw), and decodes nodes, skin joints and\r\n/// animation samplers into plain arrays. Throws <see cref=\"FormatException\"/> on anything\r\n/// malformed or unsupported.\r\n/// </summary>\r\ninternal sealed class GltfDocument\r\n{\r\n private const uint GlbMagic = 0x46546C67; // 'glTF' little-endian\r\n private const uint ChunkJson = 0x4E4F534A; // 'JSON'\r\n private const uint ChunkBin = 0x004E4942; // 'BIN\\0'\r\n\r\n /// <summary>All nodes, indexed as in the file, with parents resolved from children lists.</summary>\r\n public List<GltfNode> Nodes { get; } = new();\r\n\r\n /// <summary>Union of all skins' joint node indices.</summary>\r\n public HashSet<int> SkinJoints { get; } = new();\r\n\r\n /// <summary>All animations with decoded rotation/translation channels (scale/weights ignored).</summary>\r\n public List<GltfAnimation> Animations { get; } = new();\r\n\r\n /// <summary>\r\n /// The VRM humanoid bone map authored in the file, when present: VRM bone name\r\n /// (<c>hips</c>, <c>leftUpperArm</c>, \u2026) \u2192 node index. Read from BOTH extension layouts:\r\n /// VRM 0.x <c>extensions.VRM.humanoid.humanBones</c> (an ARRAY of\r\n /// <c>{ \"bone\": \"hips\", \"node\": 14 }</c> entries) and VRM 1.0\r\n /// <c>extensions.VRMC_vrm.humanoid.humanBones</c> (an OBJECT\r\n /// <c>{ \"hips\": { \"node\": 14 }, \u2026 }</c>). Null when the file carries neither.\r\n /// </summary>\r\n public Dictionary<string, int>? VrmHumanBones { get; private set; }\r\n\r\n /// <summary>Which VRM extension supplied <see cref=\"VrmHumanBones\"/>: <c>0</c> for the\r\n /// 0.x <c>VRM</c> extension, <c>1</c> for the 1.0 <c>VRMC_vrm</c> extension, <c>-1</c>\r\n /// when none.</summary>\r\n public int VrmVersion { get; private set; } = -1;\r\n\r\n private GltfDocument()\r\n {\r\n }\r\n\r\n /// <summary>Parses GLB or plain-JSON glTF bytes.</summary>\r\n /// <exception cref=\"FormatException\">Truncated/malformed container, invalid JSON,\r\n /// unresolvable buffers, or unsupported accessor layouts.</exception>\r\n public static GltfDocument Parse(byte[] data)\r\n {\r\n ArgumentNullException.ThrowIfNull(data);\r\n\r\n byte[] json;\r\n byte[]? bin = null;\r\n if (data.Length >= 4 && ReadU32(data, 0) == GlbMagic)\r\n (json, bin) = ParseGlbContainer(data);\r\n else\r\n json = data;\r\n\r\n JsonElement root;\r\n try\r\n {\r\n // Parse via string: Memory<T>/ReadOnlyMemory<T> are not on the s&box runtime\r\n // whitelist (SB1000), and the string path also lets us strip a UTF-8 BOM\r\n // (Utf8JsonReader rejects raw BOM bytes). Clone detaches from the disposed\r\n // JsonDocument.\r\n var text = System.Text.Encoding.UTF8.GetString(json).TrimStart('\\uFEFF');\r\n using var doc = JsonDocument.Parse(text);\r\n root = doc.RootElement.Clone();\r\n }\r\n catch (JsonException e)\r\n {\r\n throw new FormatException($\"glTF: invalid JSON ({e.Message})\");\r\n }\r\n\r\n if (root.ValueKind != JsonValueKind.Object || !root.TryGetProperty(\"asset\", out _))\r\n throw new FormatException(\"glTF: missing required 'asset' object (not a glTF file?).\");\r\n\r\n var document = new GltfDocument();\r\n var buffers = ResolveBuffers(root, bin);\r\n document.ReadNodes(root);\r\n document.ReadSkins(root);\r\n document.ReadAnimations(root, buffers);\r\n document.ReadVrmHumanoid(root);\r\n return document;\r\n }\r\n\r\n // ================================================================== VRM humanoid\r\n\r\n /// <summary>\r\n /// Reads the authored humanoid bone map of a VRM file (a .vrm is a regular glTF 2.0/GLB\r\n /// container plus a VRM extension). VRM 1.0's <c>VRMC_vrm</c> wins when both extensions\r\n /// are present. Defensive throughout: malformed entries and out-of-range node indices\r\n /// are skipped (a broken bone map degrades to the regular detection cascade rather than\r\n /// failing the import).\r\n /// </summary>\r\n private void ReadVrmHumanoid(JsonElement root)\r\n {\r\n if (!root.TryGetProperty(\"extensions\", out var extensions)\r\n || extensions.ValueKind != JsonValueKind.Object)\r\n return;\r\n\r\n // ---- VRM 1.0: extensions.VRMC_vrm.humanoid.humanBones = { \"<bone>\": { \"node\": n } } ----\r\n if (TryGetHumanBones(extensions, \"VRMC_vrm\", out var humanBones1)\r\n && humanBones1.ValueKind == JsonValueKind.Object)\r\n {\r\n var map = new Dictionary<string, int>(StringComparer.Ordinal);\r\n foreach (var property in humanBones1.EnumerateObject())\r\n {\r\n if (property.Value.ValueKind == JsonValueKind.Object\r\n && property.Value.TryGetProperty(\"node\", out var node)\r\n && node.ValueKind == JsonValueKind.Number\r\n && node.TryGetInt32(out var index)\r\n && index >= 0 && index < Nodes.Count)\r\n {\r\n map[property.Name] = index;\r\n }\r\n }\r\n if (map.Count > 0)\r\n {\r\n VrmHumanBones = map;\r\n VrmVersion = 1;\r\n return;\r\n }\r\n }\r\n\r\n // ---- VRM 0.x: extensions.VRM.humanoid.humanBones = [ { \"bone\": \"...\", \"node\": n } ] ----\r\n if (TryGetHumanBones(extensions, \"VRM\", out var humanBones0)\r\n && humanBones0.ValueKind == JsonValueKind.Array)\r\n {\r\n var map = new Dictionary<string, int>(StringComparer.Ordinal);\r\n foreach (var entry in humanBones0.EnumerateArray())\r\n {\r\n if (entry.ValueKind == JsonValueKind.Object\r\n && entry.TryGetProperty(\"bone\", out var bone)\r\n && bone.ValueKind == JsonValueKind.String\r\n && entry.TryGetProperty(\"node\", out var node)\r\n && node.ValueKind == JsonValueKind.Number\r\n && node.TryGetInt32(out var index)\r\n && index >= 0 && index < Nodes.Count)\r\n {\r\n map[bone.GetString()!] = index;\r\n }\r\n }\r\n if (map.Count > 0)\r\n {\r\n VrmHumanBones = map;\r\n VrmVersion = 0;\r\n }\r\n }\r\n }\r\n\r\n private static bool TryGetHumanBones(JsonElement extensions, string extensionName, out JsonElement humanBones)\r\n {\r\n humanBones = default;\r\n return extensions.TryGetProperty(extensionName, out var vrm)\r\n && vrm.ValueKind == JsonValueKind.Object\r\n && vrm.TryGetProperty(\"humanoid\", out var humanoid)\r\n && humanoid.ValueKind == JsonValueKind.Object\r\n && humanoid.TryGetProperty(\"humanBones\", out humanBones);\r\n }\r\n\r\n // ================================================================== GLB container\r\n\r\n /// <summary>GLB layout: 12-byte header (magic 'glTF', u32 version = 2, u32 length),\r\n /// then chunks of (u32 length, u32 type, bytes): one JSON chunk, optionally one BIN.</summary>\r\n private static (byte[] Json, byte[]? Bin) ParseGlbContainer(byte[] data)\r\n {\r\n if (data.Length < 12)\r\n throw new FormatException(\"GLB: truncated header (need 12 bytes).\");\r\n\r\n uint version = ReadU32(data, 4);\r\n if (version != 2)\r\n throw new FormatException($\"GLB: unsupported container version {version} (expected 2).\");\r\n\r\n long declared = ReadU32(data, 8);\r\n if (declared > data.Length)\r\n throw new FormatException(\r\n $\"GLB: truncated file (header declares {declared} bytes, got {data.Length}).\");\r\n\r\n byte[]? json = null, bin = null;\r\n long offset = 12;\r\n while (offset + 8 <= declared)\r\n {\r\n long length = ReadU32(data, (int)offset);\r\n uint type = ReadU32(data, (int)offset + 4);\r\n offset += 8;\r\n if (offset + length > data.Length)\r\n throw new FormatException(\"GLB: truncated chunk (declared length exceeds the file).\");\r\n\r\n if (type == ChunkJson && json is null)\r\n json = data.AsSpan((int)offset, (int)length).ToArray();\r\n else if (type == ChunkBin && bin is null)\r\n bin = data.AsSpan((int)offset, (int)length).ToArray();\r\n // Unknown chunk types are skipped per spec.\r\n\r\n offset += length + (length % 4 == 0 ? 0 : 4 - length % 4); // chunks are 4-aligned\r\n }\r\n\r\n if (json is null)\r\n throw new FormatException(\"GLB: no JSON chunk found.\");\r\n return (json, bin);\r\n }\r\n\r\n private static uint ReadU32(byte[] data, int offset)\r\n => (uint)(data[offset] | data[offset + 1] << 8 | data[offset + 2] << 16 | data[offset + 3] << 24);\r\n\r\n // ================================================================== buffers\r\n\r\n /// <summary>\r\n /// Resolves every entry of <c>buffers</c>: no <c>uri</c> = the GLB BIN chunk (spec: only\r\n /// buffer 0 may do this), <c>data:</c> URIs are base64-decoded inline. External file\r\n /// URIs are NOT supported \u2014 this library does no file IO; users should export .glb.\r\n /// </summary>\r\n private static List<byte[]> ResolveBuffers(JsonElement root, byte[]? bin)\r\n {\r\n var buffers = new List<byte[]>();\r\n if (!root.TryGetProperty(\"buffers\", out var array) || array.ValueKind != JsonValueKind.Array)\r\n return buffers;\r\n\r\n foreach (var buffer in array.EnumerateArray())\r\n {\r\n if (!buffer.TryGetProperty(\"uri\", out var uriProp))\r\n {\r\n buffers.Add(bin ?? throw new FormatException(\r\n \"glTF: buffer has no uri but the file has no GLB BIN chunk.\"));\r\n continue;\r\n }\r\n\r\n var uri = uriProp.GetString() ?? \"\";\r\n if (uri.StartsWith(\"data:\", StringComparison.OrdinalIgnoreCase))\r\n {\r\n int comma = uri.IndexOf(',');\r\n if (comma < 0 || !uri[..comma].EndsWith(\";base64\", StringComparison.OrdinalIgnoreCase))\r\n throw new FormatException(\"glTF: only base64 data: URIs are supported for buffers.\");\r\n try\r\n {\r\n buffers.Add(Convert.FromBase64String(uri[(comma + 1)..]));\r\n }\r\n catch (Exception e) when (e is FormatException or ArgumentException)\r\n {\r\n throw new FormatException(\"glTF: invalid base64 in buffer data: URI.\");\r\n }\r\n }\r\n else\r\n {\r\n throw new FormatException(\r\n $\"glTF: buffer references an external file ('{uri}') which this importer cannot \"\r\n + \"read (no file IO). Export as .glb (binary, self-contained) instead.\");\r\n }\r\n }\r\n return buffers;\r\n }\r\n\r\n // ================================================================== nodes + skins\r\n\r\n private void ReadNodes(JsonElement root)\r\n {\r\n if (!root.TryGetProperty(\"nodes\", out var array) || array.ValueKind != JsonValueKind.Array)\r\n return;\r\n\r\n Span<float> m = stackalloc float[16]; // matrix scratch (outside the loop: CA2014)\r\n foreach (var n in array.EnumerateArray())\r\n {\r\n var node = new GltfNode\r\n {\r\n Name = n.TryGetProperty(\"name\", out var name) ? name.GetString() : null,\r\n HasMesh = n.TryGetProperty(\"mesh\", out _),\r\n };\r\n\r\n if (n.TryGetProperty(\"children\", out var children) && children.ValueKind == JsonValueKind.Array)\r\n {\r\n var list = new List<int>();\r\n foreach (var c in children.EnumerateArray())\r\n list.Add(c.GetInt32());\r\n node.Children = list.ToArray();\r\n }\r\n\r\n if (n.TryGetProperty(\"matrix\", out var matrix) && matrix.ValueKind == JsonValueKind.Array)\r\n {\r\n // Column-major 16 floats; the element order maps 1:1 onto System.Numerics'\r\n // row-vector matrices (translation in elements 12..14 either way).\r\n int i = 0;\r\n foreach (var v in matrix.EnumerateArray())\r\n {\r\n if (i >= 16)\r\n break;\r\n m[i++] = v.GetSingle();\r\n }\r\n if (i < 16)\r\n throw new FormatException(\"glTF: node matrix has fewer than 16 elements.\");\r\n var local = new Matrix4x4(\r\n m[0], m[1], m[2], m[3],\r\n m[4], m[5], m[6], m[7],\r\n m[8], m[9], m[10], m[11],\r\n m[12], m[13], m[14], m[15]);\r\n if (Matrix4x4.Decompose(local, out var scale, out var rot, out var pos))\r\n {\r\n node.Translation = pos;\r\n node.Rotation = rot;\r\n node.Scale = scale;\r\n }\r\n else\r\n {\r\n node.Translation = local.Translation; // degenerate: keep position at least\r\n }\r\n }\r\n else\r\n {\r\n node.Translation = ReadVec3(n, \"translation\", Vector3.Zero);\r\n node.Scale = ReadVec3(n, \"scale\", Vector3.One);\r\n if (n.TryGetProperty(\"rotation\", out var r) && r.ValueKind == JsonValueKind.Array\r\n && r.GetArrayLength() >= 4)\r\n {\r\n node.Rotation = new Quaternion(\r\n r[0].GetSingle(), r[1].GetSingle(), r[2].GetSingle(), r[3].GetSingle());\r\n }\r\n }\r\n\r\n Nodes.Add(node);\r\n }\r\n\r\n // Resolve parents (per spec a node is referenced by at most one other node's children).\r\n for (int i = 0; i < Nodes.Count; i++)\r\n {\r\n foreach (var child in Nodes[i].Children)\r\n {\r\n if (child < 0 || child >= Nodes.Count)\r\n throw new FormatException($\"glTF: node {i} references nonexistent child {child}.\");\r\n if (Nodes[child].Parent < 0)\r\n Nodes[child].Parent = i;\r\n }\r\n }\r\n }\r\n\r\n private static Vector3 ReadVec3(JsonElement element, string property, Vector3 fallback)\r\n {\r\n if (!element.TryGetProperty(property, out var v) || v.ValueKind != JsonValueKind.Array\r\n || v.GetArrayLength() < 3)\r\n return fallback;\r\n return new Vector3(v[0].GetSingle(), v[1].GetSingle(), v[2].GetSingle());\r\n }\r\n\r\n private void ReadSkins(JsonElement root)\r\n {\r\n if (!root.TryGetProperty(\"skins\", out var array) || array.ValueKind != JsonValueKind.Array)\r\n return;\r\n\r\n foreach (var skin in array.EnumerateArray())\r\n {\r\n if (!skin.TryGetProperty(\"joints\", out var joints) || joints.ValueKind != JsonValueKind.Array)\r\n continue;\r\n foreach (var j in joints.EnumerateArray())\r\n {\r\n int index = j.GetInt32();\r\n if (index >= 0 && index < Nodes.Count)\r\n SkinJoints.Add(index);\r\n }\r\n }\r\n }\r\n\r\n // ================================================================== animations\r\n\r\n private void ReadAnimations(JsonElement root, List<byte[]> buffers)\r\n {\r\n if (!root.TryGetProperty(\"animations\", out var array) || array.ValueKind != JsonValueKind.Array)\r\n return;\r\n\r\n root.TryGetProperty(\"accessors\", out var accessors);\r\n root.TryGetProperty(\"bufferViews\", out var views);\r\n\r\n foreach (var a in array.EnumerateArray())\r\n {\r\n var animation = new GltfAnimation\r\n {\r\n Name = a.TryGetProperty(\"name\", out var name) ? name.GetString() : null,\r\n };\r\n\r\n if (!a.TryGetProperty(\"channels\", out var channels) || !a.TryGetProperty(\"samplers\", out var samplers))\r\n {\r\n Animations.Add(animation);\r\n continue;\r\n }\r\n\r\n foreach (var channel in channels.EnumerateArray())\r\n {\r\n if (!channel.TryGetProperty(\"target\", out var target)\r\n || !target.TryGetProperty(\"node\", out var nodeProp)\r\n || !target.TryGetProperty(\"path\", out var pathProp))\r\n continue; // extension targets (e.g. KHR_animation_pointer) are ignored\r\n\r\n var path = pathProp.GetString();\r\n if (path is not (\"rotation\" or \"translation\"))\r\n continue; // scale / weights channels are ignored by design\r\n\r\n int node = nodeProp.GetInt32();\r\n if (node < 0 || node >= Nodes.Count)\r\n continue;\r\n\r\n int samplerIndex = channel.TryGetProperty(\"sampler\", out var s) ? s.GetInt32() : -1;\r\n if (samplerIndex < 0 || samplerIndex >= samplers.GetArrayLength())\r\n throw new FormatException(\"glTF: animation channel references a nonexistent sampler.\");\r\n var sampler = samplers[samplerIndex];\r\n\r\n var interpolation = sampler.TryGetProperty(\"interpolation\", out var interp)\r\n ? interp.GetString() ?? \"LINEAR\"\r\n : \"LINEAR\";\r\n\r\n bool isRotation = path == \"rotation\";\r\n int comps = isRotation ? 4 : 3;\r\n\r\n var times = ReadAccessor(accessors, views, buffers,\r\n RequiredInt(sampler, \"input\", \"animation sampler\"), 1, normalizedAllowed: false);\r\n var values = ReadAccessor(accessors, views, buffers,\r\n RequiredInt(sampler, \"output\", \"animation sampler\"), comps, normalizedAllowed: isRotation);\r\n\r\n int elementsPerKey = interpolation == \"CUBICSPLINE\" ? 3 : 1;\r\n if (times.Length == 0 || values.Length < times.Length * elementsPerKey * comps)\r\n continue; // empty or under-filled sampler: nothing usable\r\n\r\n animation.Channels.Add(new GltfChannel\r\n {\r\n NodeIndex = node,\r\n IsRotation = isRotation,\r\n Times = times,\r\n Values = values,\r\n Interpolation = interpolation,\r\n });\r\n }\r\n\r\n Animations.Add(animation);\r\n }\r\n }\r\n\r\n private static int RequiredInt(JsonElement element, string property, string context)\r\n {\r\n if (!element.TryGetProperty(property, out var v))\r\n throw new FormatException($\"glTF: {context} is missing '{property}'.\");\r\n return v.GetInt32();\r\n }\r\n\r\n // ================================================================== accessors\r\n\r\n /// <summary>\r\n /// Decodes an accessor to floats. Component types: f32 directly; normalized i8/u8/i16/u16\r\n /// per the spec's normalization rules when <paramref name=\"normalizedAllowed\"/> (rotation\r\n /// outputs); anything else throws. Honors accessor/bufferView byte offsets and an\r\n /// explicit byteStride. Sparse accessors are not supported.\r\n /// </summary>\r\n private static float[] ReadAccessor(\r\n JsonElement accessors, JsonElement views, List<byte[]> buffers,\r\n int accessorIndex, int expectedComps, bool normalizedAllowed)\r\n {\r\n if (accessors.ValueKind != JsonValueKind.Array || accessorIndex < 0\r\n || accessorIndex >= accessors.GetArrayLength())\r\n throw new FormatException($\"glTF: accessor {accessorIndex} does not exist.\");\r\n var accessor = accessors[accessorIndex];\r\n\r\n if (accessor.TryGetProperty(\"sparse\", out _))\r\n throw new FormatException(\"glTF: sparse accessors are not supported.\");\r\n\r\n var type = accessor.TryGetProperty(\"type\", out var t) ? t.GetString() : null;\r\n int comps = type switch\r\n {\r\n \"SCALAR\" => 1,\r\n \"VEC3\" => 3,\r\n \"VEC4\" => 4,\r\n _ => throw new FormatException($\"glTF: unsupported accessor type '{type}'.\"),\r\n };\r\n if (comps != expectedComps)\r\n throw new FormatException(\r\n $\"glTF: accessor {accessorIndex} is {type}, expected {expectedComps} component(s).\");\r\n\r\n int count = RequiredInt(accessor, \"count\", \"accessor\");\r\n int componentType = RequiredInt(accessor, \"componentType\", \"accessor\");\r\n bool normalized = accessor.TryGetProperty(\"normalized\", out var n) && n.GetBoolean();\r\n\r\n // The count is attacker-controlled: validate it BEFORE any allocation sized by it.\r\n // Negative would throw OverflowException from the array allocation (breaking the\r\n // FormatException malformed-file contract); huge would OOM; count * comps can wrap.\r\n if (count < 0)\r\n throw new FormatException($\"glTF: accessor {accessorIndex} has a negative count ({count}).\");\r\n\r\n int compSize = componentType switch\r\n {\r\n 5126 => 4, // FLOAT\r\n 5120 or 5121 => 1, // BYTE / UNSIGNED_BYTE\r\n 5122 or 5123 => 2, // SHORT / UNSIGNED_SHORT\r\n _ => throw new FormatException(\r\n $\"glTF: unsupported accessor componentType {componentType}.\"),\r\n };\r\n if (componentType != 5126 && !(normalized && normalizedAllowed))\r\n throw new FormatException(\r\n $\"glTF: accessor {accessorIndex} must be float (or a normalized integer \"\r\n + \"rotation output).\");\r\n\r\n int elementSize = comps * compSize;\r\n\r\n if (!accessor.TryGetProperty(\"bufferView\", out var viewIndexProp))\r\n {\r\n // Zero-filled when no bufferView (legal per spec) \u2014 but then nothing backs the\r\n // count, so cap it by the file's total decoded buffer bytes (a real file's\r\n // accessors never outgrow its payload; a small floor keeps tiny legitimate\r\n // zero-filled accessors working in buffer-less documents).\r\n long totalBufferBytes = 0;\r\n foreach (var b in buffers)\r\n totalBufferBytes += b.Length;\r\n long capacity = Math.Min(\r\n Math.Max(totalBufferBytes / elementSize, 65536),\r\n int.MaxValue / comps); // keeps count * comps int-representable\r\n if (count > capacity)\r\n throw new FormatException(\r\n $\"glTF: accessor {accessorIndex} count {count} exceeds what the file's \"\r\n + \"buffers could back (malformed or hostile file).\");\r\n return new float[checked(count * comps)];\r\n }\r\n\r\n int viewIndex = viewIndexProp.GetInt32();\r\n if (views.ValueKind != JsonValueKind.Array || viewIndex < 0 || viewIndex >= views.GetArrayLength())\r\n throw new FormatException($\"glTF: bufferView {viewIndex} does not exist.\");\r\n var view = views[viewIndex];\r\n\r\n int bufferIndex = RequiredInt(view, \"buffer\", \"bufferView\");\r\n if (bufferIndex < 0 || bufferIndex >= buffers.Count)\r\n throw new FormatException($\"glTF: buffer {bufferIndex} does not exist.\");\r\n var buffer = buffers[bufferIndex];\r\n\r\n int viewOffset = view.TryGetProperty(\"byteOffset\", out var vo) ? vo.GetInt32() : 0;\r\n int accessorOffset = accessor.TryGetProperty(\"byteOffset\", out var ao) ? ao.GetInt32() : 0;\r\n int stride = view.TryGetProperty(\"byteStride\", out var st) ? st.GetInt32() : elementSize;\r\n if (stride < elementSize)\r\n throw new FormatException(\"glTF: bufferView byteStride is smaller than the element size.\");\r\n\r\n // Bounds check in long arithmetic BEFORE allocating: the backing range must fit the\r\n // buffer, which also caps count at buffer.Length / stride (+1) \u2014 so the allocation\r\n // below is bounded by the actual file size and checked() can no longer overflow.\r\n long start = (long)viewOffset + accessorOffset;\r\n long end = start + (long)(count - 1) * stride + elementSize;\r\n if (count > 0 && (start < 0 || end > buffer.Length))\r\n throw new FormatException(\r\n $\"glTF: accessor {accessorIndex} reads past the end of its buffer (truncated file?).\");\r\n\r\n var result = new float[checked(count * comps)];\r\n for (int element = 0; element < count; element++)\r\n {\r\n int offset = (int)(start + (long)element * stride);\r\n for (int c = 0; c < comps; c++)\r\n {\r\n int at = offset + c * compSize;\r\n result[element * comps + c] = componentType switch\r\n {\r\n 5126 => BitConverter.ToSingle(buffer, at),\r\n 5120 => MathF.Max((sbyte)buffer[at] / 127f, -1f),\r\n 5121 => buffer[at] / 255f,\r\n 5122 => MathF.Max(BitConverter.ToInt16(buffer, at) / 32767f, -1f),\r\n _ => BitConverter.ToUInt16(buffer, at) / 65535f,\r\n };\r\n }\r\n }\r\n return result;\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_retargeter",
"Path": "Code/HumanoidRetargeter/Maths/XForm.cs",
"FileName": "XForm.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 311783,
"Code": "#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Numerics;\r\n\r\nnamespace HumanoidRetargeter.Maths;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)\r\n\r\n/// <summary>\r\n/// A rigid transform: rotation followed by translation (no scale or shear).\r\n/// </summary>\r\n/// <remarks>\r\n/// Project conventions (fixed for the whole library):\r\n/// <list type=\"bullet\">\r\n/// <item>Positions are centimeters.</item>\r\n/// <item>Quaternions are XYZW unit quaternions (<see cref=\"System.Numerics.Quaternion\"/> native layout).</item>\r\n/// <item>Column-vector convention: a local-space point maps to outer space as\r\n/// <c>p' = rotate(Rot, p) + Pos</c>, and <c>a * b</c> on quaternions applies <c>b</c> first.</item>\r\n/// </list>\r\n/// </remarks>\r\npublic struct XForm : IEquatable<XForm>\r\n{\r\n /// <summary>Translation component, in centimeters.</summary>\r\n public Vector3 Pos;\r\n\r\n /// <summary>Rotation component, XYZW unit quaternion.</summary>\r\n public Quaternion Rot;\r\n\r\n /// <summary>Creates a transform from a translation and a rotation.</summary>\r\n public XForm(Vector3 pos, Quaternion rot)\r\n {\r\n Pos = pos;\r\n Rot = rot;\r\n }\r\n\r\n /// <summary>The identity transform (zero translation, identity rotation).</summary>\r\n public static XForm Identity => new(Vector3.Zero, Quaternion.Identity);\r\n\r\n /// <summary>\r\n /// Composes a parent transform with a child-local transform, producing the child's\r\n /// transform in the parent's outer space (world = parent \u2218 local):\r\n /// <c>pos = parent.Pos + rotate(parent.Rot, local.Pos)</c>, <c>rot = parent.Rot * local.Rot</c>.\r\n /// The resulting rotation is re-normalized to suppress floating-point drift.\r\n /// </summary>\r\n public static XForm Compose(in XForm parent, in XForm local)\r\n => new(\r\n parent.Pos + Vector3.Transform(local.Pos, parent.Rot),\r\n MathQ.Normalize(parent.Rot * local.Rot));\r\n\r\n /// <summary>\r\n /// Returns the inverse transform, such that <c>Compose(x, x.Inverse())</c> and\r\n /// <c>Compose(x.Inverse(), x)</c> are both identity.\r\n /// </summary>\r\n public readonly XForm Inverse()\r\n {\r\n var invRot = Quaternion.Conjugate(MathQ.Normalize(Rot));\r\n return new XForm(-Vector3.Transform(Pos, invRot), invRot);\r\n }\r\n\r\n /// <summary>\r\n /// Re-expresses a world transform relative to a parent world transform; the inverse of\r\n /// <see cref=\"Compose\"/>: <c>ToLocal(p, Compose(p, l)) == l</c>.\r\n /// </summary>\r\n public static XForm ToLocal(in XForm parentWorld, in XForm world)\r\n => Compose(parentWorld.Inverse(), world);\r\n\r\n /// <summary>Transforms a point from this transform's local space to its outer space.</summary>\r\n public readonly Vector3 TransformPoint(Vector3 point) => Pos + Vector3.Transform(point, Rot);\r\n\r\n /// <summary>Rotates a direction vector by this transform's rotation (translation ignored).</summary>\r\n public readonly Vector3 TransformVector(Vector3 vector) => Vector3.Transform(vector, Rot);\r\n\r\n /// <inheritdoc />\r\n public readonly bool Equals(XForm other) => Pos.Equals(other.Pos) && Rot.Equals(other.Rot);\r\n\r\n /// <inheritdoc />\r\n public override readonly bool Equals(object? obj) => obj is XForm other && Equals(other);\r\n\r\n /// <inheritdoc />\r\n public override readonly int GetHashCode() => HashCode.Combine(Pos, Rot);\r\n\r\n /// <summary>Componentwise equality (no tolerance).</summary>\r\n public static bool operator ==(XForm left, XForm right) => left.Equals(right);\r\n\r\n /// <summary>Componentwise inequality (no tolerance).</summary>\r\n public static bool operator !=(XForm left, XForm right) => !left.Equals(right);\r\n\r\n /// <inheritdoc />\r\n public override readonly string ToString() => $\"XForm(Pos={Pos}, Rot={Rot})\";\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_retargeter",
"Path": "Code/HumanoidRetargeter/Solve/CanonicalFrames.cs",
"FileName": "CanonicalFrames.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 311783,
"Code": "#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Numerics;\r\nusing HumanoidRetargeter.Mapping;\r\nusing HumanoidRetargeter.Maths;\r\nusing SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;\r\n\r\nnamespace HumanoidRetargeter.Solve;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)\r\n\r\n/// <summary>\r\n/// Canonical anatomical frames: one world-space rest basis per mapped <see cref=\"BoneRole\"/>,\r\n/// derived from rest <b>geometry</b> (joint head positions) of any rig plus its mapping.\r\n/// Built with the same deterministic convention on source and target, so world-rotation deltas\r\n/// conjugated through these frames transfer between rigs with different bone local axes\r\n/// (the s&box Citizen rig's local axes encode no anatomy \u2014 bone-Y points chest-forward).\r\n/// </summary>\r\n/// <remarks>\r\n/// <para><b>Frame convention</b> \u2014 for each role the frame quaternion <c>F</c> rotates unit\r\n/// axes onto: <c>X = P</c> (primary), <c>Z</c> = the secondary hint <c>S</c> orthonormalized\r\n/// against <c>P</c>, <c>Y = cross(Z, X)</c> (right-handed; for fingers Y is the curl hinge).</para>\r\n/// <para><b>Primary axis P</b> = normalize(chain-child head \u2212 bone head), where the chain\r\n/// child is the next <i>mapped</i> role down the bone's anatomical chain\r\n/// (Hips\u2192Spine0..4\u2192Neck\u2192Head; Clavicle\u2192UpperArm\u2192LowerArm\u2192Hand; UpperLeg\u2192LowerLeg\u2192Foot\u2192Toe;\r\n/// per-finger Meta\u2192Prox\u2192Mid\u2192Dist). Tips: the Head inherits its previous chain segment\r\n/// (neck\u2192head \u2014 the skull-base axis, real anatomy; measured 0\u201327\u00b0 forward of character up\r\n/// across neutral-rest rigs), falling back to a virtual character-up extension only when\r\n/// that segment is absent or degenerate; Hand points at the midpoint of its mapped finger\r\n/// proximals (else along the forearm); Foot without a toe and Toe extend along character\r\n/// forward; finger distals extend along their previous segment. Other bones with nothing\r\n/// mapped below inherit the previous chain segment's direction.</para>\r\n/// <para><b>Secondary axis S</b> by bone class: spine/neck/head/hips and legs use character\r\n/// forward (knee hinge lateral); clavicle/arms/hands use <c>cross(P, characterUp)</c>\r\n/// (elbow hinge \u22a5 limb in the character's horizontal plane at T-pose), falling back to\r\n/// character forward when P is vertical; feet/toes use character up; fingers use the hand's\r\n/// dorsal palm normal (see <see cref=\"HandGeometry.Dorsal\"/>) so a positive rotation about\r\n/// frame Y curls fingertips toward the palm on both hands.</para>\r\n/// <para>When used by the solver, build the frames on the <see cref=\"RestNormalizer\"/>-\r\n/// normalized rest via <see cref=\"Build(SkeletonModel, MappingResult, IReadOnlyList{XForm})\"/>;\r\n/// this class itself just measures whatever rest it is given.</para>\r\n/// </remarks>\r\npublic sealed class CanonicalFrames\r\n{\r\n private readonly Dictionary<BoneRole, Quaternion> _frames;\r\n private readonly HashSet<BoneRole> _virtualPrimary;\r\n\r\n /// <summary>Character forward (the direction the toes point at rest), unit length.</summary>\r\n public Vector3 CharacterForward { get; }\r\n\r\n /// <summary>Character up (hips toward shoulders at rest), unit length.</summary>\r\n public Vector3 CharacterUp { get; }\r\n\r\n /// <summary>Rest hip height above the lowest foot/toe point, along character up, cm.</summary>\r\n public float HipHeight { get; }\r\n\r\n private CanonicalFrames(\r\n Dictionary<BoneRole, Quaternion> frames, HashSet<BoneRole> virtualPrimary,\r\n Vector3 forward, Vector3 up, float hipHeight)\r\n {\r\n _frames = frames;\r\n _virtualPrimary = virtualPrimary;\r\n CharacterForward = forward;\r\n CharacterUp = up;\r\n HipHeight = hipHeight;\r\n }\r\n\r\n /// <summary>True when a canonical frame exists for <paramref name=\"role\"/> (the role is\r\n /// mapped and its chain geometry is resolvable).</summary>\r\n public bool Has(BoneRole role) => _frames.ContainsKey(role);\r\n\r\n /// <summary>\r\n /// True when the role's primary axis is a <b>virtual</b> character-axis extension rather\r\n /// than real joint geometry (e.g. a Foot with no mapped Toe extends along character\r\n /// forward; mapped Toes extend along character forward by convention; a Head whose\r\n /// neck\u2192head segment is degenerate extends along character up). Absolute direction\r\n /// matching against a virtual primary imposes an arbitrary direction, so the solver\r\n /// falls back to delta transfer when the source is virtual but the target is real\r\n /// (see <see cref=\"GeometricSolver\"/> remarks).\r\n /// </summary>\r\n public bool HasVirtualPrimary(BoneRole role) => _virtualPrimary.Contains(role);\r\n\r\n /// <summary>The world-space canonical rest frame of <paramref name=\"role\"/>.</summary>\r\n /// <exception cref=\"InvalidOperationException\">Thrown when <see cref=\"Has\"/> is false for\r\n /// the role.</exception>\r\n public Quaternion WorldFrameOf(BoneRole role)\r\n => _frames.TryGetValue(role, out var frame)\r\n ? frame\r\n : throw new InvalidOperationException($\"No canonical frame for role {role} (not mapped or unresolvable).\");\r\n\r\n /// <summary>Builds frames from the skeleton's bind rest (<c>skeleton.RestWorld</c>).</summary>\r\n public static CanonicalFrames Build(SkeletonModel skeleton, MappingResult map)\r\n => Build(skeleton, map, (skeleton ?? throw new ArgumentNullException(nameof(skeleton))).RestWorld);\r\n\r\n /// <summary>\r\n /// Builds frames from explicit rest world transforms (e.g. a <see cref=\"RestPose\"/>\r\n /// produced by <see cref=\"RestNormalizer\"/>), indexed like <c>skeleton.Bones</c>.\r\n /// </summary>\r\n public static CanonicalFrames Build(\r\n SkeletonModel skeleton, MappingResult map, IReadOnlyList<XForm> worldRest)\r\n {\r\n ArgumentNullException.ThrowIfNull(skeleton);\r\n ArgumentNullException.ThrowIfNull(map);\r\n ArgumentNullException.ThrowIfNull(worldRest);\r\n if (worldRest.Count != skeleton.Count)\r\n throw new ArgumentException(\r\n $\"worldRest has {worldRest.Count} entries for a {skeleton.Count}-bone skeleton.\");\r\n\r\n var cf = CharacterFrame.Compute(skeleton, map, worldRest);\r\n var frames = new Dictionary<BoneRole, Quaternion>();\r\n var virtualPrimary = new HashSet<BoneRole>();\r\n\r\n foreach (var (chain, kind, left) in Chains())\r\n BuildChainFrames(chain, kind, left, map, worldRest, cf, frames, virtualPrimary);\r\n\r\n return new CanonicalFrames(frames, virtualPrimary, cf.Forward, cf.Up, cf.HipHeight);\r\n }\r\n\r\n // ---------------------------------------------------------------- chain construction\r\n\r\n private enum ChainKind\r\n {\r\n Body,\r\n Arm,\r\n Leg,\r\n Finger,\r\n }\r\n\r\n private static IEnumerable<(BoneRole[] Chain, ChainKind Kind, bool Left)> Chains()\r\n {\r\n yield return (new[]\r\n {\r\n BoneRole.Hips, BoneRole.Spine0, BoneRole.Spine1, BoneRole.Spine2, BoneRole.Spine3,\r\n BoneRole.Spine4, BoneRole.Neck, BoneRole.Head,\r\n }, ChainKind.Body, false);\r\n\r\n foreach (var left in new[] { true, false })\r\n {\r\n var s = left ? \"L\" : \"R\";\r\n yield return (new[]\r\n {\r\n Role(\"Clavicle\", s), Role(\"UpperArm\", s), Role(\"LowerArm\", s), Role(\"Hand\", s),\r\n }, ChainKind.Arm, left);\r\n yield return (new[]\r\n {\r\n Role(\"UpperLeg\", s), Role(\"LowerLeg\", s), Role(\"Foot\", s), Role(\"Toe\", s),\r\n }, ChainKind.Leg, left);\r\n\r\n foreach (var finger in new[] { \"Thumb\", \"Index\", \"Middle\", \"Ring\", \"Pinky\" })\r\n {\r\n yield return (new[]\r\n {\r\n Role(finger + \"Meta\", s), Role(finger + \"Prox\", s),\r\n Role(finger + \"Mid\", s), Role(finger + \"Dist\", s),\r\n }, ChainKind.Finger, left);\r\n }\r\n }\r\n }\r\n\r\n private static BoneRole Role(string baseName, string side) => Enum.Parse<BoneRole>(baseName + side);\r\n\r\n private static void BuildChainFrames(\r\n BoneRole[] chain, ChainKind kind, bool left, MappingResult map,\r\n IReadOnlyList<XForm> worldRest, CharacterFrame cf, Dictionary<BoneRole, Quaternion> frames,\r\n HashSet<BoneRole> virtualPrimary)\r\n {\r\n // Collapse to the mapped chain members; gaps are skipped so e.g. a missing Spine1\r\n // makes Spine0 point straight at Spine2.\r\n var mapped = new List<(BoneRole Role, Vector3 Pos)>(chain.Length);\r\n foreach (var role in chain)\r\n {\r\n if (map.RoleToBone.TryGetValue(role, out var index))\r\n mapped.Add((role, worldRest[index].Pos));\r\n }\r\n\r\n Vector3? dorsal = kind == ChainKind.Finger ? HandGeometry.Dorsal(map, worldRest, left) : null;\r\n\r\n for (var i = 0; i < mapped.Count; i++)\r\n {\r\n var (role, pos) = mapped[i];\r\n Vector3? prevDir = i > 0 ? pos - mapped[i - 1].Pos : null;\r\n\r\n var (primary, isVirtual) = i + 1 < mapped.Count\r\n ? ((Vector3?)(mapped[i + 1].Pos - pos), false)\r\n : TipPrimary(kind, role, pos, prevDir, left, map, worldRest, cf);\r\n if (primary is null || primary.Value.LengthSquared() < 1e-8f)\r\n continue;\r\n\r\n var secondary = Secondary(kind, role, primary.Value, dorsal, cf);\r\n frames[role] = BasisFromPrimarySecondary(primary.Value, secondary, cf);\r\n if (isVirtual)\r\n virtualPrimary.Add(role);\r\n }\r\n }\r\n\r\n /// <summary>Primary direction for the last mapped bone of a chain. <c>Virtual</c> is true\r\n /// when the direction is a character-axis convention rather than this rig's real joint\r\n /// geometry (see <see cref=\"HasVirtualPrimary\"/>).</summary>\r\n private static (Vector3? Dir, bool Virtual) TipPrimary(\r\n ChainKind kind, BoneRole role, Vector3 pos, Vector3? prevDir, bool left,\r\n MappingResult map, IReadOnlyList<XForm> worldRest, CharacterFrame cf)\r\n {\r\n switch (kind)\r\n {\r\n case ChainKind.Body:\r\n // Head: its primary is the REAL previous chain segment (neck\u2192head \u2014 the\r\n // skull-base axis; the rest lean of that segment is head-joint-placement\r\n // anatomy the delta transfer modes reference, and the posed-rest gaze\r\n // fallback measures \u2014 see GeometricSolver remarks). Only a degenerate or\r\n // absent segment falls back to the virtual character-up extension (e.g. a\r\n // head stacked on the neck). A body chain that ends early keeps its\r\n // previous segment direction, defaulting to up.\r\n if (role == BoneRole.Head)\r\n return prevDir is { } seg && seg.LengthSquared() >= 1e-8f ? (seg, false) : (cf.Up, true);\r\n return prevDir is not null ? (prevDir, false) : (cf.Up, true);\r\n\r\n case ChainKind.Arm:\r\n if (role is BoneRole.HandL or BoneRole.HandR)\r\n {\r\n var knuckles = HandGeometry.FingerProximalMidpoint(map, worldRest, left);\r\n if (knuckles is not null)\r\n return (knuckles.Value - pos, false);\r\n }\r\n return (prevDir, false); // along the forearm / previous segment; null \u2192 no frame\r\n\r\n case ChainKind.Leg:\r\n // Foot without a mapped toe, and the toe itself, extend along character\r\n // forward (toes point forward by the character-frame convention).\r\n if (role is BoneRole.FootL or BoneRole.FootR or BoneRole.ToeL or BoneRole.ToeR)\r\n return (cf.Forward, true);\r\n return (prevDir, false);\r\n\r\n case ChainKind.Finger:\r\n if (prevDir is not null)\r\n return (prevDir, false); // distal tip extrapolates its previous segment\r\n // Single mapped finger bone: point away from the hand when possible.\r\n var handRole = left ? BoneRole.HandL : BoneRole.HandR;\r\n if (map.RoleToBone.TryGetValue(handRole, out var handIndex))\r\n return (pos - worldRest[handIndex].Pos, false);\r\n return (null, false);\r\n\r\n default:\r\n return (null, false);\r\n }\r\n }\r\n\r\n /// <summary>Secondary (Z) hint by bone class; see the class remarks for rationale.</summary>\r\n private static Vector3 Secondary(ChainKind kind, BoneRole role, Vector3 primary, Vector3? dorsal, CharacterFrame cf)\r\n {\r\n switch (kind)\r\n {\r\n case ChainKind.Body:\r\n return cf.Forward;\r\n\r\n case ChainKind.Arm:\r\n {\r\n var hinge = Vector3.Cross(Vector3.Normalize(primary), cf.Up);\r\n return hinge.LengthSquared() < 1e-6f ? cf.Forward : hinge;\r\n }\r\n\r\n case ChainKind.Leg:\r\n // Feet and toes lie near the character-forward direction, so they use up as\r\n // the secondary; thigh/calf use forward (knee hinge lateral).\r\n if (role is BoneRole.FootL or BoneRole.FootR or BoneRole.ToeL or BoneRole.ToeR)\r\n return cf.Up;\r\n return cf.Forward;\r\n\r\n case ChainKind.Finger:\r\n if (dorsal is not null)\r\n return dorsal.Value;\r\n var fallback = Vector3.Cross(Vector3.Normalize(primary), cf.Up);\r\n return fallback.LengthSquared() < 1e-6f ? cf.Forward : fallback;\r\n\r\n default:\r\n return cf.Forward;\r\n }\r\n }\r\n\r\n /// <summary>\r\n /// Orthonormal right-handed basis: <c>X = normalize(primary)</c>, <c>Z = secondary</c>\r\n /// Gram-Schmidt-orthonormalized against X (falling back to character forward, then up,\r\n /// then world axes when degenerate), <c>Y = cross(Z, X)</c>.\r\n /// </summary>\r\n private static Quaternion BasisFromPrimarySecondary(Vector3 primary, Vector3 secondary, CharacterFrame cf)\r\n {\r\n var x = Vector3.Normalize(primary);\r\n\r\n var z = Orthonormalized(secondary, x)\r\n ?? Orthonormalized(cf.Forward, x)\r\n ?? Orthonormalized(cf.Up, x)\r\n ?? Orthonormalized(Vector3.UnitZ, x)\r\n ?? Orthonormalized(Vector3.UnitX, x)!.Value;\r\n\r\n var y = Vector3.Cross(z, x);\r\n\r\n // System.Numerics matrices act on row vectors: the rows are the images of the unit\r\n // axes under the rotation (row1 = R*X, row2 = R*Y, row3 = R*Z).\r\n var m = new Matrix4x4(\r\n x.X, x.Y, x.Z, 0f,\r\n y.X, y.Y, y.Z, 0f,\r\n z.X, z.Y, z.Z, 0f,\r\n 0f, 0f, 0f, 1f);\r\n\r\n return MathQ.Normalize(Quaternion.CreateFromRotationMatrix(m));\r\n }\r\n\r\n private static Vector3? Orthonormalized(Vector3 hint, Vector3 x)\r\n {\r\n var z = hint - x * Vector3.Dot(hint, x);\r\n return z.LengthSquared() < 1e-6f ? null : Vector3.Normalize(z);\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_retargeter",
"Path": "Code/HumanoidRetargeter/Solve/HandGeometry.cs",
"FileName": "HandGeometry.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 311783,
"Code": "#nullable enable annotations\r\n\r\nusing System.Collections.Generic;\r\nusing System.Numerics;\r\nusing HumanoidRetargeter.Mapping;\r\nusing HumanoidRetargeter.Maths;\r\n\r\nnamespace HumanoidRetargeter.Solve;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)\r\n\r\n/// <summary>\r\n/// Hand rest-geometry helpers shared by <see cref=\"CanonicalFrames\"/> (finger secondary axes)\r\n/// and <see cref=\"RestNormalizer\"/> (palm-down roll correction). Everything derives from joint\r\n/// positions only \u2014 bone local axes carry no anatomical meaning on the s&box rig.\r\n/// </summary>\r\ninternal static class HandGeometry\r\n{\r\n private static readonly BoneRole[] LeftProximals =\r\n {\r\n BoneRole.ThumbProxL, BoneRole.IndexProxL, BoneRole.MiddleProxL, BoneRole.RingProxL, BoneRole.PinkyProxL,\r\n };\r\n\r\n private static readonly BoneRole[] RightProximals =\r\n {\r\n BoneRole.ThumbProxR, BoneRole.IndexProxR, BoneRole.MiddleProxR, BoneRole.RingProxR, BoneRole.PinkyProxR,\r\n };\r\n\r\n // Index \u2192 pinky order; the knuckle line is taken from the first and last mapped of these.\r\n private static readonly BoneRole[] LeftNonThumbProximals =\r\n {\r\n BoneRole.IndexProxL, BoneRole.MiddleProxL, BoneRole.RingProxL, BoneRole.PinkyProxL,\r\n };\r\n\r\n private static readonly BoneRole[] RightNonThumbProximals =\r\n {\r\n BoneRole.IndexProxR, BoneRole.MiddleProxR, BoneRole.RingProxR, BoneRole.PinkyProxR,\r\n };\r\n\r\n /// <summary>\r\n /// Midpoint of all mapped finger proximal heads of one hand (the hand's anatomical\r\n /// \"chain child\" point), or null when no finger proximal is mapped.\r\n /// </summary>\r\n public static Vector3? FingerProximalMidpoint(MappingResult map, IReadOnlyList<XForm> worldRest, bool left)\r\n {\r\n var sum = Vector3.Zero;\r\n var count = 0;\r\n foreach (var role in left ? LeftProximals : RightProximals)\r\n {\r\n if (map.RoleToBone.TryGetValue(role, out var index))\r\n {\r\n sum += worldRest[index].Pos;\r\n count++;\r\n }\r\n }\r\n return count > 0 ? sum / count : null;\r\n }\r\n\r\n /// <summary>\r\n /// Dorsal palm normal of one hand: the unit vector pointing out of the <b>back</b> of the\r\n /// hand (away from the palm), or null when the hand/finger geometry is unmapped or\r\n /// degenerate.\r\n /// </summary>\r\n /// <remarks>\r\n /// Formula (mirror-consistent by construction, verified on the ActorCore fixture by the\r\n /// finger-curl test): <c>dorsal = sideSign \u00b7 cross(knuckle, fingerDir)</c> with\r\n /// <c>sideSign = +1</c> left / <c>\u22121</c> right, <c>knuckle = IndexProx.head \u2212\r\n /// PinkyProx.head</c> (first/last mapped non-thumb proximal), and <c>fingerDir =\r\n /// FingerProximalMidpoint \u2212 Hand.head</c>. On every fixture rig the thumb proximal lies on\r\n /// the \u2212dorsal (palmar) side of the hand plane, grounding the sign anatomically. A positive\r\n /// rotation about a finger frame's hinge axis (frame Y = cross(dorsal, fingerChainDir))\r\n /// curls the fingertip toward the palm on <b>both</b> hands.\r\n /// </remarks>\r\n public static Vector3? Dorsal(MappingResult map, IReadOnlyList<XForm> worldRest, bool left)\r\n {\r\n if (!map.RoleToBone.TryGetValue(left ? BoneRole.HandL : BoneRole.HandR, out var handIndex))\r\n return null;\r\n var hand = worldRest[handIndex].Pos;\r\n\r\n var nonThumb = left ? LeftNonThumbProximals : RightNonThumbProximals;\r\n Vector3? first = null, last = null;\r\n foreach (var role in nonThumb)\r\n {\r\n if (!map.RoleToBone.TryGetValue(role, out var index))\r\n continue;\r\n first ??= worldRest[index].Pos;\r\n last = worldRest[index].Pos;\r\n }\r\n if (first is null || last is null || (first.Value - last.Value).LengthSquared() < 1e-8f)\r\n return null;\r\n\r\n var midpoint = FingerProximalMidpoint(map, worldRest, left);\r\n if (midpoint is null)\r\n return null;\r\n\r\n var knuckle = first.Value - last.Value;\r\n var fingerDir = midpoint.Value - hand;\r\n var raw = Vector3.Cross(knuckle, fingerDir) * (left ? 1f : -1f);\r\n return raw.LengthSquared() < 1e-8f ? null : Vector3.Normalize(raw);\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_retargeter",
"Path": "Code/HumanoidRetargeter/Solve/TargetRigMapping.cs",
"FileName": "TargetRigMapping.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 311783,
"Code": "#nullable enable annotations\r\n\r\nusing HumanoidRetargeter.Mapping;\r\nusing HumanoidRetargeter.Target;\r\n\r\nnamespace HumanoidRetargeter.Solve;\r\n\r\n/// <summary>\r\n/// Bridges a <see cref=\"TargetRig\"/>'s role annotations into the <see cref=\"MappingResult\"/>\r\n/// shape shared with source mappings, so target-side machinery (rest normalization, canonical\r\n/// frames) can run on the exact same code paths as the source side.\r\n/// </summary>\r\npublic static class TargetRigMappingExtensions\r\n{\r\n /// <summary>Role \u2192 target bone index mapping of the rig's annotated animated bones.</summary>\r\n public static MappingResult ToMappingResult(this TargetRig rig)\r\n {\r\n ArgumentNullException.ThrowIfNull(rig);\r\n\r\n var map = new MappingResult(rig.Name, MappingSource.Preset) { Confidence = 1f };\r\n for (var i = 0; i < rig.Skeleton.Count; i++)\r\n {\r\n if (rig.RoleOf(i) is BoneRole role)\r\n map.RoleToBone[role] = i;\r\n }\r\n return map;\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_retargeter",
"Path": "HumanoidRetargeter/Assembly.cs",
"FileName": "Assembly.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 311783,
"Code": "#nullable enable annotations\r\n\r\n// Global usings for the s&box in-engine compiler.\r\n//\r\n// The plain net8.0 dev harness gets these automatically via <ImplicitUsings>,\r\n// but s&box's compiler injects no BCL usings at all - without this file the\r\n// library fails to compile inside the editor (CS0246 on List<>, IEnumerable<>,\r\n// FormatException, ...). Duplicating the SDK's implicit set is harmless there\r\n// (verified: no warnings).\r\n\r\nglobal using System;\r\nglobal using System.Collections.Generic;\r\nglobal using System.IO;\r\nglobal using System.Linq;\r\nglobal using System.Threading;\r\nglobal using System.Threading.Tasks;\r\n\r\n// NOTE on Vector3: s&box declares its own Vector3 in the *global namespace*,\r\n// which wins over `using System.Numerics;` imports during name lookup and\r\n// breaks this System.Numerics-based core (.X/.Y/.Z, static helpers, delegate\r\n// signatures). A global using-alias does NOT fix this (CS0576: alias conflicts\r\n// with the global-namespace type at every use site). The working fix is a\r\n// *namespace-scoped* alias, declared after the file-scoped namespace line:\r\n//\r\n// namespace HumanoidRetargeter.Xyz;\r\n// using Vector3 = System.Numerics.Vector3;\r\n//\r\n// Every file in this tree that uses the simple name Vector3 carries that line.\r\n// (Quaternion and Matrix4x4 are not global-namespace types in s&box and need\r\n// no alias.)\r\n"
}
]
}