s&box Package Code Search
Search C# source code, UI razor templates, shaders, and configs across s&box packages.
link Raw Facepunch API Link: https://public.facepunch.com/sbox/code/search/1/?ident=notpointless.chomnr_humanoid_mocap_x&take=20
Showing code results for query:
*
(609 total matches found)
Game
library
// C# adaptation of FlowHMR's x1 Euler sampling. See THIRD_PARTY_NOTICES.md.
using System;
using System.Threading;
namespace HumanoidMocapX;
/// <summary>Predicts the clean 491-channel motion latent at a normalized time in [0,1).
/// Writes every output element; failures and cancellation propagate.</summary>
public delegate void FlowPrediction(ReadOnlySpan<float> state, float time, Span<float> output, CancellationToken cancellation);
/// <summary>The released Base and Latest models use twenty Euler steps and x1 prediction.</summary>
public static class FlowSampler
{
public const int Channels = 491;
public const int Steps = 20;
/// <summary>Replaces caller-supplied initial noise with sampled motion using a reusable
/// prediction buffer. Rejects invalid shapes and non-finite predictions. A failed or
/// cancelled run may leave state partially advanced; it must not be exported.</summary>
public static void Sample(float[] state, float[] prediction, FlowPrediction predict,
CancellationToken cancellation = default)
{
ArgumentNullException.ThrowIfNull(state);
ArgumentNullException.ThrowIfNull(prediction);
ArgumentNullException.ThrowIfNull(predict);
if (state.Length == 0 || state.Length % Channels != 0 || state.Length / Channels > FlowMotion.MaximumFrames
|| prediction.Length != state.Length || ReferenceEquals(state, prediction))
throw new ArgumentException("Sampling needs separate, equally sized [frames,491] state and prediction buffers.");
CheckFinite(state);
const float minimumRemaining = 1f / Steps;
for (var step = 0; step < Steps; step++)
{
cancellation.ThrowIfCancellationRequested();
var time = step / (float)Steps;
var delta = (step + 1) / (float)Steps - time;
predict(state, time, prediction, cancellation);
cancellation.ThrowIfCancellationRequested();
CheckFinite(prediction);
var remaining = Math.Max(1f - time, minimumRemaining);
for (var i = 0; i < state.Length; i++)
state[i] += (prediction[i] - state[i]) / remaining * delta;
CheckFinite(state);
}
}
static void CheckFinite(ReadOnlySpan<float> values)
{
foreach (var value in values)
if (!float.IsFinite(value)) throw new ArithmeticException("FlowHMR sampling produced or received a non-finite latent.");
}
}
Game
library
using System;
namespace HumanoidMocapX;
/// <summary>Video-range and model settings sent to a processing host. Times are source seconds.</summary>
public sealed record HostedCaptureOptions(FlowModel Model, int CameraInterval, double Start, double? End)
{
/// <summary>Rejects unsupported models, sampling intervals and invalid time ranges.</summary>
public void Validate()
{
new CaptureOptions { Model = Model, CameraInterval = CameraInterval }.Validate();
if (!double.IsFinite(Start) || Start < 0 || End is double end && (!double.IsFinite(end) || end <= Start))
throw new ArgumentException("The video range needs a finite, nonnegative start and a later end.");
}
}
/// <summary>Protocol version, processor readiness, the upload limit in bytes, how many unfinished
/// jobs one account may have, how many jobs wait and run across all accounts, how many process at
/// once, and the longest video range in seconds. A provider's site reports the signed-in account's
/// own limits and whether its jobs wait in the priority queue.</summary>
public sealed record HostedCapabilities(int Protocol, bool ProcessingAvailable, long MaximumUploadBytes,
int MaximumActiveJobs = 1, int QueueLength = 0, int Processing = 0, int ConcurrentJobs = 1,
double? MaximumClipSeconds = null, bool Priority = false);
/// <summary>Job state returned by the host. QueuePosition is one-based while queued, out of
/// QueueLength waiting jobs; Progress (0 to 1) is set while processing. SecondsUntilStart and
/// SecondsRemaining are the host's estimates. ExpiresAt is when a finished job and its result
/// are deleted. Priority jobs wait ahead of the others. IDs are opaque; result URLs are derived locally.</summary>
#nullable enable annotations
public sealed record HostedJob(Guid Id, string State, string Message, int? QueuePosition = null, string? Name = null,
DateTimeOffset CreatedAt = default, int QueueLength = 0, double? Progress = null, int? SecondsUntilStart = null,
int? SecondsRemaining = null, DateTimeOffset? ExpiresAt = null, bool Priority = false);
#nullable restore
Game
library
#nullable enable annotations
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Threading;
using HumanoidMocapX.Core.Maths;
using HumanoidMocapX.Core.Skeleton;
namespace HumanoidMocapX.Core.Formats.Fbx;
using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidMocap/Assembly.cs)
/// <summary>Options for <see cref="FbxImporter.Import"/>.</summary>
public sealed class FbxImportOptions
{
/// <summary>Fixed resampling rate for all clips, frames per second.</summary>
public float SampleFps { get; init; } = 30f;
public long MaximumTransformSamples { get; init; } = long.MaxValue;
public CancellationToken CancellationToken { get; init; }
/// <summary>
/// When the static rest pose is degenerate (Mixamo-style zeroed bind translations) and no
/// usable BindPose node exists, sample frame 0 of the first clip as the rest pose.
/// </summary>
public bool RestFromFrame0WhenBindDegenerate { get; init; } = true;
}
Game
library
#nullable enable annotations
using System;
using System.Numerics;
using HumanoidMocapX.Core.Maths;
namespace HumanoidMocapX.Core.Formats.Fbx;
using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidMocap/Assembly.cs)
/// <summary>
/// Per-node FBX local-transform evaluation: the static pivot/pre-rotation data of one Model
/// plus the full FBX transform formula.
/// </summary>
/// <remarks>
/// The FBX SDK formula (column-vector convention, right-multiply children):
/// <code>
/// World = ParentWorld · T · Roff · Rp · Rpre · R · Rpost⁻¹ · Rp⁻¹ · Soff · Sp · S · Sp⁻¹
/// </code>
/// System.Numerics matrices are row-vector (<c>p' = p·M</c>), so the local matrix is composed
/// in reverse order with row-form factors:
/// <code>
/// Local = T(-Sp)·S·T(Sp)·T(Soff)·T(-Rp)·R(Rpre·R·Rpost⁻¹)·T(Rp)·T(Roff)·T(t)
/// World = Local · ParentWorld
/// </code>
/// <para><b>RotationOrder</b> (0=XYZ, 1=XZY, 2=YZX, 3=YXZ, 4=ZXY, 5=ZYX): the letters give the
/// application order, first letter applied first — eEulerXYZ composes <c>R = Rz·Ry·Rx</c> in
/// column convention (verified against Blender-imported ground truth of Mixamo and citizen
/// rigs). Pre/Post rotations always use XYZ order and are only applied when
/// <c>RotationActive</c> is set (matching Blender's importer; defaults to active when the
/// property is absent everywhere).</para>
/// <para><b>InheritType</b> (0=RrSs, 1=RSrs, 2=Rrs): scale is dropped to rigid transforms by
/// this importer — world matrices are evaluated with full scale, then decomposed to pos+rot
/// with scale folded into child translations — so types 0 and 1 are handled identically.
/// Files that use non-uniform scale meaningfully with InheritType 2 (no parent scale
/// propagation) are evaluated as type 0; this is a documented limitation.</para>
/// </remarks>
public sealed class FbxTransform
{
/// <summary>Static "Lcl Translation" default (native units).</summary>
public Vector3 LclTranslation { get; init; }
/// <summary>Static "Lcl Rotation" default, euler degrees in <see cref="RotationOrder"/>.</summary>
public Vector3 LclRotationDeg { get; init; }
/// <summary>Static "Lcl Scaling" default.</summary>
public Vector3 LclScaling { get; init; } = Vector3.One;
/// <summary>PreRotation as a quaternion (identity when inactive or absent).</summary>
public Quaternion PreRotation { get; init; } = Quaternion.Identity;
/// <summary>PostRotation as a quaternion (identity when inactive or absent).</summary>
public Quaternion PostRotation { get; init; } = Quaternion.Identity;
/// <summary>RotationOffset (Roff).</summary>
public Vector3 RotationOffset { get; init; }
/// <summary>RotationPivot (Rp).</summary>
public Vector3 RotationPivot { get; init; }
/// <summary>ScalingOffset (Soff).</summary>
public Vector3 ScalingOffset { get; init; }
/// <summary>ScalingPivot (Sp).</summary>
public Vector3 ScalingPivot { get; init; }
/// <summary>Euler application order code, 0=XYZ … 5=ZYX (see class remarks).</summary>
public int RotationOrder { get; init; }
/// <summary>InheritType 0=RrSs, 1=RSrs, 2=Rrs (see class remarks for handling).</summary>
public int InheritType { get; init; }
/// <summary>Reads the static transform data of a Model from its (template-backed) properties.</summary>
public static FbxTransform FromModel(FbxScene scene, FbxObject model)
{
ArgumentNullException.ThrowIfNull(scene);
ArgumentNullException.ThrowIfNull(model);
// Pre/Post rotation participate only when RotationActive is set (Blender-compatible).
// When the property is absent on both the object and its template, default to active —
// exporters that strip RotationActive but keep PreRotation expect it applied.
bool rotationActive = scene.FindProperty(model, "RotationActive") is { } ra
? ra.GetInt() != 0
: true;
var pre = scene.GetVector3(model, "PreRotation", Vector3.Zero);
var post = scene.GetVector3(model, "PostRotation", Vector3.Zero);
return new FbxTransform
{
LclTranslation = scene.GetVector3(model, "Lcl Translation", Vector3.Zero),
LclRotationDeg = scene.GetVector3(model, "Lcl Rotation", Vector3.Zero),
LclScaling = scene.GetVector3(model, "Lcl Scaling", Vector3.One),
PreRotation = rotationActive ? EulerDegreesToQuaternion(pre, 0) : Quaternion.Identity,
PostRotation = rotationActive ? EulerDegreesToQuaternion(post, 0) : Quaternion.Identity,
RotationOffset = scene.GetVector3(model, "RotationOffset", Vector3.Zero),
RotationPivot = scene.GetVector3(model, "RotationPivot", Vector3.Zero),
ScalingOffset = scene.GetVector3(model, "ScalingOffset", Vector3.Zero),
ScalingPivot = scene.GetVector3(model, "ScalingPivot", Vector3.Zero),
RotationOrder = scene.GetInt(model, "RotationOrder", 0),
InheritType = scene.GetInt(model, "InheritType", 0),
};
}
/// <summary>
/// Local matrix (row-vector layout) for given TRS values; pass the Lcl defaults for the
/// rest pose or per-frame sampled values for animation.
/// </summary>
public Matrix4x4 LocalMatrix(Vector3 translation, Vector3 rotationDeg, Vector3 scaling)
{
// Column chain R-part: Rpre · R · Rpost⁻¹ → our quaternion convention (a*b applies
// b first) composes the same way.
var rotation = MathQ.Normalize(
PreRotation *
EulerDegreesToQuaternion(rotationDeg, RotationOrder) *
Quaternion.Conjugate(PostRotation));
// Row-vector composition, first-applied factor leftmost (see class remarks).
var m = Matrix4x4.CreateTranslation(-ScalingPivot);
m *= Matrix4x4.CreateScale(scaling);
m *= Matrix4x4.CreateTranslation(ScalingPivot + ScalingOffset - RotationPivot);
m *= Matrix4x4.CreateFromQuaternion(rotation);
m *= Matrix4x4.CreateTranslation(RotationPivot + RotationOffset + translation);
return m;
}
/// <summary>Local matrix from the static Lcl defaults (rest evaluation, no animation).</summary>
public Matrix4x4 LocalMatrixDefault() => LocalMatrix(LclTranslation, LclRotationDeg, LclScaling);
/// <summary>
/// Converts FBX euler angles (degrees) to a quaternion for the given RotationOrder code.
/// The order letters list the application sequence (XYZ = X first, then Y, then Z).
/// </summary>
public static Quaternion EulerDegreesToQuaternion(Vector3 degrees, int order)
{
const float degToRad = MathF.PI / 180f;
var qx = Quaternion.CreateFromAxisAngle(Vector3.UnitX, degrees.X * degToRad);
var qy = Quaternion.CreateFromAxisAngle(Vector3.UnitY, degrees.Y * degToRad);
var qz = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, degrees.Z * degToRad);
// q = third * second * first (our convention applies the right factor first).
return order switch
{
0 => qz * qy * qx, // XYZ
1 => qy * qz * qx, // XZY
2 => qx * qz * qy, // YZX
3 => qz * qx * qy, // YXZ
4 => qy * qx * qz, // ZXY
5 => qx * qy * qz, // ZYX
6 => qz * qy * qx, // eSphericXYZ — treated as XYZ
_ => throw new FormatException($"Unsupported FBX RotationOrder {order}."),
};
}
/// <summary>
/// Extracts the rigid part of a (possibly scaled) world matrix: translation taken directly,
/// rotation from the orthonormalized basis. Scale is thereby folded into child translations
/// when parent-relative transforms are derived from rigid worlds. Mirrored (negative
/// determinant) bases fall back to the dominant proper rotation (best effort — mirrored
/// skeletons are not meaningfully supported).
/// </summary>
public static XForm ToRigid(in Matrix4x4 world)
{
var pos = world.Translation;
var x = new Vector3(world.M11, world.M12, world.M13);
var y = new Vector3(world.M21, world.M22, world.M23);
var z = new Vector3(world.M31, world.M32, world.M33);
x = SafeNormalize(x, Vector3.UnitX);
y = SafeNormalize(y, Vector3.UnitY);
z = SafeNormalize(z, Vector3.UnitZ);
// Mirrored basis: flip the third axis to restore a proper rotation.
if (Vector3.Dot(Vector3.Cross(x, y), z) < 0f)
z = -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 new XForm(pos, MathQ.Normalize(Quaternion.CreateFromRotationMatrix(m)));
}
private static Vector3 SafeNormalize(Vector3 v, Vector3 fallback)
{
float lsq = v.LengthSquared();
if (!float.IsFinite(lsq) || lsq < 1e-20f)
return fallback;
return v / MathF.Sqrt(lsq);
}
}
Game
library
using System;
using System.Collections.Generic;
using System.Linq;
using HumanoidMocapX.Core.Maths;
using HumanoidMocapX.Core.Skeleton;
namespace HumanoidMocapX.Core.Formats.Fbx;
using SkeletonModel = HumanoidMocapX.Core.Skeleton.Skeleton;
/// <summary>FBX with the source-to-engine scale baked into every translation for direct ModelDoc import.</summary>
public static class ModelDocFbxExport
{
/// <summary>Uses Humanoid Retargeter's source scale for centimetre rigs; compiled engine rigs already use inches.</summary>
public static byte[] Write( SkeletonModel skeleton, string name, IReadOnlyList<XForm[]> frames,
IReadOnlyList<double> times, double fps, TargetUpAxis axis, string motionSpace )
{
ArgumentNullException.ThrowIfNull( skeleton );
ArgumentNullException.ThrowIfNull( frames );
if ( frames.Count is < 1 or > 108000 || skeleton.Count is < 1 or > 1024
|| (long)frames.Count * skeleton.Count > FbxAnimationWriter.MaximumTransformSamples )
throw new ArgumentException( "Animation exceeds the export budget. Select a shorter range." );
if ( axis is not (TargetUpAxis.YUpCm or TargetUpAxis.ZUpEngine) )
throw new ArgumentOutOfRangeException( nameof( axis ) );
// Like Retargeter's ModelModifier_ScaleAndMirror, this converts raw source values.
// UnitScaleFactor alone is insufficient for ModelDoc's raw-unit import. Bake the
// factor into bind positions, all local translations, root motion and appended props.
if ( axis == TargetUpAxis.YUpCm )
{
XForm Scale( XForm pose ) => new( pose.Pos * RetargetTargetSpec.SboxSourceScale, pose.Rot );
skeleton = SkeletonModel.Create( skeleton.Bones.Select( bone => new BoneDefinition(
bone.Name, bone.ParentIndex < 0 ? null : skeleton[ bone.ParentIndex ].Name,
Scale( bone.RestLocal ) ) ).ToArray() );
frames = frames.Select( frame => frame.Select( Scale ).ToArray() ).ToArray();
}
return FbxAnimationWriter.Write( skeleton, name, frames, times, fps,
axis == TargetUpAxis.YUpCm ? 1 : 2, 2.54, motionSpace );
}
}
Game
library
#nullable enable annotations
using System;
using System.Collections.Generic;
using System.Linq;
using HumanoidMocapX.Core.Mapping;
using HumanoidMocapX.Core.Maths;
using HumanoidMocapX.Core.Target;
using HumanoidMocapX.Core.Skeleton;
namespace HumanoidMocapX.Core;
/// <summary>
/// Result of <see cref="Retargeter.Inspect"/>: the mapping report plus the file's take
/// metadata, so UI listings can expand a multi-take file into one entry per take (each
/// convertible independently via <see cref="RetargetRequest.TakeIndex"/>).
/// </summary>
public sealed class InspectResult
{
/// <summary>Mapping report from the same preset-then-auto cascade conversion uses.</summary>
public required MappingReportInfo Mapping { get; init; }
/// <summary>Names of the file's animation takes, in take-index order (the index is what
/// <see cref="RetargetRequest.TakeIndex"/> accepts). Empty when the file has no animation.</summary>
public required IReadOnlyList<string> TakeNames { get; init; }
/// <summary>Number of animation takes in the file.</summary>
public int TakeCount => TakeNames.Count;
}
Game
library
using System;
using System.Numerics;
using HumanoidMocapX.Core.Maths;
namespace HumanoidMocapX.Core.Motion;
using Vector3 = System.Numerics.Vector3;
public sealed class ArmSettings
{
public Vector3 Shoulder { get; set; } = new(.18f,-.15f,0);
public Vector3 ElbowTarget { get; set; } = new(.4f,-.4f,.2f);
public float UpperLength { get; set; } = .28f;
public float ForearmLength { get; set; } = .25f;
public float MaximumReach { get; set; } = .995f;
public float MinimumElbowDegrees { get; set; } = 5;
public float MaximumElbowDegrees { get; set; } = 155;
}
Game
library
using System;
namespace HumanoidMocapX.Core.Motion;
using Vector3 = System.Numerics.Vector3;
/// <summary>Leaves a small knee bend when moving the pelvis to reach captured foot targets.</summary>
internal static class CaptureLegReach
{
// Exact full extension is singular: a millimetre of ankle travel can unfold the knee
// by centimetres. Reserve half a percent of reach and enter the correction smoothly.
internal const float ReachFraction = .995f;
internal static float Lowering( Vector3 hipToFoot, Vector3 up, float length )
{
var vertical = Vector3.Dot( hipToFoot, up );
var horizontal = hipToFoot - up * vertical;
var reach = length * ReachFraction;
if ( length <= 0 || vertical <= 0 || horizontal.LengthSquared() >= reach * reach ) return 0;
var deficit = vertical - MathF.Sqrt( reach * reach - horizontal.LengthSquared() );
var ease = length * .01f;
// Smooth positive part, evaluated without subtracting two nearly equal numbers.
var diagonal = MathF.Sqrt( deficit * deficit + ease * ease );
return deficit >= 0 ? (deficit + diagonal) * .5f : ease * ease / (2 * (diagonal - deficit));
}
}
Game
library
using System;
namespace HumanoidMocapX.Core.Preview;
/// <summary>Owns a bounded RGBA preview frame. Reuses storage until the preview size
/// changes; callers must synchronize concurrent readers and writers.</summary>
public sealed class PreviewFrameBuffer
{
const int MaximumWidth = 960, MaximumHeight = 540, PixelBytes = 4;
byte[] pixels = Array.Empty<byte>();
/// <summary>Width of the owned preview in pixels; zero before the first frame.</summary>
public int Width { get; private set; }
/// <summary>Height of the owned preview in pixels; zero before the first frame.</summary>
public int Height { get; private set; }
/// <summary>Increases after each successful copy so consumers can skip unchanged frames.</summary>
public long Version { get; private set; }
/// <summary>Borrowed pixels, valid until the next Copy call.</summary>
public ReadOnlySpan<byte> Pixels => pixels;
/// <summary>Copies a packed RGBA frame, reducing it to fit 960×540 without changing
/// aspect ratio. Rejects nonpositive dimensions and an incorrect byte count.</summary>
public void Copy(ReadOnlySpan<byte> source, int width, int height)
{
if (width < 1 || height < 1 || (long)width * height * PixelBytes != source.Length)
throw new ArgumentException("Video preview requires a complete packed RGBA frame.");
var scale = Math.Min(1, Math.Min((double)MaximumWidth / width, (double)MaximumHeight / height));
var targetWidth = Math.Max(1, (int)(width * scale));
var targetHeight = Math.Max(1, (int)(height * scale));
var length = targetWidth * targetHeight * PixelBytes;
if (pixels.Length != length) pixels = new byte[length];
if (targetWidth == width && targetHeight == height) source.CopyTo(pixels);
else
for (var y = 0; y < targetHeight; y++)
for (var x = 0; x < targetWidth; x++)
{
var from = ((long)y * height / targetHeight * width + (long)x * width / targetWidth) * PixelBytes;
var to = (y * targetWidth + x) * PixelBytes;
source.Slice((int)from, PixelBytes).CopyTo(pixels.AsSpan(to, PixelBytes));
}
Width = targetWidth; Height = targetHeight; Version++;
}
}
Game
library
#nullable enable annotations
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Text;
using HumanoidMocapX.Core.Cleanup;
using HumanoidMocapX.Core.Formats;
using HumanoidMocapX.Core.Formats.Dmx;
using HumanoidMocapX.Core.Formats.Fbx;
using HumanoidMocapX.Core.Formats.Gltf;
using HumanoidMocapX.Core.Mapping;
using HumanoidMocapX.Core.Maths;
using HumanoidMocapX.Core.Skeleton;
using HumanoidMocapX.Core.Solve;
using HumanoidMocapX.Core.Target;
using SkeletonModel = HumanoidMocapX.Core.Skeleton.Skeleton;
namespace HumanoidMocapX.Core;
using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidMocap/Assembly.cs)
/// <summary>
/// Engine-agnostic pipeline facade (design §5): bytes in → DMX + vmdl text out. Composes
/// import → mapping → solve → cleanup → IK baking → DMX → vmdl assembly. Batch is
/// first-class: N files × all takes per file → ONE combined vmdl, with per-request profile
/// detection (a batch may mix Mixamo + ActorCore + BVH sources) and per-clip failure
/// isolation. No file IO anywhere in this type — callers pass bytes/strings and write the
/// returned strings.
/// </summary>
/// <remarks>
/// Pipeline per clip:
/// <list type="number">
/// <item><b>Import</b> — format by file extension (<c>.hmotion</c>/<c>.fbx</c>/<c>.glb</c>/
/// <c>.gltf</c>/<c>.vrm</c>), content sniff as fallback → <see cref="SourceScene"/> (cm,
/// native axes).</item>
/// <item><b>Mapping</b> — per request: <see cref="RetargetRequest.MappingOverride"/> wins;
/// else <see cref="ProfileDetector.Detect"/> over the preset library (user presets are
/// loaded Editor-side and arrive as overrides); else <see cref="AutoMapper.Map"/> as best
/// effort with <see cref="MappingReportInfo.NeedsUserDecision"/> set when its confidence is
/// below <see cref="ProfileDetector.DetectionThreshold"/>.</item>
/// <item><b>Solve</b> — <see cref="GeometricSolver"/> per take (clip name = take name, or
/// <see cref="RetargetRequest.ClipNameOverride"/> + index).</item>
/// <item><b>Cleanup</b> (target space) — <see cref="FootPlant"/> when enabled (chains from
/// the target rig's UpperLeg/LowerLeg/Foot/Toe roles, up axis from the TARGET character
/// frame, fps from the clip); optional arm <see cref="EffectorIk"/> (off by default — the
/// solver already matches anatomical directions); <see cref="RootMotion"/> per mode (see
/// below).</item>
/// <item><b>IK baking</b> — <see cref="IkBoneBaker.Bake"/> when the rig has
/// <see cref="BoneClass.IkBaked"/> bones.</item>
/// <item><b>DMX</b> — <see cref="DmxWriter"/> over the target skeleton.</item>
/// <item><b>Assembly</b> — <see cref="VmdlWriter.GenerateStandalone"/> over all successful
/// clips, plus <see cref="VmdlAugmenter.Augment"/> when
/// <see cref="BatchOptions.AugmentVmdlText"/> is provided; clip-name collisions are
/// auto-suffixed (<c>_2</c>, <c>_3</c>, …) across the batch.</item>
/// </list>
/// <para><b>Root-motion ↔ target mapping.</b> <see cref="RootMotionMode.Extract"/> bakes the
/// ground-projected hips path onto the target's dedicated root bone — defined as a parentless
/// <see cref="BoneClass.Animated"/> bone that is an ancestor of (and distinct from) the Hips
/// bone. The s&box rig has NO such bone (pelvis is itself parentless; root_IK is IkBaked),
/// so there Extract leaves the frames untouched, adds a report note, and instead sets
/// <see cref="AnimEntry.ExtractMotion"/> on the clip's vmdl entry: Source 2 extracts the
/// ground-plane translation from <see cref="RetargetTargetSpec.DefaultRootBone"/> at compile
/// time, which is the engine-native equivalent. The ExtractMotion flag is set for every
/// Extract request either way. <see cref="RootMotionMode.InPlace"/> always operates on the
/// hips channels directly and needs no dedicated root.</para>
/// </remarks>
public static class Retargeter
{
/// <summary>
/// Applies the same mapping, diagnostics, and mid-pose bind-rest selection used by
/// <see cref="ConvertBatch"/>, while leaving the sampled clips available to an editor
/// integration that needs an in-memory target rather than DMX output.
/// </summary>
public static ResolvedSource ResolveSource( SourceScene scene, MappingResult? mappingOverride = null,
Func<string, MappingResult?>? userPresetLookup = null )
{
ArgumentNullException.ThrowIfNull( scene );
var (map, report) = ResolveMapping( scene.Skeleton, mappingOverride, userPresetLookup, scene.AuthoredMapping );
foreach ( var note in scene.Notes ) AddNote( report, note );
scene = MaybeAdoptBindRest( scene, map, report );
return new( scene, map, report );
}
/// <summary>Converts one source file — all takes, or only
/// <see cref="RetargetRequest.TakeIndex"/> when set. Equivalent to a one-request batch.</summary>
public static RetargetResult Convert(RetargetRequest request, RetargetTargetSpec target)
{
ArgumentNullException.ThrowIfNull(request);
var batch = ConvertBatch(new[] { request }, target);
return new RetargetResult
{
Clips = batch.Clips,
StandaloneVmdl = batch.StandaloneVmdl,
Errors = batch.Errors,
};
}
/// <summary>
/// Converts a batch of source files against one target. Per-request profile detection,
/// per-clip failure isolation (a bad file yields a failed <see cref="ClipResult"/> and
/// the batch continues), one standalone vmdl with every successful clip, optional
/// augmentation of an existing vmdl.
/// </summary>
public static RetargetBatchResult ConvertBatch(
IReadOnlyList<RetargetRequest> requests, RetargetTargetSpec target, BatchOptions? options = null)
{
ArgumentNullException.ThrowIfNull(requests);
ArgumentNullException.ThrowIfNull(target);
options ??= new BatchOptions();
var context = new TargetContext(target.Rig);
var result = new RetargetBatchResult();
var usedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var entries = new List<AnimEntry>();
// Augment mode: names already taken in the existing vmdl's AnimationList must not be
// silently repointed — seed the collision set with every existing node name EXCEPT
// our own AnimFiles (source_filename under DmxFolderRelative), which are replaceable
// so re-running the same batch stays idempotent.
if (options.AugmentVmdlText is not null)
SeedUsedNamesFromExistingVmdl(options.AugmentVmdlText, options.DmxFolderRelative, usedNames);
var mappedPinky = false;
foreach (var request in requests)
{
if (request is null)
continue;
mappedPinky |= ProcessRequest(
request, target, context, options, usedNames, result.Clips, entries);
}
foreach (var clip in result.Clips)
{
if (!clip.Success)
result.Errors.Add($"{clip.SourceFileName}: {clip.Error}");
}
// Locomotion families are detected on the FINAL (collision-suffixed) clip names so
// the blend grids reference exactly what the vmdl registers; folder/blend names are
// made unique against the same name set the clips used.
IReadOnlyList<LocomotionSetSpec>? locomotionSets = null;
if (options.DetectLocomotionSets)
{
var (sets, reports) = LocomotionSetDetector.Detect(
entries, usedNames, options.AutoSuffixCollisions);
result.LocomotionSets.AddRange(reports);
if (sets.Count > 0)
locomotionSets = sets;
}
// The target's OWN embedded animations ride along every generated vmdl (an FBX
// picked as target keeps its animation next to the retargeted ones). Skipped when
// a same-named entry is already registered (idempotent re-runs / augment targets
// that carry them from an earlier conversion); the retargeted clips claimed their
// names first, so an embedded take never displaces a converted clip.
if (target.ExtraAnimFiles is { Count: > 0 })
{
foreach (var extra in target.ExtraAnimFiles)
{
if (usedNames.Add(extra.Name))
entries.Add(extra);
}
}
result.StandaloneVmdl = VmdlWriter.GenerateStandalone(
target.BaseModelPath, entries, target.VmdlScale, target.DefaultRootBone,
locomotionSets, target.MeshFilePath, target.MeshImportScale, target.MaterialRemaps,
target.MeshImportNames);
if (options.AugmentVmdlText is not null)
{
try
{
var removedSequences = new List<string>();
result.AugmentedVmdl = VmdlAugmenter.Augment(
options.AugmentVmdlText, entries, out _,
new AugmentOptions
{
DefaultRootBone = target.DefaultRootBone,
// The citizen base model copies ring→pinky via its CopyPinky
// constraints; when this batch actually drives the pinky, the
// augmented vmdl must neutralize them or the exported pinky
// channels get overridden at runtime.
NeutralizePinkyConstraints = mappedPinky,
LocomotionSets = locomotionSets,
// Same ownership rule the name seeding above used: pipeline-owned
// locomotion folders (every AnimFile sourced under our DMX folder)
// are replaceable wholesale on re-runs.
DmxFolderRelative = options.DmxFolderRelative,
// Stale entries whose DMX the caller found missing on disk: keep
// them and the WHOLE vmdl stops compiling ("Node 'X' resolve
// failure"), new sequences included.
MissingSourceFiles = options.MissingAnimSources,
},
removedSequences);
foreach (var name in removedSequences)
{
result.Warnings.Add(
$"removed stale sequence '{name}' from the augmented vmdl: its "
+ "animation source file no longer exists on disk (a missing source "
+ "fails the whole model recompile)");
}
}
catch (Exception e)
{
result.Errors.Add($"vmdl augmentation failed: {e.Message}");
}
}
return result;
}
/// <summary>Collects names already present in the existing vmdl's AnimationList that the
/// batch must not reuse (everything except our own replaceable AnimFiles).</summary>
private static void SeedUsedNamesFromExistingVmdl(
string vmdlText, string dmxFolderRelative, HashSet<string> usedNames)
{
Kv3Document doc;
try
{
doc = Kv3.Parse(vmdlText);
}
catch (FormatException)
{
return; // the augmentation step itself surfaces the parse error
}
if (doc.Root is not KvObject root || root.GetOrNull("rootNode") is not KvObject rootNode
|| rootNode.GetOrNull("children") is not KvArray children)
return;
var animList = children.Items.OfType<KvObject>()
.FirstOrDefault(o => o.GetString("_class") == "AnimationList");
if (animList?.GetOrNull("children") is not KvArray items)
return;
var folder = dmxFolderRelative.Replace('\\', '/').TrimEnd('/');
SeedNames(items, folder, usedNames);
}
/// <summary>
/// Recursive seeding step: every named AnimationList node reserves its name EXCEPT our
/// own replaceable output — AnimFiles whose source lives in the batch's DMX folder, and
/// locomotion Folders consisting entirely of such AnimFiles (a re-run replaces the whole
/// folder, so neither its name nor its content may block the new batch's names). Foreign
/// folders are seeded by name and their content seeded recursively.
/// </summary>
private static void SeedNames(KvArray items, string dmxFolder, HashSet<string> usedNames)
{
foreach (var node in items.Items.OfType<KvObject>())
{
var name = node.GetString("name");
var cls = node.GetString("_class");
if (cls == "AnimFile")
{
if (!string.IsNullOrEmpty(name) && !IsOurAnimFile(node, dmxFolder))
usedNames.Add(name);
continue;
}
if (cls == "Folder")
{
if (IsOurLocomotionFolder(node, dmxFolder))
continue; // replaceable wholesale — nothing inside reserves a name
if (!string.IsNullOrEmpty(name))
usedNames.Add(name);
if (node.GetOrNull("children") is KvArray nested)
SeedNames(nested, dmxFolder, usedNames);
continue;
}
if (!string.IsNullOrEmpty(name))
usedNames.Add(name);
}
}
/// <summary>Whether an AnimFile was written by this pipeline: its source_filename sits
/// inside the batch's DMX folder.</summary>
private static bool IsOurAnimFile(KvObject node, string dmxFolder)
{
var source = (node.GetString("source_filename") ?? "").Replace('\\', '/');
return dmxFolder.Length == 0
? !source.Contains('/')
: source.StartsWith(dmxFolder + "/", StringComparison.OrdinalIgnoreCase);
}
/// <summary>Whether a Folder is a locomotion group this pipeline spliced: it contains at
/// least one AnimFile and every AnimFile inside it (recursively) is ours.</summary>
private static bool IsOurLocomotionFolder(KvObject folderNode, string dmxFolder)
{
var sawAnimFile = false;
return Walk(folderNode) && sawAnimFile;
bool Walk(KvObject node)
{
if (node.GetString("_class") == "AnimFile")
{
sawAnimFile = true;
return IsOurAnimFile(node, dmxFolder);
}
if (node.GetOrNull("children") is KvArray children)
{
foreach (var child in children.Items.OfType<KvObject>())
{
if (!Walk(child))
return false;
}
}
return true;
}
}
/// <summary>
/// Detection-only entry point for UI listings: imports the file and runs the same
/// preset-then-auto mapping as conversion, without solving anything. Also reports the
/// file's take metadata (clip names, in take-index order) so listings can expand a
/// multi-take file into one entry per take. Throws <see cref="FormatException"/> when
/// the file is unreadable.
/// </summary>
public static InspectResult Inspect(byte[] sourceData, string fileName)
{
ArgumentNullException.ThrowIfNull(sourceData);
ArgumentNullException.ThrowIfNull(fileName);
var scene = ImportSource(sourceData, fileName);
var takeNames = new List<string>(scene.Clips.Count);
foreach (var clip in scene.Clips)
takeNames.Add(clip.Name);
return new InspectResult
{
Mapping = ResolveMapping(scene.Skeleton, authoredMapping: scene.AuthoredMapping).Report,
TakeNames = takeNames,
};
}
// ================================================================ per-request pipeline
/// <summary>Runs one request end to end. Returns true when at least one clip converted
/// successfully with a mapping that drives a pinky role (CopyPinky handling).</summary>
private static bool ProcessRequest(
RetargetRequest request, RetargetTargetSpec target, TargetContext context,
BatchOptions options, HashSet<string> usedNames,
List<ClipResult> clips, List<AnimEntry> entries)
{
var sourceId = request.SourceId ?? request.SourceFileName;
SourceScene scene;
MappingReportInfo report;
MappingResult map;
try
{
scene = ImportSource(
request.SourceData, request.SourceFileName, request.SampleFps,
request.ExternalBufferResolver);
(map, report) = ResolveMapping(
scene.Skeleton, request.MappingOverride, authoredMapping: scene.AuthoredMapping);
// Import diagnostics (mid-pose exports, cross-stack static disagreements, ...)
// ride the mapping report so UIs can surface them next to the conversion result.
foreach (var note in scene.Notes)
AddNote(report, note);
scene = MaybeAdoptBindRest(scene, map, report);
}
catch (Exception e)
{
clips.Add(new ClipResult
{
ClipName = FileStem(request.SourceFileName),
SourceFileName = request.SourceFileName,
SourceId = sourceId,
Success = false,
Error = e.Message,
});
return false;
}
if (scene.Clips.Count == 0)
{
clips.Add(new ClipResult
{
ClipName = FileStem(request.SourceFileName),
SourceFileName = request.SourceFileName,
SourceId = sourceId,
Mapping = report,
Success = false,
Error = "Source file contains no animation takes.",
});
return false;
}
var mapsPinky = MapsPinkyRole(map);
if (mapsPinky && options.AugmentVmdlText is null
&& target.BaseModelPath == RetargetTargetSpec.SboxHumanMalePath)
{
// A standalone child vmdl cannot override the base model's AnimConstraintList:
// the citizen's CopyPinky (ring → pinky orient copy) keeps driving the pinky at
// runtime even though the DMX carries real pinky channels.
AddNote(report,
"Pinky channels are exported, but the base model's CopyPinky constraints "
+ "(ring → pinky copy) cannot be overridden from a standalone vmdl, so the "
+ "pinky will visibly mirror the ring finger's motion in engine — use "
+ "augment mode (add to the existing vmdl) to neutralize them.");
}
if (target.UpAxis == TargetUpAxis.ZUpEngine)
{
AddNote(report,
"Target rig is engine-space (Z-up, inches): the DMX declares a Z-up axis "
+ "system so resourcecompiler performs no Y-up conversion (best-effort — "
+ "verify the compiled sequence on engine-space targets).");
}
// External clip definitions (Unity .fbx.meta clipAnimations): one output clip per
// DEFINITION, sliced out of its take; TakeIndex addresses the definitions then
// (see RetargetRequest.ClipDefinitions).
if (request.ClipDefinitions is { Count: > 0 } clipDefs)
{
return ProcessClipDefinitions(
request, target, context, options, usedNames, clips, entries,
scene, map, report, sourceId, mapsPinky, clipDefs);
}
// TakeIndex narrows the conversion to a single take (UI per-take entries submit one
// request per selected take); null keeps the historical convert-all-takes behavior.
var takeStart = 0;
var takeEnd = scene.Clips.Count;
if (request.TakeIndex is { } takeIndex)
{
if (takeIndex < 0 || takeIndex >= scene.Clips.Count)
{
clips.Add(new ClipResult
{
ClipName = FileStem(request.SourceFileName),
SourceFileName = request.SourceFileName,
SourceId = sourceId,
Mapping = report,
Success = false,
Error = $"Take index {takeIndex} is out of range: the source file "
+ $"contains {scene.Clips.Count} take(s).",
});
return false;
}
takeStart = takeIndex;
takeEnd = takeIndex + 1;
}
var anyPinkySuccess = false;
for (var take = takeStart; take < takeEnd; take++)
{
var clipName = UniqueClipName(
SanitizeClipName(RequestedClipName(request, scene, take)),
usedNames, options.AutoSuffixCollisions);
anyPinkySuccess |= ConvertOne(
request, target, context, options, usedNames, clips, entries,
scene, map, report, sourceId, mapsPinky, take, clipName);
}
return anyPinkySuccess;
}
/// <summary>
/// The definitions variant of the take loop: each <see cref="ExternalClipDef"/> becomes
/// its own clip result — the definition's take is located by
/// <see cref="ExternalClipDef.TakeName"/> (falling back to the file's first take), sliced
/// to the definition's native-frame range via <see cref="UnityMeta.Slice"/> and solved as
/// a single-clip scene. Returns true when at least one clip converted successfully with a
/// pinky-driving mapping.
/// </summary>
private static bool ProcessClipDefinitions(
RetargetRequest request, RetargetTargetSpec target, TargetContext context,
BatchOptions options, HashSet<string> usedNames,
List<ClipResult> clips, List<AnimEntry> entries,
SourceScene scene, MappingResult map, MappingReportInfo report,
string sourceId, bool mapsPinky, IReadOnlyList<ExternalClipDef> clipDefs)
{
var defStart = 0;
var defEnd = clipDefs.Count;
if (request.TakeIndex is { } defIndex)
{
if (defIndex < 0 || defIndex >= clipDefs.Count)
{
clips.Add(new ClipResult
{
ClipName = FileStem(request.SourceFileName),
SourceFileName = request.SourceFileName,
SourceId = sourceId,
Mapping = report,
Success = false,
Error = $"Clip-definition index {defIndex} is out of range: the request "
+ $"carries {clipDefs.Count} clip definition(s).",
});
return false;
}
defStart = defIndex;
defEnd = defIndex + 1;
}
var anyPinkySuccess = false;
for (var d = defStart; d < defEnd; d++)
{
var def = clipDefs[d];
var requestedName = !string.IsNullOrWhiteSpace(def.Name)
? def.Name
: clipDefs.Count > 1 ? $"{FileStem(request.SourceFileName)}_{d + 1}" : FileStem(request.SourceFileName);
var clipName = UniqueClipName(
SanitizeClipName(requestedName), usedNames, options.AutoSuffixCollisions);
var take = TakeForDefinition(scene, def);
var sliced = UnityMeta.Slice(scene.Clips[take], def, clipName);
// Same skeleton/axes, ONE clip: the regular pipeline then solves "take 0" of it.
var defScene = new SourceScene(
scene.Skeleton, new[] { sliced }, scene.UnitScaleCm,
scene.UpAxis, scene.UpAxisSign,
scene.FrontAxis, scene.FrontAxisSign,
scene.CoordAxis, scene.CoordAxisSign,
scene.OriginalUpAxis, scene.Notes)
{
AuthoredMapping = scene.AuthoredMapping,
RestPlacementAuthored = scene.RestPlacementAuthored,
};
anyPinkySuccess |= ConvertOne(
request, target, context, options, usedNames, clips, entries,
defScene, map, report, sourceId, mapsPinky, take: 0, clipName);
}
return anyPinkySuccess;
}
/// <summary>The take a definition's frame range refers to: matched by take name when the
/// definition records one (Unity's <c>takeName</c>, e.g. <c>root|Animation</c>), else —
/// and when nothing matches — the file's first take.</summary>
private static int TakeForDefinition(SourceScene scene, ExternalClipDef def)
{
if (!string.IsNullOrWhiteSpace(def.TakeName))
{
for (var i = 0; i < scene.Clips.Count; i++)
{
if (string.Equals(scene.Clips[i].Name, def.TakeName, StringComparison.Ordinal))
return i;
}
}
return 0;
}
/// <summary>Solves ONE clip (take <paramref name="take"/> of <paramref name="scene"/>)
/// end to end — solve + cleanup + DMX, plus the optional mirrored twin
/// (<see cref="RetargetRequest.CreateMirroredVariant"/>) — appending success or failure
/// <see cref="ClipResult"/>s (failures never abort the batch). Returns true on success
/// with a pinky-driving mapping.</summary>
private static bool ConvertOne(
RetargetRequest request, RetargetTargetSpec target, TargetContext context,
BatchOptions options, HashSet<string> usedNames,
List<ClipResult> clips, List<AnimEntry> entries,
SourceScene scene, MappingResult map, MappingReportInfo report,
string sourceId, bool mapsPinky, int take, string clipName)
{
Clip clip;
try
{
clip = SolveAndClean(request, target, context, scene, map, report, take, clipName);
EmitClip(request, target, context, options, usedNames, clips, entries, report,
sourceId, clipName, clip.Frames, clip.Fps, clip.Looping, isMirrored: false);
}
catch (Exception e)
{
clips.Add(new ClipResult
{
ClipName = clipName,
SourceFileName = request.SourceFileName,
SourceId = sourceId,
Mapping = report,
Success = false,
Error = e.Message,
});
return false;
}
if (request.CreateMirroredVariant)
{
// The twin gets its own collision-suffixed name and its own failure isolation:
// a mirror problem (asymmetric rig) fails ONLY the twin, never the primary clip.
var mirroredName = UniqueClipName(
SanitizeClipName(clipName + "_M"), usedNames, options.AutoSuffixCollisions);
try
{
// SOURCE-SIDE MIRROR (southpaw G8 mirror fix, gate3_review.md 3.4 and the
// G8 bisect evidence): the SOURCE clip is mirrored across the source
// character's sagittal plane and the twin then runs through the COMPLETE
// unmodified primary pipeline (solver, cleanups, ground alignment, IK
// bake, orphan re-anchor). The emitted _M clip is a first-class primary
// clip in every representational respect, indistinguishable from a clip
// an animator authored mirrored. Target-space channel mirroring, however
// geometrically exact, produced data the ENGINE's render-side sequence
// evaluation mangles (measured: a mirrored pelvis channel alone renders
// the whole model upside down while every CPU-side bone API reports a
// perfect upright mirror); no data conjugation convention survived that
// stage, so the mirror moved to the source side where no alien data
// shape can exist.
var mirroredScene = SourceMirror.MirrorTake(scene, map, take);
var mirroredClip = SolveAndClean(
request, target, context, mirroredScene, map, report, take, mirroredName);
EmitClip(request, target, context, options, usedNames, clips, entries, report,
sourceId, mirroredName, mirroredClip.Frames, mirroredClip.Fps,
clip.Looping, isMirrored: true);
}
catch (Exception e)
{
clips.Add(new ClipResult
{
ClipName = mirroredName,
SourceFileName = request.SourceFileName,
SourceId = sourceId,
Mapping = report,
Success = false,
Error = $"mirrored variant: {e.Message}",
});
}
}
return mapsPinky;
}
/// <summary>Shared tail of clip production (primary and mirrored twin): optional footstep
/// events, DMX serialization, the <see cref="ClipResult"/> and the vmdl
/// <see cref="AnimEntry"/> — plus the additive (<c>_delta</c>) companion entry when
/// <see cref="RetargetRequest.CreateAdditiveVariant"/> is on (a second AnimFile REUSING
/// the clip's DMX with an AnimSubtract child, shipped-citizen shape; no separate
/// <see cref="ClipResult"/> since no separate DMX exists).</summary>
private static void EmitClip(
RetargetRequest request, RetargetTargetSpec target, TargetContext context,
BatchOptions options, HashSet<string> usedNames,
List<ClipResult> clips, List<AnimEntry> entries,
MappingReportInfo report, string sourceId,
string clipName, List<XForm[]> frames, float fps, bool looping, bool isMirrored)
{
var events = GenerateFootsteps(request, target, context, report, frames, fps);
var dmxFileName = SanitizeFileName(clipName) + ".dmx";
// Embedded-mesh vmdls and compiled Z-up targets compile root channels 90° about the declared
// up axis away from the mesh bind; child channels are unaffected. Compensate the
// serialized copy only so compiled playback matches the solved preview.
var dmxFrames = !string.IsNullOrEmpty(target.MeshFilePath) || target.UpAxis == TargetUpAxis.ZUpEngine
? CompensateEmbeddedMeshRootYaw(frames, target.Rig, target.UpAxis)
: frames;
var dmx = DmxWriter.Write(
target.Rig.Skeleton, new Clip(clipName, fps, looping, dmxFrames), new DmxWriteOptions
{
Name = clipName,
SourceNote = isMirrored ? request.SourceFileName + " (mirrored)" : request.SourceFileName,
// Design §3: ConstraintDriven (twist/helper) bones keep their joints +
// bind in the DMX but get NO channels — the model's AnimConstraintList
// drives them. Face bones are exempt from the exclusion (rest-local
// channels): nothing drives them in a compiled sequence, and channel-less
// face joints bake statically (eyes out of sockets in ModelDoc). Custom
// rigs (FromSkeleton) have NO constraint list — their helpers keep baked
// channels or twist bones freeze (candy-wrapped wrists).
// MIRRORED clips use the IDENTICAL exclusion set (data-shape parity with
// primary clips, southpaw G8 mirror fix): a channel-less constraint-driven
// bone on a mirrored-body clip is no different from one on any leftward
// primary pose; the model's AnimConstraintList drives it at runtime either
// way. (ClipMirror.MirrorSafeExclusions remains available for consumers
// that need reflection-exact data for excluded bones.)
ChannelExcludedBones = target.Rig.HelpersAreConstraintDriven
? context.ConstraintDrivenBones
: null,
UpAxisY = target.UpAxis == TargetUpAxis.YUpCm,
});
var extractMotion = request.RootMotion == RootMotionMode.Extract;
// Additive variant: '<clip>_delta' (shipped naming), collision-suffixed like every
// other batch name. Only the NAME is produced here — the vmdl entry below reuses
// the base clip's DMX (resourcecompiler does the reference-frame subtraction).
var deltaName = request.CreateAdditiveVariant
? UniqueClipName(
SanitizeClipName(clipName + "_delta"), usedNames, options.AutoSuffixCollisions)
: null;
clips.Add(new ClipResult
{
ClipName = clipName,
SourceFileName = request.SourceFileName,
SourceId = sourceId,
DmxFileName = dmxFileName,
DmxContent = dmx,
Mapping = report,
Success = true,
SolvedFrames = frames,
Fps = fps,
Looping = looping,
ExtractMotion = extractMotion,
FootstepEvents = events,
IsMirroredVariant = isMirrored,
HasAdditiveVariant = deltaName is not null,
AdditiveVariantName = deltaName,
});
var sourceFilename = JoinAssetPath(options.DmxFolderRelative, dmxFileName);
entries.Add(new AnimEntry
{
Name = clipName,
SourceFilename = sourceFilename,
Looping = looping,
ExtractMotion = extractMotion,
Events = events,
});
if (deltaName is not null)
{
// The shipped _delta sequences carry the AnimSubtract child and nothing else
// (no motion extraction, no events) — an additive layer fires no footsteps and
// extracting root motion from a delta makes no sense.
entries.Add(new AnimEntry
{
Name = deltaName,
SourceFilename = sourceFilename,
Looping = looping,
SubtractAnimName = clipName,
SubtractFrame = 0,
});
}
}
/// <summary>
/// Generates the <c>AE_FOOTSTEP</c> events for a solved clip when the request asks for
/// them (<see cref="RetargetRequest.GenerateFootstepEvents"/>): plant intervals are
/// detected on the SOLVED TARGET frames (this is where the engine plays the clip, so
/// touchdowns must be measured here), each interval start = one footstep
/// (<see cref="FootstepEvents"/>). Empty when the feature is off or the target rig lacks
/// the leg chains / character up (noted on the report then).
/// </summary>
private static IReadOnlyList<AnimEventEntry> GenerateFootsteps(
RetargetRequest request, RetargetTargetSpec target, TargetContext context,
MappingReportInfo report, List<XForm[]> frames, float fps)
{
if (!request.GenerateFootstepEvents)
return Array.Empty<AnimEventEntry>();
if (context.Up is not { } up || context.FootChains is not { } feet)
{
AddNote(report, "Footstep events skipped: " + context.UpOrChainProblem);
return Array.Empty<AnimEventEntry>();
}
return FootstepEvents.Generate(
frames, target.Rig.Skeleton, feet.Left, feet.Right, up, fps,
ScaledPlantOptions(target));
}
/// <summary>Default plant thresholds are cm-tuned; engine-space rigs are in inches, so
/// they are scaled by the cm→inch factor to keep the same physical sensitivity (shared by
/// the foot-plant cleanup and the footstep-event detection).</summary>
private static FootPlantOptions ScaledPlantOptions(RetargetTargetSpec target)
{
var options = new FootPlantOptions();
if (target.UpAxis == TargetUpAxis.ZUpEngine)
{
options.SpeedThresholdCmPerSec *= RetargetTargetSpec.SboxSourceScale;
options.HeightThresholdCm *= RetargetTargetSpec.SboxSourceScale;
}
return options;
}
private static bool MapsPinkyRole(MappingResult map)
{
foreach (var role in map.RoleToBone.Keys)
{
if (role.ToString().StartsWith("Pinky", StringComparison.Ordinal))
return true;
}
return false;
}
/// <summary>Solve one take and run the target-space cleanup + IK baking passes.</summary>
private static Clip SolveAndClean(
RetargetRequest request, RetargetTargetSpec target, TargetContext context,
SourceScene scene, MappingResult map, MappingReportInfo report, int take, string clipName)
{
var requested = request.Solve ?? new SolveOptions();
if(request.MocapCorrections is {} wristSettings)
{
Motion.WristPositionOffsets.Validate(wristSettings.WristOffsets);
if(wristSettings.WristOffsets.Any(e=>e.Enabled))
throw new NotSupportedException("Manual wrist offsets currently require camera-relative hand capture.");
}
if(scene.CaptureContacts?.Contacts.Any(c=>c.Review==Motion.ContactReview.Confirmed&&!string.IsNullOrEmpty(c.Object))==true)
throw new NotSupportedException("Confirmed prop constraints currently require camera-relative hand capture. Disable those constraints to preview body motion; object tracks remain available in captured-skeleton export.");
var captureClavicleDirections = scene.CaptureSpace is not null && requested.TransferModes is null;
if (captureClavicleDirections)
{
// Body-model collar joints are not the target's anatomical rest carriage.
// SMPL's zero-pose collar->shoulder slopes upward; its reconstructed neutral
// pose rotates that line down to level. Replaying the same delta onto the
// Human's already-level collar line lowers its shoulders a second time.
AddNote(report, "Full-body capture transfers observed clavicle directions while retaining target bone lengths and attachment positions.");
}
var solved = new GeometricSolver().Solve(scene, map, target.Rig, new SolveOptions
{
GroundedLegDirections = request.FootPlantCleanup,
HipScaleHorizontal = requested.HipScaleHorizontal,
HipScaleVertical = requested.HipScaleVertical,
TransferFingers = requested.TransferFingers,
TransferModes = requested.TransferModes,
CaptureClavicleDirections = captureClavicleDirections,
NeutralSourceRest = captureClavicleDirections,
ClipIndex = take,
ClipName = clipName,
});
var frames = solved.Frames;
// ---- embedded-take fidelity: authored local translations pass through ----------
if (request.PreserveSourceTranslations)
PassThroughSourceTranslations(scene, target.Rig, frames, take, report);
// ---- foot-plant cleanup (target space; up axis from the TARGET character frame) ----
if (request.FootPlantCleanup)
{
if (context.Up is { } up && context.FootChains is { } feet)
{
// Grounded stance alignment first: levels planted soles against the ground
// (removes the stance offset a non-stance source rest leaves in the solver's
// rest-relative foot transfer). Plants are detected on the SOURCE clip —
// source-space candidates; hip-height rescaling can push the solved target trajectories
// outside the cm-tuned Kovar thresholds. Composes with the position pinning
// below (this rotates feet about their own joints; the pinning preserves
// foot world rotations).
// Reconstructed bodies: restore the performer's stance width on differently
// proportioned hips before anything is levelled or anchored.
if(scene.CaptureSpace is not null)
{
var stance=Motion.CaptureStanceProportion.Apply(frames,scene,map,target.Rig,feet.Left,feet.Right);
if(stance.Samples>0)AddNote(report,$"Stance proportion: each foot moved {stance.HalfWidthCorrection:F2} target units toward the body's centre line, because this target's hip joints are wider relative to its legs than the performer's. Leg lengths and foot orientation are preserved.");
}
GroundAlignFeet(frames, scene, map, target.Rig.Skeleton, feet, up, solved.Fps, take);
var plantOptions=ScaledPlantOptions(target);
// Reconstructed contacts can be unreachable on different proportions.
// Keep captured target limb lengths fixed instead of stretching to a guess.
if(scene.CaptureSpace is not null)plantOptions.MaxStretch=0f;
FootPlant.Apply(
frames, target.Rig.Skeleton, feet.Left, feet.Right, up, solved.Fps,
plantOptions);
// Support ground alignment: one constant vertical offset per clip that
// restores the SOURCE-authored foot-to-ground relationship (southpaw
// final regeneration package, gate4_review.md 3.1 / DEFERRED.md f2).
AlignSupportToGround(frames, scene, map, target.Rig, feet, up, report, take);
MatchSourceFootHeights(frames, scene, map, target.Rig, feet, up, take);
}
else
{
AddNote(report, "Foot-plant cleanup skipped: " + context.UpOrChainProblem);
}
}
// A reconstructed body has no finger tracks; give its hands a resting shape, not the bind pose.
if(scene.CaptureSpace is not null)
{
if(Motion.CaptureSpineSpread.Apply(frames,target.Rig)>0)AddNote(report,"Spine: each frame's bend from the hips to the chest is shared evenly between the spine bones; the network puts most of it in the lowest one. Chest, head and arms are unchanged.");
var shoulders=Motion.CaptureShoulderCarriage.Apply(frames,scene.Clips[take].Frames,scene.Skeleton,map,target.Rig);
if(shoulders>0)AddNote(report,$"Shoulders: {shoulders} clavicle samples carry the performer's change from their own rest shoulder line onto this target's rest shoulder line, instead of copying a differently built clavicle's direction. Arms keep their solved orientation.");
// After the shoulders are placed: restore the performer's hand spacing on differently proportioned shoulders.
var arms=Motion.CaptureArmProportion.Apply(frames,scene.Skeleton,map,target.Rig);
if(arms.Samples>0)AddNote(report,$"Hand spacing: wrists in front of or across the body moved up to {arms.HalfWidthCorrection:F2} target units toward the centre line, because this target's shoulders are wider relative to its arms than the performer's. Arm lengths and hand orientation are preserved; outstretched arms are left alone.");
var together=Motion.CaptureHandRelation.Apply(frames,scene.Clips[take].Frames,scene.Skeleton,map,target.Rig);
if(together>0)AddNote(report,$"Hands together: in {together} frames the performer's hands were close (crossing, clapping or touching) and the character's hands were placed the same way from each other, scaled to its arms.");
var relaxed=Motion.RelaxedHands.Apply(frames,map,target.Rig);
if(relaxed>0)AddNote(report,$"Hands: this body capture has no finger tracks, so {relaxed} target finger joints hold one authored, slightly curled resting pose instead of the bind pose. It is not captured finger motion.");
}
// ---- optional arm effector IK (default off: the solver already matches anatomical
// directions; arm IK is only for reach-critical work) ----
if (request.ArmEffectorIk)
{
var problem = ArmIkCleanup.Apply(frames, scene, map, target.Rig, context, take);
if (problem is not null)
AddNote(report, "Arm effector IK skipped: " + problem);
}
// ---- root motion (see class remarks for the Extract ↔ ExtractMotion mapping) ----
if(request.FootPlantCleanup&&(request.MocapCorrections?.StabilizeFeet??true))
{
var anchored=Motion.CaptureContactRoot.Apply(frames,scene,map,target.Rig,target.UpAxis);
if(anchored>0)AddNote(report,FormattableString.Invariant($"Travel from contacts: planted feet no longer slide; the body's travel was corrected by up to {anchored:F0} cm to match them."));
var locking=Motion.CaptureFootLock.Apply(frames,scene,map,target.Rig,target.UpAxis);
if(locking.ConstrainedSamples>0)AddNote(report,$"Final target foot anchors: {locking.ConstrainedSamples} leg samples; maximum reach residual {locking.MaximumReachResidual:F4} target units; level-floor height drift of up to {locking.MaximumFloorDrift:F2} target units removed from the root. Backend static probabilities are contact suggestions, not measured ground truth.");
// Keep the whole clip on the floor, including camera-relative captures and dance with few plants.
if(scene.CaptureSpace is not null)
{
var contactMask=Motion.CaptureContactRoot.ContactMask(frames,scene,map,target.Rig,target.UpAxis);
var grounded=Motion.CaptureGround.Apply(frames,target.Rig,target.UpAxis,solved.Fps,contactMask);
if(grounded>0)AddNote(report,FormattableString.Invariant($"Floor followed through the clip: up to {grounded:F1} cm of drift toward or away from the floor removed."));
var hovering=Motion.CaptureHover.Apply(frames,target.Rig,target.UpAxis,solved.Fps,contactMask);
if(hovering>0)AddNote(report,$"Kept on the floor in {hovering} frames: brief hovering above it or sinking into it removed.");
var seatedFrames=Motion.CaptureSeat.Apply(frames,scene,map,target.Rig,target.UpAxis);
if(seatedFrames>0)AddNote(report,$"Seated on the floor in {seatedFrames} frames: hips lowered onto the floor, feet and resting hands held in place.");
var handsHeld=Motion.CaptureHandPlant.Apply(frames,target.Rig,target.UpAxis,solved.Fps,PlantedHands(scene,map,frames.Count),HandSpread(scene,map,take));
if(handsHeld>0)AddNote(report,$"Hands on the floor held in place in {handsHeld} hand samples: palms resting on the floor no longer slide or hover.");
}
}
// Last of the leg work: no thigh, shin or foot turns far more in one frame than around it.
if(scene.CaptureSpace is not null&&Motion.CaptureLegSnaps.Apply(frames,target.Rig,solved.Fps,target.UpAxis==TargetUpAxis.ZUpEngine?39.3700787f:100f,PlantedFeet(scene,map,frames.Count)) is var snaps&&snaps>0)
AddNote(report,$"Leg continuity corrected in {snaps} samples: isolated rotation or knee-position jolts were spread over neighbouring frames, preserving foot anchors where the constraints allowed.");
ApplyRootMotion(request.RootMotion, frames, context, report);
if (request.MocapCorrections is { } corrections)
{
Motion.TargetCorrections.Apply(frames, target.Rig, target.UpAxis, corrections);
if (corrections.FirstPerson) AddNote(report, "Shoulders and elbows are generated target-rig IK, not measured joints.");
}
if (scene.CaptureSpace is not null)
{
var clearance = Motion.CaptureArmClearance.Apply(frames, scene.Clips[take].Frames, scene.Skeleton, map,
target.Rig, solved.Fps, out var unresolvedContacts, out var maximumPenetration);
if (clearance.Samples > 0)
AddNote(report, FormattableString.Invariant($"Torso contact: DigitalRune CCD adjusted {clearance.Samples} hand samples by up to {clearance.Largest:F2} target units, preserving wrist attitude and finger articulation. Clearance uses a bone-derived body proxy."));
if (unresolvedContacts > 0)
AddNote(report, FormattableString.Invariant($"Torso contact remains unresolved in {unresolvedContacts} hand samples (up to {maximumPenetration:F3} target units). Fixed arm lengths and wrist attitude prevented a clear proxy pose; review these contacts."));
}
// Last of the pose cleanup: whatever snap a step above left (an IK flip, a contact pushed clear, a hold handed
// over) is pulled back onto its neighbours' path, keeping planted hands and feet where they are.
if (scene.CaptureSpace is not null && Motion.CaptureDespike.Apply(frames, target.Rig, target.UpAxis == TargetUpAxis.ZUpEngine ? 1 / 2.54f : 1f) is var smoothedSnaps && smoothedSnaps > 0)
AddNote(report, $"Snaps removed after cleanup: {smoothedSnaps} joint samples that left the smooth path through their neighbours were pulled back onto it.");
// ---- IK helper bones (root_IK, IK targets, ikrule) need real baked channels ----
if (context.HasIkBakedBones)
IkBoneBaker.Bake(frames, target.Rig);
// ---- unmapped limb twist bones follow their joint's roll ------------------------
// Mocap exports a portable baked armature, so it cannot rely on the target
// model's runtime constraints. Its preview overrides those helpers too.
// Keep runtime-owned helpers only for the ordinary model-constraint path.
var twistCount = TwistBoneFollow.Apply(frames, target.Rig,
request.MocapCorrections is null && target.Rig.HelpersAreConstraintDriven ? context.ConstraintDrivenBones : null);
if (twistCount > 0)
AddNote(report, $"{twistCount} limb deform helper bone(s) follow their "
+ "neighboring joints (left at rest they pinch or candy-wrap the skin).");
// ---- orphan helper bones ride the hips (LAST: anchored to the FINAL hips, after
// foot-plant pinning / root motion have settled the trajectory) -------------------
FollowHipsWithOrphanBones(target.Rig, frames, report);
var looping = request.LoopingOverride ?? solved.Looping;
return new Clip(clipName, solved.Fps, looping, frames);
}
/// <summary>Per source frame, how far apart the performer's hands are across the floor, in shoulder widths; null when the
/// source lacks hands or shoulders. The capture is Y-up.</summary>
static float[] HandSpread(SourceScene scene,MappingResult map,int take)
{
if(!map.RoleToBone.TryGetValue(BoneRole.HandL,out var handL)||!map.RoleToBone.TryGetValue(BoneRole.HandR,out var handR)
||!map.RoleToBone.TryGetValue(BoneRole.UpperArmL,out var armL)||!map.RoleToBone.TryGetValue(BoneRole.UpperArmR,out var armR))return null;
var source=scene.Clips[take].Frames;var world=new XForm[scene.Skeleton.Count];var spread=new float[source.Count];
for(var f=0;f<source.Count;f++)
{
FkUtil.ToWorld(source[f],scene.Skeleton,world);
var apart=world[handL].Pos-world[handR].Pos;apart.Y=0;
var shoulders=Vector3.Distance(world[armL].Pos,world[armR].Pos);
spread[f]=shoulders>1e-6f?apart.Length()/shoulders:0;
}
return spread;
}
/// <summary>Frames where the capture marks each hand on the floor (stationary probability at least one half).</summary>
static Dictionary<BoneRole,bool[]> PlantedHands(SourceScene scene,MappingResult map,int count)
=>new[]{BoneRole.HandL,BoneRole.HandR}.ToDictionary(role=>role,role=>
{
var result=new bool[count];
if(map.RoleToBone.TryGetValue(role,out var bone)&&scene.CaptureStationaryJoints?.TryGetValue(scene.Skeleton[bone].Name,out var p)==true&&p.Length==count)
for(var f=0;f<count;f++)result[f]=p[f]>=.5f;
return result;
});
/// <summary>Frames where the capture marks each joint planted (stationary probability at least one half): left ankle, left
/// toe, right ankle, right toe.</summary>
static bool[][] PlantedFeet(SourceScene scene,MappingResult map,int count)
=>new[]{BoneRole.FootL,BoneRole.ToeL,BoneRole.FootR,BoneRole.ToeR}.Select(role=>
{
var result=new bool[count];
if(map.RoleToBone.TryGetValue(role,out var bone)&&scene.CaptureStationaryJoints?.TryGetValue(scene.Skeleton[bone].Name,out var p)==true&&p.Length==count)
for(var f=0;f<count;f++)result[f]=p[f]>=.5f;
return result;
}).ToArray();
/// <summary>
/// Unmapped bone subtrees OUTSIDE the mapped skeleton (no mapped ancestor — cloth and
/// physics helpers parented to the scene root; real case: a skirt of 48 kilt bones on
/// a Sketchfab catgirl, siblings of the body root) ride the character rigidly. Left
/// at their rest locals they freeze at the bind spot while the body animates away
/// ("one part of the body doesn't move at all") and the mesh skinned across the
/// boundary shears ("completely stretched"). Each subtree's topmost bone anchors to
/// the HIPS — or to an extremity (hand/foot/head) when its rest sits clearly closer
/// to it (a sword resting in the hand follows the hand, not the pelvis). Descendants
/// ride along on their rest locals; bones on the hips' own ancestor path stay
/// solver-owned (trajectory). A mapped body branch on a separate control tree is
/// similarly anchored to the nearest hips-connected mapped joint; its solved rotation
/// remains unchanged.
/// </summary>
private static void FollowHipsWithOrphanBones(
TargetRig rig, List<XForm[]> frames, MappingReportInfo report)
{
var skeleton = rig.Skeleton;
if (rig.BoneForRole(BoneRole.Hips) is not { } hips || frames.Count == 0)
return;
var hipsPath = new HashSet<int>();
for (var b = hips; b >= 0; b = skeleton[b].ParentIndex)
hipsPath.Add(b);
bool HasMappedAncestor(int bone)
{
for (var b = skeleton[bone].ParentIndex; b >= 0; b = skeleton[b].ParentIndex)
{
if (rig.RoleOf(b) is not null)
return true;
}
return false;
}
var rest = skeleton.RestWorld;
bool DescendsFrom(int bone, int ancestor)
{
for (var b = bone; b >= 0; b = skeleton[b].ParentIndex)
if (b == ancestor)
return true;
return false;
}
// Extremity anchors: adopted only when the orphan rests at less than half its
// hips distance — near-torso helpers (skirts, tails, capes) stay on the hips,
// which never mistakes a skirt panel brushing a thigh for hand luggage.
var extremities = new List<int>();
foreach (var role in new[]
{
BoneRole.HandL, BoneRole.HandR, BoneRole.FootL, BoneRole.FootR, BoneRole.Head,
})
{
if (rig.BoneForRole(role) is { } bone)
extremities.Add(bone);
}
// Topmost orphans: unmapped, off the hips' ancestor path, no mapped ancestor,
// and the parent is either the scene root path or absent (children of deeper
// orphans ride along unchanged).
var tops = new List<(int Bone, int Anchor)>();
for (var i = 0; i < skeleton.Count; i++)
{
if (rig.RoleOf(i) is not null || hipsPath.Contains(i) || HasMappedAncestor(i))
continue;
var parent = skeleton[i].ParentIndex;
if (parent >= 0 && !hipsPath.Contains(parent))
continue;
var anchor = hips;
var hipsDistance = (rest[i].Pos - rest[hips].Pos).Length();
var best = hipsDistance * 0.5f;
foreach (var candidate in extremities)
{
var distance = (rest[i].Pos - rest[candidate].Pos).Length();
if (distance < best)
{
best = distance;
anchor = candidate;
}
}
tops.Add((i, anchor));
}
// A few constraint-oriented exports place a real mapped body branch on a parallel
// control hierarchy. The common case is a skinned head under CONTROL_NECK while
// the deform cervical chain is under the hips. glTF/FBX does not preserve the DCC
// constraint that joined those trees, so rotations solve but the branch stays at
// its bind position. Anchor each topmost detached mapped branch to the nearest
// mapped joint that really descends from the hips, preserving its solved rotation.
var connectedMapped = Enumerable.Range(0, skeleton.Count)
.Where(i => rig.RoleOf(i) is not null && DescendsFrom(i, hips))
.ToList();
var detachedMapped = new List<(int Bone, int Anchor)>();
for (var i = 0; i < skeleton.Count; i++)
{
if (rig.RoleOf(i) is null || hipsPath.Contains(i) || DescendsFrom(i, hips)
|| HasMappedAncestor(i) || connectedMapped.Count == 0)
continue;
var anchor = connectedMapped
.OrderBy(candidate => (rest[i].Pos - rest[candidate].Pos).LengthSquared())
.First();
detachedMapped.Add((i, anchor));
}
// A detached deform head may retain a small control-space bind pitch after the DCC
// constraint is lost. Infer its gaze only when one local axis already agrees closely
// with character-forward, then remove the small vertical component. Applying the
// correction in rest-local space preserves every solved animation delta.
var neutralHead = -1;
var neutralHeadCorrection = Quaternion.Identity;
foreach (var (bone, _) in detachedMapped)
{
if (rig.RoleOf(bone) == BoneRole.Head
&& TryDetachedHeadNeutralCorrection(rig, bone, out neutralHeadCorrection))
{
neutralHead = bone;
break;
}
}
if (tops.Count == 0 && detachedMapped.Count == 0)
return;
var anchorRestInverse = new Dictionary<int, XForm>();
foreach (var (_, anchor) in tops)
anchorRestInverse[anchor] = rest[anchor].Inverse();
foreach (var (_, anchor) in detachedMapped)
anchorRestInverse[anchor] = rest[anchor].Inverse();
foreach (var frame in frames)
{
var world = new Skeleton.Pose(frame).ToWorld(skeleton);
foreach (var (top, anchor) in tops)
{
var delta = XForm.Compose(world[anchor], anchorRestInverse[anchor]);
var parent = skeleton[top].ParentIndex;
var parentWorld = parent >= 0 ? world[parent] : XForm.Identity;
frame[top] = XForm.ToLocal(
parentWorld, XForm.Compose(delta, rest[top]));
}
if (detachedMapped.Count == 0)
continue;
// Re-evaluate after moving orphan control roots (the detached branch's actual
// parent may be inside one). Only position is anchored: its mapped rotation is
// the solver's result and must remain authoritative.
world = new Skeleton.Pose(frame).ToWorld(skeleton);
foreach (var (bone, anchor) in detachedMapped)
{
var delta = XForm.Compose(world[anchor], anchorRestInverse[anchor]);
var rotation = bone == neutralHead
? MathQ.Normalize(world[bone].Rot * neutralHeadCorrection)
: world[bone].Rot;
var desired = new XForm(
XForm.Compose(delta, rest[bone]).Pos,
rotation);
var parent = skeleton[bone].ParentIndex;
frame[bone] = parent < 0
? desired
: XForm.ToLocal(world[parent], desired);
}
}
if (tops.Count > 0)
AddNote(report,
$"{tops.Count} helper bone subtree(s) outside the mapped skeleton ride the "
+ "character (cloth/physics/prop bones would otherwise freeze at their bind position).");
if (detachedMapped.Count > 0)
AddNote(report,
$"{detachedMapped.Count} mapped body branch(es) on separate control hierarchies "
+ "follow the connected skeleton (exported DCC constraints are not available at runtime).");
if (neutralHead >= 0)
AddNote(report,
"Detached head bind pitch was leveled to the character facing plane while preserving animation.");
}
private static void MatchSourceFootHeights(
List<XForm[]> frames, SourceScene scene, MappingResult map, TargetRig rig,
(FootChain Left, FootChain Right) feet, Vector3 up, int take)
{
if (frames.Count == 0 || !map.RoleToBone.TryGetValue(BoneRole.Hips, out var srcHips)
|| rig.BoneForRole(BoneRole.Hips) is not int tgtHips)
return;
var source = scene.Skeleton;
var target = rig.Skeleton;
if (!scene.RestPlacementAuthored && !RestNormalizer.IsAnatomicalRest(source, map, source.RestWorld))
return; // Bone-length-only rests cannot calibrate an anatomical support height.
var srcFrames = scene.Clips[take].Frames;
if (srcFrames.Count != frames.Count)
return;
var srcUp = scene.UpAxis switch
{
0 => new Vector3(scene.UpAxisSign, 0, 0),
2 => new Vector3(0, 0, scene.UpAxisSign),
_ => new Vector3(0, scene.UpAxisSign, 0),
};
up = GroundUp(up);
var worlds = new XForm[frames.Count][];
var restGround = float.PositiveInfinity;
var motionGround = float.PositiveInfinity;
foreach (var rest in source.RestWorld)
restGround = MathF.Min(restGround, Vector3.Dot(rest.Pos, srcUp));
for (var f = 0; f < frames.Count; f++)
{
worlds[f] = new Skeleton.Pose(srcFrames[f]).ToWorld(source);
foreach (var bone in worlds[f])
motionGround = MathF.Min(motionGround, Vector3.Dot(bone.Pos, srcUp));
}
var placement = scene.RestPlacementAuthored ? 0f : motionGround - restGround;
var corrections = new List<(FootChain Chain, Vector3[] Goals)>();
var lowerPelvis = new float[frames.Count];
foreach (var (footRole, toeRole, chain) in new[]
{
(BoneRole.FootL, BoneRole.ToeL, feet.Left), (BoneRole.FootR, BoneRole.ToeR, feet.Right),
})
{
if (!map.RoleToBone.TryGetValue(footRole, out var foot)
|| !map.RoleToBone.TryGetValue(toeRole, out var toe) || chain.Toe is not int tgtToe)
continue;
float Support(IReadOnlyList<XForm> pose, int ankle, int tip, Vector3 vertical)
=> MathF.Min(Vector3.Dot(pose[ankle].Pos, vertical), Vector3.Dot(pose[tip].Pos, vertical));
var srcRest = Support(source.RestWorld, foot, toe, srcUp);
var tgtRest = Support(target.RestWorld, chain.Ankle, tgtToe, up);
// Animation-only FBXs can store a mid-stride rest, with one foot lifted.
// That raised foot is not a ground reference: subtracting it drives its
// planted frames below the floor. An authored rest never needs a higher
// support baseline than the lowest support actually reached by that foot.
if (scene.RestPlacementAuthored)
foreach (var pose in worlds)
srcRest = MathF.Min(srcRest, Support(pose, foot, toe, srcUp));
var srcHeight = Vector3.Dot(source.RestWorld[srcHips].Pos, srcUp) - srcRest;
if (srcHeight <= .001f)
continue;
var ratio = Math.Clamp((Vector3.Dot(target.RestWorld[tgtHips].Pos, up) - tgtRest) / srcHeight, .25f, 4f);
var goals = new Vector3[frames.Count];
for (var f = 0; f < frames.Count; f++)
{
var world = new Skeleton.Pose(frames[f]).ToWorld(target);
var desired = tgtRest + (Support(worlds[f], foot, toe, srcUp) - srcRest - placement) * ratio;
goals[f] = world[chain.Ankle].Pos + up * (desired - Support(world, chain.Ankle, tgtToe, up));
var reach = Vector3.Distance(world[chain.Hip].Pos, world[chain.Knee].Pos)
+ Vector3.Distance(world[chain.Knee].Pos, world[chain.Ankle].Pos);
var toHip = world[chain.Hip].Pos - goals[f];
if (scene.CaptureSpace is not null)
{
lowerPelvis[f] = MathF.Max(lowerPelvis[f], Motion.CaptureLegReach.Lowering(toHip, up, reach));
continue;
}
var vertical = Vector3.Dot(toHip, up);
var horizontal = toHip - up * vertical;
if (horizontal.LengthSquared() < reach * reach)
lowerPelvis[f] = MathF.Max(lowerPelvis[f], vertical
- MathF.Sqrt(reach * reach - horizontal.LengthSquared()));
}
corrections.Add((chain, goals));
}
// At full extension, lower the pelvis only as far as needed to reach BOTH
// contacts. Stretching bones or accepting an unreachable goal leaves a hover.
for (var f = 0; f < frames.Count; f++)
{
if (lowerPelvis[f] <= 0f) continue;
var parent = target[tgtHips].ParentIndex;
var shift = -up * lowerPelvis[f];
if (parent >= 0)
shift = Vector3.Transform(shift, Quaternion.Conjugate(
new Skeleton.Pose(frames[f]).ToWorld(target)[parent].Rot));
frames[f][tgtHips].Pos += shift;
}
foreach (var (chain, goals) in corrections)
{
// Vertical IK preserves horizontal travel and foot rotation; it never scales bones.
EffectorIk.ApplyGoals(frames, target,
new LimbChain { Upper = chain.Hip, Lower = chain.Knee, End = chain.Ankle },
goals, ArmIkCleanup.RestBendAxis(target, chain.Hip, chain.Knee, chain.Ankle), soften: 0f);
}
}
private static bool TryDetachedHeadNeutralCorrection(
TargetRig rig, int head, out Quaternion localCorrection)
{
localCorrection = Quaternion.Identity;
CharacterFrame frame;
try
{
frame = CharacterFrame.Compute(
rig.Skeleton, rig.ToMappingResult(), rig.Skeleton.RestWorld);
}
catch (ArgumentException)
{
return false;
}
var restRotation = MathQ.Normalize(rig.Skeleton.RestWorld[head].Rot);
var gaze = Vector3.Zero;
var agreement = float.NegativeInfinity;
foreach (var localAxis in new[] { Vector3.UnitX, Vector3.UnitY, Vector3.UnitZ })
{
var axis = Vector3.Transform(localAxis, restRotation);
var dot = Vector3.Dot(axis, frame.Forward);
if (MathF.Abs(dot) <= agreement)
continue;
agreement = MathF.Abs(dot);
gaze = dot < 0f ? -axis : axis;
}
var vertical = Vector3.Dot(gaze, frame.Up);
const float minAgreement = 0.94f; // local axis must be within 20° of facing
const float minPitch = 0.5f * MathF.PI / 180f;
const float maxPitch = 12f * MathF.PI / 180f;
var pitch = MathF.Asin(Math.Clamp(vertical, -1f, 1f));
if (agreement < minAgreement || MathF.Abs(pitch) < minPitch || MathF.Abs(pitch) > maxPitch)
return false;
var levelGaze = gaze - frame.Up * vertical;
if (levelGaze.LengthSquared() < 1e-8f)
return false;
levelGaze = Vector3.Normalize(levelGaze);
var worldCorrection = MathQ.FromTo(gaze, levelGaze);
localCorrection = MathQ.Normalize(
Quaternion.Conjugate(restRotation) * worldCorrection * restRotation);
return true;
}
/// <summary>
/// The engine-side yaw correction for embedded meshes and compiled Z-up targets (see
/// <see cref="EmitClip"/>): the compiler's source-axis conversion for ROOT-LEVEL
/// animation channels lands 90° about up away from where it puts the mesh bind.
/// Pre-rotating root locals by the inverse importer yaw (−90° for Y-up,
/// +90° for Z-up) makes compiled playback match the bind; children are parent-relative.
/// </summary>
private static List<XForm[]> CompensateEmbeddedMeshRootYaw(
IReadOnlyList<XForm[]> frames, TargetRig rig, TargetUpAxis upAxis)
{
var yUp = upAxis == TargetUpAxis.YUpCm;
var axis = yUp ? Vector3.UnitY : Vector3.UnitZ;
var yaw = Quaternion.CreateFromAxisAngle(axis, (yUp ? -1f : 1f) * MathF.PI * 0.5f);
var skeleton = rig.Skeleton;
var result = new List<XForm[]>(frames.Count);
foreach (var frame in frames)
{
var copy = new XForm[frame.Length];
for (var i = 0; i < frame.Length; i++)
{
copy[i] = i < skeleton.Count && skeleton[i].ParentIndex < 0
? new XForm(
Vector3.Transform(frame[i].Pos, yaw),
MathQ.Normalize(yaw * frame[i].Rot))
: frame[i];
}
result.Add(copy);
}
return result;
}
/// <summary>Test seam for <see cref="CompensateEmbeddedMeshRootYaw"/> (gate bisects
/// re-serialize mutated frames with the same compensation the pipeline applies).</summary>
public static List<XForm[]> TestHook_CompensateEmbeddedMeshRootYaw(
IReadOnlyList<XForm[]> frames, RetargetTargetSpec target)
=> !string.IsNullOrEmpty(target.MeshFilePath) || target.UpAxis == TargetUpAxis.ZUpEngine
? CompensateEmbeddedMeshRootYaw(frames, target.Rig, target.UpAxis)
: frames.ToList();
/// <summary>Test seam for <see cref="FollowHipsWithOrphanBones"/>.</summary>
public static void TestHook_FollowOrphans(TargetRig rig, List<XForm[]> frames)
=> FollowHipsWithOrphanBones(rig, frames, new MappingReportInfo
{
ProfileName = "test",
Source = MappingSource.Manual,
Confidence = 1f,
NeedsUserDecision = false,
MappedRoleCount = 0,
SkeletonSignature = "",
});
/// <summary>
/// <see cref="RetargetRequest.PreserveSourceTranslations"/>: per-frame source local
/// translations onto same-named target bones. Names are compared engine-sanitized
/// (non-alphanumerics → <c>_</c>: the compiled-model rig the editor rebuilds carries
/// <c>Bip01_R_Hand</c> for the file's <c>Bip01 R Hand</c>). A matched bone whose REST
/// translation disagrees between the rigs is skipped — the name collision is then
/// coincidence, not the same joint. The hips and its ancestors always stay
/// solver-owned (trajectory, root-motion modes, hip rescale).
/// </summary>
private static void PassThroughSourceTranslations(
SourceScene scene, TargetRig rig, List<XForm[]> frames, int take,
MappingReportInfo report)
{
var src = scene.Skeleton;
var tgt = rig.Skeleton;
var srcClip = scene.Clips[take];
if (srcClip.Frames.Count != frames.Count)
{
AddNote(report, "Source translations not preserved: frame counts differ "
+ $"({srcClip.Frames.Count} source vs {frames.Count} solved).");
return;
}
static string Key(string name)
{
Span<char> c = stackalloc char[name.Length];
for (var i = 0; i < name.Length; i++)
c[i] = char.IsLetterOrDigit(name[i]) ? char.ToLowerInvariant(name[i]) : '_';
return new string(c);
}
var srcByName = new Dictionary<string, int>();
for (var i = 0; i < src.Count; i++)
{
// Duplicate keys are ambiguous - poison them rather than guess.
var key = Key(src[i].Name);
srcByName[key] = srcByName.ContainsKey(key) ? -1 : i;
}
var excluded = new HashSet<int>();
if (rig.BoneForRole(BoneRole.Hips) is { } hips)
for (var b = hips; b >= 0; b = tgt[b].ParentIndex)
excluded.Add(b);
var applied = 0;
for (var t = 0; t < tgt.Count; t++)
{
if (excluded.Contains(t)
|| !srcByName.TryGetValue(Key(tgt[t].Name), out var s) || s < 0)
continue;
// Same joint check: rest translations must agree (same rig, same space).
var restDelta = (src[s].RestLocal.Pos - tgt[t].RestLocal.Pos).Length();
if (restDelta > MathF.Max(1f, 0.1f * tgt[t].RestLocal.Pos.Length()))
continue;
for (var f = 0; f < frames.Count; f++)
frames[f][t] = new XForm(srcClip.Frames[f][s].Pos, frames[f][t].Rot);
applied++;
}
if (applied > 0)
AddNote(report, $"Embedded take: authored local translations preserved on {applied} bones.");
}
/// <summary>
/// Normalized-rest foot pitch beyond which (toe approaching/above ankle level) the rest
/// cannot be a flat stance — measured stance rests sit at −11°…−51°; the repro artifact
/// rig measured +2.7° on one foot.
/// </summary>
private const float StancePitchMaxDeg = -5f;
/// <summary>Left/right normalized-rest foot pitch asymmetry beyond which the rest is not
/// a (symmetric) stance — measured stance rests differ ≤ 2.4°; the repro artifact rig
/// 13.8°.</summary>
private const float StancePitchAsymmetryDeg = 6f;
/// <summary>
/// The grounded-foot stance recalibration step (<see cref="FootGroundAlign"/>). Runs ONLY
/// when the source's NORMALIZED rest — the solver's delta reference — is implausible as a
/// flat stance (a foot's toe at/above ankle level, or the two feet resting asymmetrically;
/// both measured on the repro rig, where T-pose normalization of a bent-leg rest swings
/// the feet 14–26° away from the rig's true stance). On plausible stance rests the solver's
/// rest-relative foot transfer is already faithful, and planted-sole deviations are
/// genuine articulation (boxing stances, heel rolls) that must NOT be flattened. Plant
/// intervals are detected on the SOURCE clip (cm space, default Kovar thresholds).
/// Best-effort — silently skipped when the source maps no complete leg chains + toes or a
/// character frame is not computable (the foot transfer is then simply left as solved).
/// </summary>
private static void GroundAlignFeet(
List<XForm[]> frames, SourceScene scene, MappingResult map, SkeletonModel targetSkeleton,
(FootChain Left, FootChain Right) targetFeet, Vector3 targetUp, float fps, int take)
{
FootChain? SourceChain(BoneRole upper, BoneRole lower, BoneRole foot, BoneRole toe)
=> map.RoleToBone.TryGetValue(upper, out var hip)
&& map.RoleToBone.TryGetValue(lower, out var knee)
&& map.RoleToBone.TryGetValue(foot, out var ankle)
? new FootChain
{
Hip = hip,
Knee = knee,
Ankle = ankle,
Toe = map.RoleToBone.TryGetValue(toe, out var t) ? t : null,
}
: null;
var srcLeft = SourceChain(BoneRole.UpperLegL, BoneRole.LowerLegL, BoneRole.FootL, BoneRole.ToeL);
var srcRight = SourceChain(BoneRole.UpperLegR, BoneRole.LowerLegR, BoneRole.FootR, BoneRole.ToeR);
if (srcLeft?.Toe is not int srcToeL || srcRight?.Toe is not int srcToeR)
return; // toe-less sources take the solver's virtual-foot fallback instead
// ---- rest-stance plausibility (normalized rest, the solver's delta reference) ----
float pitchL, pitchR;
Vector3 srcUp;
try
{
var (srcNorm, _) = RestNormalizer.Normalize(scene.Skeleton, map);
var srcCanon = CanonicalFrames.Build(scene.Skeleton, map, srcNorm.WorldRest);
srcUp = Vector3.Normalize(srcCanon.CharacterUp);
float RestPitchDeg(int foot, int toe)
{
var dir = srcNorm.WorldRest[toe].Pos - srcNorm.WorldRest[foot].Pos;
if (dir.LengthSquared() < 1e-8f)
return 0f;
var s = Vector3.Dot(Vector3.Normalize(dir), srcUp);
return MathF.Asin(Math.Clamp(s, -1f, 1f)) * (180f / MathF.PI);
}
pitchL = RestPitchDeg(srcLeft.Ankle, srcToeL);
pitchR = RestPitchDeg(srcRight.Ankle, srcToeR);
}
catch (ArgumentException)
{
return;
}
var restIsStance = pitchL < StancePitchMaxDeg && pitchR < StancePitchMaxDeg
&& MathF.Abs(pitchL - pitchR) <= StancePitchAsymmetryDeg;
if (restIsStance)
return;
var srcFrames = scene.Clips[take].Frames;
var (plantsL, plantsR) = FootPlant.DetectPlantIntervals(
srcFrames, scene.Skeleton, srcLeft, srcRight, srcUp, fps);
if (plantsL.Count == 0 && plantsR.Count == 0)
return;
FootGroundAlign.Apply(
frames, targetSkeleton, targetFeet.Left, targetFeet.Right, targetUp,
plantsL, plantsR);
}
/// <summary>
/// Restores the clip's SOURCE-authored foot-to-ground relationship with ONE constant
/// vertical offset per clip (southpaw final regeneration package: DEFERRED.md f2 and
/// gate4_review.md 3.1). The solver's vertical reference is the source REST pelvis;
/// mid-pose exports whose kept node rest erased the authored crouch leave every solved
/// frame hovering by exactly the erased crouch depth (measured: stance +5.9 cm, steps
/// +7.8 to +10.3, slips ~+9, while the source FBX is grounded everywhere). Fix, the
/// generalization of the BVH ClipPlacementOffset vertical logic to every source: measure
/// the clip's minimum foot support (ankles + toes) in the SOLVED target frames and shift
/// the whole clip vertically so that support equals the TARGET rest support plus the
/// source clip's own authored support delta (source clip support minus source rest
/// support, hip-height scaled). One constant per clip preserves every within-clip lift,
/// heel raise and step cycle exactly: authored articulation is never flattened and an
/// authored jump stays airborne (its source support delta rides along).
/// </summary>
private static void AlignSupportToGround(
List<XForm[]> frames, SourceScene scene, MappingResult map, TargetRig rig,
(FootChain Left, FootChain Right) feet, Vector3 up, MappingReportInfo report, int take)
{
if (frames.Count == 0)
return;
var skeleton = rig.Skeleton;
var rest = skeleton.RestWorld;
var srcClip = scene.Clips[take];
var src = scene.Skeleton;
// The GROUND normal is the rig-space vertical axis, not the character-lean up:
// the character frame's up tilts a few degrees with the rest posture, and a
// tilted dot-metric slants the ground line across a traveling clip (measured:
// 28 cm of false offset on a long corpus walk). Snap to the dominant axis.
up = GroundUp(up);
// ---- support-bone set: the roles mapped on BOTH rigs (metrics must compare
// the same anatomical points or anthropometric offsets leak into the gap) ----
var supportRoles = new List<BoneRole>();
foreach (var role in new[] { BoneRole.FootL, BoneRole.FootR, BoneRole.ToeL, BoneRole.ToeR })
{
if (map.RoleToBone.ContainsKey(role) && rig.BoneForRole(role) is not null)
supportRoles.Add(role);
}
var sourceMeasured = supportRoles.Contains(BoneRole.FootL) && supportRoles.Contains(BoneRole.FootR)
&& srcClip.Frames.Count > 0 && srcClip.Frames[0].Length == src.Count;
if (!sourceMeasured)
{
// No reliable common support metric: fall back to the ankles the target
// chains provide and a zero source gap (best guess: grounded).
supportRoles.Clear();
}
var tgtSupportBones = new List<int>();
foreach (var role in supportRoles)
tgtSupportBones.Add(rig.BoneForRole(role)!.Value);
if (tgtSupportBones.Count == 0)
{
tgtSupportBones.Add(feet.Left.Ankle);
tgtSupportBones.Add(feet.Right.Ankle);
}
var tgtRestSupport = float.MaxValue;
foreach (var b in tgtSupportBones)
tgtRestSupport = MathF.Min(tgtRestSupport, Vector3.Dot(rest[b].Pos, up));
var solvedSupport = float.MaxValue;
foreach (var frame in frames)
{
var world = new Skeleton.Pose(frame).ToWorld(skeleton);
foreach (var b in tgtSupportBones)
solvedSupport = MathF.Min(solvedSupport, Vector3.Dot(world[b].Pos, up));
}
// ---- source-authored support gap --------------------------------------
float sourceRel = 0f;
if (sourceMeasured)
{
var srcSupportBones = new List<int>();
foreach (var role in supportRoles)
srcSupportBones.Add(map.RoleToBone[role]);
var srcUp = scene.UpAxis switch
{
0 => new Vector3(scene.UpAxisSign, 0f, 0f),
2 => new Vector3(0f, 0f, scene.UpAxisSign),
_ => new Vector3(0f, scene.UpAxisSign, 0f),
};
var srcHips = map.RoleToBone.TryGetValue(BoneRole.Hips, out var sh) ? sh : -1;
var srcRestSupport = float.MaxValue;
var srcRestGround = float.MaxValue;
for (var b = 0; b < src.Count; b++)
{
var h = Vector3.Dot(src.RestWorld[b].Pos, srcUp);
srcRestGround = MathF.Min(srcRestGround, h);
}
foreach (var b in srcSupportBones)
srcRestSupport = MathF.Min(srcRestSupport, Vector3.Dot(src.RestWorld[b].Pos, srcUp));
var srcClipSupport = float.MaxValue;
var srcMotionGround = float.MaxValue;
foreach (var frame in srcClip.Frames)
{
var world = new Skeleton.Pose(frame).ToWorld(src);
foreach (var b in srcSupportBones)
srcClipSupport = MathF.Min(srcClipSupport, Vector3.Dot(world[b].Pos, srcUp));
if (!scene.RestPlacementAuthored)
{
for (var b = 0; b < src.Count; b++)
srcMotionGround = MathF.Min(srcMotionGround, Vector3.Dot(world[b].Pos, srcUp));
}
}
// AUTHORED placements (FBX): the rest stands on the authored ground, so the
// gap is simply clip support minus rest support (same bones, same space).
// UN-PLACED sources (BVH capture volumes): absolute placement is meaningless,
// so both terms go placement-free: the clip's support above its OWN motion
// ground (lowest joint anywhere, incl. static origin markers) compared to the
// rest pose's support above ITS lowest joint. A grounded walk reads ~0, a
// jump reads its true airborne height, a crawl reads its raised feet.
float gap;
if (scene.RestPlacementAuthored)
gap = srcClipSupport - srcRestSupport;
else
gap = (srcClipSupport - srcMotionGround) - (srcRestSupport - srcRestGround);
// Hip-height ratio scales the (small) source gap into target units,
// clamped against degenerate rest measurements.
var ratio = 1f;
if (srcHips >= 0 && rig.BoneForRole(BoneRole.Hips) is { } tgtHips)
{
var srcHipHeight = Vector3.Dot(src.RestWorld[srcHips].Pos, srcUp) - srcRestSupport;
var tgtHipHeight = Vector3.Dot(rest[tgtHips].Pos, up) - tgtRestSupport;
if (srcHipHeight > 1e-3f && float.IsFinite(tgtHipHeight / srcHipHeight))
ratio = Math.Clamp(tgtHipHeight / srcHipHeight, 0.25f, 4f);
}
sourceRel = gap * ratio;
}
var delta = tgtRestSupport + sourceRel - solvedSupport;
if (!float.IsFinite(delta) || MathF.Abs(delta) > 100f)
{
AddNote(report, $"Support ground alignment skipped: implausible offset {delta:0.0}.");
return;
}
if (MathF.Abs(delta) < 0.01f)
return; // already aligned
var shift = up * delta;
foreach (var frame in frames)
{
for (var i = 0; i < skeleton.Count; i++)
{
if (skeleton[i].ParentIndex < 0)
frame[i] = new XForm(frame[i].Pos + shift, frame[i].Rot);
}
}
AddNote(report, sourceMeasured
? $"Support ground alignment: {delta:0.00} vertical offset applied "
+ $"(source-authored support delta {sourceRel:0.00})."
: $"Support ground alignment: {delta:0.00} vertical offset applied "
+ "(source feet unmapped; clip support aligned to target rest support).");
}
private static Vector3 GroundUp(Vector3 up)
{
var absolute = Vector3.Abs(up);
return absolute.X >= absolute.Y && absolute.X >= absolute.Z
? new Vector3(MathF.Sign(up.X), 0, 0)
: absolute.Y >= absolute.Z
? new Vector3(0, MathF.Sign(up.Y), 0)
: new Vector3(0, 0, MathF.Sign(up.Z));
}
private static void ApplyRootMotion(
RootMotionMode mode, List<XForm[]> frames, TargetContext context, MappingReportInfo report)
{
if (mode == RootMotionMode.Off)
return;
if (context.HipsIndex is not { } hips)
{
AddNote(report, $"Root motion ({mode}) skipped: target rig maps no Hips role.");
return;
}
if (context.Up is not { } up)
{
AddNote(report, $"Root motion ({mode}) skipped: {context.UpOrChainProblem}");
return;
}
if (mode == RootMotionMode.Extract && context.DedicatedRootIndex is null)
{
// s&box-style target: pelvis is parentless and there is no separate translating
// root bone (root_IK is IkBaked). Bone-level extraction is impossible — the vmdl
// AnimFile entry carries an ExtractMotion node instead (compile-time extraction).
AddNote(report,
"Root-motion extract: target has no dedicated animated root bone distinct from "
+ "the hips; frames left as solved and motion extraction delegated to the vmdl "
+ "AnimFile ExtractMotion node.");
return;
}
var root = context.DedicatedRootIndex ?? hips;
var worldUp = GroundUp(up);
// Skeleton-aware overload: hips world via real parent-chain FK, locals re-derived
// against the actual parent (HipsParentIsRoot is ignored by this overload).
RootMotion.Apply(frames, context.Rig.Skeleton, new RootMotionAxes
{
// Match the world vertical used by support-height cleanup. A slightly
// leaning target torso must not tilt the plane used to remove travel;
// otherwise this final pass reintroduces vertical drift into grounded feet.
Up = worldUp,
RootIndex = root,
HipsIndex = hips,
HipsParentIsRoot = context.HipsParentIsRoot,
}, mode);
if (mode == RootMotionMode.InPlace && frames.Count > 0)
{
// Animation-only files may start far from their static reference pose.
// Removing travel alone freezes that offset into the clip, so blending
// directions moves the mesh away from its controller. Center every clip
// on the target bind hips, preserving vertical motion and local rotations.
var skeleton = context.Rig.Skeleton;
var center = Vector3.Zero;
foreach (var frame in frames)
center += FkUtil.BoneWorld(frame, skeleton, hips).Pos;
var offset = skeleton.RestWorld[hips].Pos - center / frames.Count;
offset -= Vector3.Dot(offset, worldUp) * worldUp;
foreach (var frame in frames)
for (var b = 0; b < skeleton.Count; b++)
if (skeleton[b].ParentIndex < 0)
frame[b] = new XForm(frame[b].Pos + offset, frame[b].Rot);
}
AddNote(report, mode == RootMotionMode.Extract
? $"Root motion extracted onto dedicated root bone (index {root})."
: "Root motion removed (in-place clip).");
}
private static void AddNote(MappingReportInfo report, string note)
{
if (!report.Notes.Contains(note))
report.Notes.Add(note);
}
/// <summary>
/// Mid-pose FBX sources: adopts the importer's bind-rest alternate skeleton when it
/// makes the character stand STRAIGHTER along the file's own up axis. Solving on a
/// posed rest breaks the solver's anatomical reasoning on exactly the posed limbs
/// (measured 48° mean world-rot off ground truth on a real rig, left foot 124°); but
/// a bind rest can also tilt the character-up estimate (shoulders behind hips on one
/// real rig tilted it 7°, starving plant/stance detection), so the straighter rest
/// wins. Clips are authored playback either way — same bones, same order.
/// </summary>
private static SourceScene MaybeAdoptBindRest(
SourceScene scene, MappingResult map, MappingReportInfo report)
{
if (scene.MidPoseBindSkeleton is not { } bindSkeleton)
return scene;
var fileUp = scene.UpAxis switch
{
0 => new Vector3(scene.UpAxisSign, 0, 0),
2 => new Vector3(0, 0, scene.UpAxisSign),
_ => new Vector3(0, scene.UpAxisSign, 0),
};
float UpTiltDeg(SkeletonModel skeleton)
{
var (norm, _) = RestNormalizer.Normalize(skeleton, map);
var up = Vector3.Normalize(
CanonicalFrames.Build(skeleton, map, norm.WorldRest).CharacterUp);
return MathF.Acos(Math.Clamp(Vector3.Dot(up, fileUp), -1f, 1f)) * 180f / MathF.PI;
}
try
{
float tiltStatics = UpTiltDeg(scene.Skeleton);
float tiltBind = UpTiltDeg(bindSkeleton);
if (tiltBind > tiltStatics + 1f)
{
AddNote(report,
$"mid-pose export: kept the exported pose as rest - the file's bind "
+ $"data stands {tiltBind:0.#} deg off the file up axis "
+ $"(vs {tiltStatics:0.#} deg)");
return scene;
}
AddNote(report,
"mid-pose export: rest pose restored from the file's own bind data "
+ "(clips keep their authored playback)");
return new SourceScene(
bindSkeleton, scene.Clips, scene.UnitScaleCm,
scene.UpAxis, scene.UpAxisSign,
scene.FrontAxis, scene.FrontAxisSign,
scene.CoordAxis, scene.CoordAxisSign,
scene.OriginalUpAxis, scene.Notes)
{
AuthoredMapping = scene.AuthoredMapping,
RestPlacementAuthored = scene.RestPlacementAuthored,
};
}
catch (Exception)
{
// Anatomy not measurable (mapping lacks the frame's roles, degenerate rest):
// keep today's behavior.
return scene;
}
}
// ================================================================ import + mapping
/// <summary>
/// Imports source-file bytes exactly like conversion does: the extension picks the
/// importer; unknown extensions fall back to content sniffing (FBX binary magic / ASCII
/// header token / GLB 'glTF' magic / glTF JSON). Public so UI listings
/// inspect files through the SAME import path the pipeline uses (no duplicate sniffing
/// logic caller-side).
/// </summary>
/// <param name="data">Raw file bytes.</param>
/// <param name="fileName">File name; only the extension is consulted.</param>
/// <param name="sampleFps">Resample rate for the imported clips; null = importer default
/// (30 fps).</param>
/// <exception cref="FormatException">Thrown when the bytes are not a readable
/// motion, FBX or glTF animation.</exception>
public static SourceScene ImportSource(
byte[] data, string fileName, float? sampleFps = null,
Func<string, byte[]>? externalBufferResolver = null)
{
ArgumentNullException.ThrowIfNull(data);
ArgumentNullException.ThrowIfNull(fileName);
var fbxOptions = sampleFps is { } fbxFps ? new FbxImportOptions { SampleFps = fbxFps } : null;
var gltfOptions = sampleFps is not null || externalBufferResolver is not null
? new GltfImportOptions
{
SampleFps = sampleFps ?? 30f,
ExternalBufferResolver = externalBufferResolver,
}
: null;
var ext = ExtensionOf(fileName);
return ext switch
{
"hmotion" => Motion.MotionDocument.Parse(data).ToSourceScene(sampleFps),
"fbx" => FbxImporter.Import(data, fbxOptions),
// .vrm = glTF 2.0 GLB container + VRM extension (the importer reads the authored
// humanoid bone map into SourceScene.AuthoredMapping); unknown-extension VRM
// bytes also land here via the GLB magic sniff below.
"glb" or "gltf" or "vrm" => GltfImporter.Import(data, gltfOptions),
_ => SniffFormat(data) switch
{
"fbx" => FbxImporter.Import(data, fbxOptions),
"gltf" => GltfImporter.Import(data, gltfOptions),
_ => throw new FormatException(
$"Unrecognized source format for '{fileName}' (expected .hmotion, .fbx, .glb, .gltf or .vrm)."),
},
};
}
private static string? SniffFormat(byte[] data)
{
// Binary FBX magic: "Kaydara FBX Binary \0".
const string fbxMagic = "Kaydara FBX Binary";
if (StartsWithAscii(data, fbxMagic))
return "fbx";
// GLB magic: 'glTF' (0x46546C67 little-endian).
if (StartsWithAscii(data, "glTF"))
return "gltf";
var head = Encoding.UTF8.GetString(data, 0, Math.Min(data.Length, 4096));
if (head.Contains("FBXHeaderExtension", StringComparison.Ordinal))
return "fbx"; // ASCII FBX
var trimmed = head.TrimStart();
if (trimmed.StartsWith("{", StringComparison.Ordinal)
&& head.Contains("\"asset\"", StringComparison.Ordinal))
return "gltf"; // plain-JSON glTF
return null;
}
private static bool StartsWithAscii(byte[] data, string prefix)
{
if (data.Length < prefix.Length)
return false;
for (var i = 0; i < prefix.Length; i++)
{
if (data[i] != (byte)prefix[i])
return false;
}
return true;
}
/// <summary>
/// THE mapping cascade, shared by conversion, UI file listings, and custom-target
/// detection: explicit override → authored mapping (from the file itself, e.g. a VRM's
/// humanoid bone map — authoritative ground truth at confidence 1.0) → user preset (via
/// <paramref name="userPresetLookup"/>, keyed by <see cref="Mapping.SkeletonSignature"/>)
/// → shipped preset detection → best-effort auto-map. The report's
/// <see cref="MappingReportInfo.NeedsUserDecision"/> is true only on the auto path below
/// the preset detection threshold — conversion still proceeds with that map; callers
/// decide whether to ask/reject.
/// </summary>
/// <param name="skeleton">Source (or candidate target) skeleton.</param>
/// <param name="mappingOverride">Explicit mapping (manual table / already-resolved user
/// preset); wins outright when non-null — a deliberate user decision beats even the
/// file's own bone map.</param>
/// <param name="userPresetLookup">Editor-side user-preset hook: receives the skeleton's
/// signature, returns the stored mapping or null. The facade itself can do no file IO.</param>
/// <param name="authoredMapping">A mapping authored INSIDE the source file
/// (<see cref="SourceScene.AuthoredMapping"/>, <see cref="MappingSource.Authored"/>);
/// consulted before user presets because the file itself is authoritative.</param>
public static (MappingResult Map, MappingReportInfo Report) ResolveMapping(
SkeletonModel skeleton, MappingResult? mappingOverride = null,
Func<string, MappingResult?>? userPresetLookup = null,
MappingResult? authoredMapping = null)
{
ArgumentNullException.ThrowIfNull(skeleton);
if (mappingOverride is not null)
return (mappingOverride, BuildReport(mappingOverride, needsUserDecision: false, skeleton));
if (authoredMapping is not null)
return (authoredMapping, BuildReport(authoredMapping, needsUserDecision: false, skeleton));
if (userPresetLookup is not null
&& userPresetLookup(Mapping.SkeletonSignature.Compute(skeleton)) is { } userPreset)
return (userPreset, BuildReport(userPreset, needsUserDecision: false, skeleton));
if (ProfileDetector.Detect(skeleton) is { } detected)
{
AutoMapper.CompleteSingleJointThumbs(skeleton, detected.Result);
AutoMapper.CompleteFingersByTopology(skeleton, detected.Result);
AutoMapper.PruneNonArticulatedFingerStubs(skeleton, detected.Result);
VetoImpossibleHead(skeleton, detected.Result);
return (detected.Result, BuildReport(detected.Result, needsUserDecision: false, skeleton));
}
var auto = AutoMapper.Map(skeleton);
VetoImpossibleHead(skeleton, auto);
var needsUserDecision = auto.Confidence < ProfileDetector.DetectionThreshold;
return (auto, BuildReport(auto, needsUserDecision, skeleton));
}
/// <summary>
/// Drops a detected/auto-mapped Head whose bone cannot anatomically drive a skull: its
/// rest sits clearly BELOW the neck (or the topmost mapped spine joint). Real case: an
/// Auto-Rig Pro export parks <c>head.x</c> at chest height ~90cm under <c>neck.x</c>
/// with the skin bind compensating — the mesh is correct at rest, but every skull
/// rotation replayed onto that bone sweeps the head geometry on the lever arm ("the
/// head is stretched and it stretched more when I played the animation"). With the
/// role unmapped the neck — whose pivot IS at the skull base on such rigs — carries
/// head motion, and the bone holds its rest like any other unmapped helper.
/// Neutral-rest corpus rigs measure the neck→head segment −8°…33° from character up
/// (dot ≥ 0.55 even on the posed Defenses bind at 40.7°); the veto fires only past
/// 101° (dot < −0.2), an impossible carriage on any working rig. Explicit,
/// authored and user-preset mappings are authoritative and never second-guessed.
/// </summary>
private static void VetoImpossibleHead(SkeletonModel skeleton, MappingResult map)
{
if (!map.RoleToBone.TryGetValue(BoneRole.Head, out var head)
|| !map.RoleToBone.TryGetValue(BoneRole.Hips, out var hips))
return;
var reference = map.RoleToBone.TryGetValue(BoneRole.Neck, out var neck)
? neck
: SpineTop(map);
if (reference is not { } refBone || refBone == hips)
return;
var rest = skeleton.RestWorld;
// Axis-free "up": the body chain ascends hips → spine → neck by construction, so
// hips→reference is the rig's own vertical regardless of world axis convention.
var up = rest[refBone].Pos - rest[hips].Pos;
var seg = rest[head].Pos - rest[refBone].Pos;
if (up.LengthSquared() < 1e-6f || seg.Length() < 0.05f * up.Length())
return; // degenerate spine / head stacked on the neck: nothing to judge
if (Vector3.Dot(Vector3.Normalize(seg), Vector3.Normalize(up)) >= -0.2f)
return;
map.RoleToBone.Remove(BoneRole.Head);
// The neck of a broken skull region rests with the head. On the real case the
// neck bone skins the skull AND a torso column (its weights are where the head's
// should be), so any solved neck bend shears half the body: the source spreads
// its idle hunch through its spine while this target's spine stays put, and the
// faithful neck world replay turned into a ~19° LOCAL kink rendered as a hump
// grafted onto the character's back. Skull attitude rides the spine instead.
var neckNote = "";
if (map.RoleToBone.Remove(BoneRole.Neck))
neckNote = $" Its neck '{skeleton[neck].Name}' rests too (on such exports it "
+ "carries the skull/torso skin the head joint should).";
map.Notes.Add(
$"Head '{skeleton[head].Name}' unmapped: its rest sits below "
+ $"'{skeleton[refBone].Name}' - not a skull joint the solver can drive "
+ "(rotations would sweep the head geometry on the offset lever arm)."
+ neckNote + " The spine carries the upper body; map manually to override.");
}
private static int? SpineTop(MappingResult map)
{
foreach (var role in new[]
{
BoneRole.Spine4, BoneRole.Spine3, BoneRole.Spine2, BoneRole.Spine1, BoneRole.Spine0,
})
{
if (map.RoleToBone.TryGetValue(role, out var bone))
return bone;
}
return null;
}
private static MappingReportInfo BuildReport(
MappingResult map, bool needsUserDecision, SkeletonModel skeleton)
{
var report = new MappingReportInfo
{
ProfileName = map.ProfileName,
Source = map.Source,
Confidence = map.Confidence,
NeedsUserDecision = needsUserDecision,
MappedRoleCount = map.RoleToBone.Count,
SkeletonSignature = Mapping.SkeletonSignature.Compute(skeleton),
};
report.Notes.AddRange(map.Notes);
if (needsUserDecision)
report.Notes.Add(
$"No preset profile matched and auto-map confidence {map.Confidence:0.00} is below "
+ $"{ProfileDetector.DetectionThreshold:0.00}; proceeding with the best-effort auto map.");
return report;
}
// ================================================================ naming
private static string RequestedClipName(RetargetRequest request, SourceScene scene, int take)
{
var multipleTakes = scene.Clips.Count > 1;
if (!string.IsNullOrWhiteSpace(request.ClipNameOverride))
return multipleTakes ? $"{request.ClipNameOverride}_{take + 1}" : request.ClipNameOverride!;
var takeName = scene.Clips[take].Name;
if (!string.IsNullOrWhiteSpace(takeName) && takeName != "motion"
&& !takeName.Equals("mixamo.com", StringComparison.OrdinalIgnoreCase)
&& !IsExporterPlaceholderTake(takeName))
return takeName;
// BVH files (clip always named "motion"), Mixamo takes (always named
// "mixamo.com"), Unreal exports (every stack named "Unreal Take") and
// unnamed takes use the file stem.
var stem = FileStem(request.SourceFileName);
return multipleTakes ? $"{stem}_{take + 1}" : stem;
}
/// <summary>
/// True for take names that identify the EXPORTER rather than the animation. Unreal's
/// FBX exporter stamps <c>Unreal Take</c> into every AnimStack it writes (numbered —
/// <c>Unreal Take 001</c> — when a sequence exports several), so a folder of UE clips
/// arrives with one identical take name on every file. Treated as authored, that name
/// beats the file stem in <see cref="RequestedClipName"/> and the whole batch collapses
/// onto one clip name, which <see cref="UniqueClipName"/> then has to pull apart into
/// <c>Unreal_Take</c>, <c>Unreal_Take_2</c>, <c>Unreal_Take_3</c>… — every file's real
/// name lost, and the resulting sequence names carry no meaning in the vmdl. Falling
/// through to the file stem restores the same per-file naming BVH and Mixamo sources
/// already get. Matched as a PREFIX, and against the underscored spelling too, so the
/// numbered variants and any already-sanitized re-import are covered.
/// </summary>
private static bool IsExporterPlaceholderTake(string takeName)
{
var trimmed = takeName.Trim();
return trimmed.StartsWith("unreal take", StringComparison.OrdinalIgnoreCase)
|| trimmed.StartsWith("unreal_take", StringComparison.OrdinalIgnoreCase);
}
/// <summary>Collision auto-suffixing across the batch: <c>name</c>, <c>name_2</c>, …
/// (case-insensitive — the names also become DMX file names).</summary>
private static string UniqueClipName(string name, HashSet<string> usedNames, bool autoSuffix)
{
if (usedNames.Add(name) || !autoSuffix)
return name;
for (var i = 2; ; i++)
{
var candidate = $"{name}_{i}";
if (usedNames.Add(candidate))
return candidate;
}
}
/// <summary>
/// Clip names become ModelDoc AnimFile node names and Source 2 sequence names, which
/// must stay within <c>[A-Za-z0-9_]</c> — a take name like <c>mixamo.com</c> otherwise
/// fails the vmdl compile with "Node 'mixamo.com' resolve failure". Runs of invalid
/// characters collapse to a single underscore; case is preserved. Public so UIs that
/// dry-run name-based detection (e.g. the locomotion-set scan over raw take names) see
/// the SAME spelling conversion will produce — a take named <c>Walk N</c> becomes the
/// clip <c>Walk_N</c>.
/// </summary>
public static string SanitizeClipName(string name)
{
var builder = new StringBuilder(name.Length);
var lastWasUnderscore = false;
foreach (var c in name)
{
if (c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or (>= '0' and <= '9') or '_')
{
builder.Append(c);
lastWasUnderscore = c == '_';
}
else if (!lastWasUnderscore)
{
builder.Append('_');
lastWasUnderscore = true;
}
}
var sanitized = builder.ToString().Trim('_');
return sanitized.Length > 0 ? sanitized : "clip";
}
private static string FileStem(string fileName)
{
var start = Math.Max(fileName.LastIndexOf('/'), fileName.LastIndexOf('\\')) + 1;
var dot = fileName.LastIndexOf('.');
var end = dot > start ? dot : fileName.Length;
var stem = fileName.Substring(start, end - start);
return stem.Length > 0 ? stem : "clip";
}
private static string ExtensionOf(string fileName)
{
var dot = fileName.LastIndexOf('.');
if (dot < 0 || dot == fileName.Length - 1)
return "";
var ext = fileName.Substring(dot + 1);
return ext.IndexOfAny(new[] { '/', '\\' }) >= 0 ? "" : ext.ToLowerInvariant();
}
private static string SanitizeFileName(string name)
{
var builder = new StringBuilder(name.Length);
foreach (var c in name)
{
builder.Append(
c is (>= 'a' and <= 'z') or (>= '0' and <= '9') or '_' or '-' ? c
: c is >= 'A' and <= 'Z' ? char.ToLowerInvariant(c)
: '_');
}
var sanitized = builder.ToString().Trim('_');
return sanitized.Length > 0 ? sanitized : "clip";
}
private static string JoinAssetPath(string folder, string file)
{
var f = folder.Replace('\\', '/').TrimEnd('/');
return f.Length == 0 ? file : f + "/" + file;
}
// ================================================================ target context
/// <summary>Everything derived once per batch from the target rig: character up axis,
/// foot chains, hips/root indices, IK-bone presence.</summary>
internal sealed class TargetContext
{
public TargetRig Rig { get; }
/// <summary>Target character up direction (midShoulders − midHips on the rest pose,
/// via <see cref="CharacterFrame"/>); null when the rig lacks the bones for it.</summary>
public Vector3? Up { get; }
/// <summary>Target character frame (for the arm-IK basis change); null when not computable.</summary>
public CharacterFrame? Frame { get; }
/// <summary>Left/right leg chains from the rig's UpperLeg/LowerLeg/Foot/Toe roles;
/// null when either leg is incompletely mapped.</summary>
public (FootChain Left, FootChain Right)? FootChains { get; }
/// <summary>Why <see cref="Up"/> / <see cref="FootChains"/> are unavailable (report note text).</summary>
public string UpOrChainProblem { get; } = "";
/// <summary>Target bone carrying the Hips role.</summary>
public int? HipsIndex { get; }
/// <summary>Dedicated translating root: a parentless Animated bone that is an
/// ancestor of (and distinct from) the hips. Null on the s&box rig (pelvis is
/// itself parentless; root_IK is IkBaked).</summary>
public int? DedicatedRootIndex { get; }
/// <summary>True when the hips bone's direct parent is the dedicated root.</summary>
public bool HipsParentIsRoot { get; }
public bool HasIkBakedBones { get; }
/// <summary>ConstraintDriven bone indices (twist/helper) — excluded from DMX
/// channels per design §3; null when the rig has none. Face bones
/// (<see cref="SboxBoneClassifier.IsFaceBone"/>) are NOT in this set even though
/// their class is ConstraintDriven: they keep rest-local channels, because no
/// constraint re-drives them in a compiled sequence (channel-less face joints
/// bake statically — eyes detach from the moving head in ModelDoc).</summary>
public IReadOnlySet<int>? ConstraintDrivenBones { get; }
public TargetContext(TargetRig rig)
{
Rig = rig;
HipsIndex = rig.BoneForRole(BoneRole.Hips);
DedicatedRootIndex = FindDedicatedRoot(rig, HipsIndex);
HipsParentIsRoot = HipsIndex is { } hips && DedicatedRootIndex is { } root
&& rig.Skeleton[hips].ParentIndex == root;
foreach (var _ in rig.BonesOfClass(BoneClass.IkBaked))
{
HasIkBakedBones = true;
break;
}
var constraintDriven = new HashSet<int>(rig.BonesOfClass(BoneClass.ConstraintDriven));
// Face bones (eye_/ear_/face_) are ConstraintDriven by class but, unlike the
// twist/helper bones, nothing re-drives them when a compiled sequence plays:
// the model's AnimConstraintList never references them and the engine's eye
// look-at / blinking only runs in game. They must KEEP their rest-local DMX
// channels (the shipped fbx2dmx clips carry them too) or ModelDoc bakes the
// channel-less joints statically and the eyes detach from the moving head
// (see SboxBoneClassifier.IsFaceBone).
constraintDriven.RemoveWhere(i => SboxBoneClassifier.IsFaceBone(rig.Skeleton[i].Name));
ConstraintDrivenBones = constraintDriven.Count > 0 ? constraintDriven : null;
try
{
Frame = CharacterFrame.Compute(rig.Skeleton, rig.ToMappingResult(), rig.Skeleton.RestWorld);
Up = Frame.Up;
}
catch (ArgumentException e)
{
UpOrChainProblem = $"target character frame not computable ({e.Message})";
return;
}
var left = LegChain(rig, BoneRole.UpperLegL, BoneRole.LowerLegL, BoneRole.FootL, BoneRole.ToeL);
var right = LegChain(rig, BoneRole.UpperLegR, BoneRole.LowerLegR, BoneRole.FootR, BoneRole.ToeR);
if (left is not null && right is not null)
FootChains = (left, right);
else
UpOrChainProblem = "target rig does not map a complete UpperLeg/LowerLeg/Foot chain on both sides";
}
private static FootChain? LegChain(TargetRig rig, BoneRole upper, BoneRole lower, BoneRole foot, BoneRole toe)
=> rig.BoneForRole(upper) is { } hip
&& rig.BoneForRole(lower) is { } knee
&& rig.BoneForRole(foot) is { } ankle
? new FootChain { Hip = hip, Knee = knee, Ankle = ankle, Toe = rig.BoneForRole(toe) }
: null;
private static int? FindDedicatedRoot(TargetRig rig, int? hipsIndex)
{
if (hipsIndex is not { } hips)
return null;
// Walk up from the hips; the topmost ancestor qualifies when it is a parentless
// Animated bone distinct from the hips itself.
var top = hips;
while (rig.Skeleton[top].ParentIndex >= 0)
top = rig.Skeleton[top].ParentIndex;
return top != hips && rig.ClassOf(top) == BoneClass.Animated ? top : null;
}
}
// ================================================================ arm effector IK
/// <summary>
/// Optional wrist-reach cleanup: per frame, the source hand offset from its shoulder is
/// limb-length-normalized, re-expressed in the target's character basis, and used as a
/// two-bone IK goal for the target arm (<see cref="EffectorIk.ApplyGoals"/>).
/// </summary>
private static class ArmIkCleanup
{
/// <summary>Runs both arms; returns a problem note (and does nothing) when the
/// prerequisites are missing, null on success.</summary>
public static string? Apply(
List<XForm[]> frames, SourceScene scene, MappingResult map,
TargetRig rig, TargetContext context, int take)
{
if (context.Frame is not { } targetFrame)
return context.UpOrChainProblem;
CharacterFrame sourceFrame;
try
{
sourceFrame = CharacterFrame.Compute(scene.Skeleton, map, scene.Skeleton.RestWorld);
}
catch (ArgumentException e)
{
return $"source character frame not computable ({e.Message})";
}
// Change of basis source space → target space (this library's convention:
// a * b applies b first).
var sourceToTarget = MathQ.Normalize(
BasisRotation(targetFrame) * Quaternion.Conjugate(BasisRotation(sourceFrame)));
var appliedAny = false;
appliedAny |= ApplyArm(frames, scene, map, rig, sourceToTarget, take,
BoneRole.UpperArmL, BoneRole.LowerArmL, BoneRole.HandL);
appliedAny |= ApplyArm(frames, scene, map, rig, sourceToTarget, take,
BoneRole.UpperArmR, BoneRole.LowerArmR, BoneRole.HandR);
return appliedAny ? null : "no complete UpperArm/LowerArm/Hand chain mapped on source and target";
}
private static bool ApplyArm(
List<XForm[]> frames, SourceScene scene, MappingResult map, TargetRig rig,
Quaternion sourceToTarget, int take, BoneRole upperRole, BoneRole lowerRole, BoneRole handRole)
{
if (!map.RoleToBone.TryGetValue(upperRole, out var srcUpper)
|| !map.RoleToBone.TryGetValue(lowerRole, out var srcLower)
|| !map.RoleToBone.TryGetValue(handRole, out var srcHand))
return false;
if (rig.BoneForRole(upperRole) is not { } tgtUpper
|| rig.BoneForRole(lowerRole) is not { } tgtLower
|| rig.BoneForRole(handRole) is not { } tgtHand)
return false;
var src = scene.Skeleton;
var tgt = rig.Skeleton;
var srcLen = Vector3.Distance(src.RestWorld[srcUpper].Pos, src.RestWorld[srcLower].Pos)
+ Vector3.Distance(src.RestWorld[srcLower].Pos, src.RestWorld[srcHand].Pos);
var tgtLen = Vector3.Distance(tgt.RestWorld[tgtUpper].Pos, tgt.RestWorld[tgtLower].Pos)
+ Vector3.Distance(tgt.RestWorld[tgtLower].Pos, tgt.RestWorld[tgtHand].Pos);
if (srcLen < 1e-4f || tgtLen < 1e-4f)
return false;
var scale = tgtLen / srcLen;
var sourceFrames = scene.Clips[take].Frames;
var count = Math.Min(frames.Count, sourceFrames.Count);
var goals = new Vector3[frames.Count];
for (var f = 0; f < frames.Count; f++)
{
var srcWorld = new Pose(sourceFrames[Math.Min(f, count - 1)]).ToWorld(src);
var tgtWorld = new Pose(frames[f]).ToWorld(tgt);
var reach = srcWorld[srcHand].Pos - srcWorld[srcUpper].Pos;
goals[f] = tgtWorld[tgtUpper].Pos + Vector3.Transform(reach, sourceToTarget) * scale;
}
EffectorIk.ApplyGoals(
frames, tgt,
new LimbChain { Upper = tgtUpper, Lower = tgtLower, End = tgtHand },
goals, RestBendAxis(tgt, tgtUpper, tgtLower, tgtHand));
return true;
}
/// <summary>Hinge-axis fallback from the rest bend plane (elbow), like the foot pass.</summary>
internal static Vector3 RestBendAxis(SkeletonModel skeleton, int upper, int lower, int end)
{
var a = skeleton.RestWorld[upper].Pos;
var b = skeleton.RestWorld[lower].Pos;
var c = skeleton.RestWorld[end].Pos;
var axis = Vector3.Cross(c - a, b - a);
if (axis.LengthSquared() > 1e-6f)
return Vector3.Normalize(axis);
var limb = c - a;
axis = Vector3.Cross(limb, Vector3.UnitY);
if (axis.LengthSquared() < 1e-6f)
axis = Vector3.Cross(limb, Vector3.UnitZ);
return axis.LengthSquared() > 1e-12f ? Vector3.Normalize(axis) : Vector3.UnitX;
}
/// <summary>Rotation taking canonical character axes (X=lateral, Y=up, Z=forward)
/// to the rig's world axes.</summary>
private static Quaternion BasisRotation(CharacterFrame frame)
{
var m = new Matrix4x4(
frame.Lateral.X, frame.Lateral.Y, frame.Lateral.Z, 0f,
frame.Up.X, frame.Up.Y, frame.Up.Z, 0f,
frame.Forward.X, frame.Forward.Y, frame.Forward.Z, 0f,
0f, 0f, 0f, 1f);
return MathQ.Normalize(Quaternion.CreateFromRotationMatrix(m));
}
}
}
Game
library
#nullable enable annotations
using System;
using System.Collections.Generic;
using HumanoidMocapX.Core.Maths;
namespace HumanoidMocapX.Core.Skeleton;
/// <summary>
/// Immutable bone hierarchy with rest pose. Bones are stored topologically sorted
/// (every bone's parent precedes it), which lets forward kinematics run as a single
/// in-order pass. Supports multiple roots (e.g. the s&box rig has both
/// <c>root_IK</c> and <c>pelvis</c> as parentless bones).
/// </summary>
public sealed class Skeleton
{
private readonly Bone[] _bones;
private readonly XForm[] _restWorld;
private readonly Dictionary<string, int> _indexByName;
private Skeleton(Bone[] bones, XForm[] restWorld, Dictionary<string, int> indexByName)
{
_bones = bones;
_restWorld = restWorld;
_indexByName = indexByName;
}
/// <summary>Number of bones.</summary>
public int Count => _bones.Length;
/// <summary>All bones, topologically sorted (parents before children).</summary>
public IReadOnlyList<Bone> Bones => _bones;
/// <summary>Rest (bind) world transforms, indexed like <see cref="Bones"/>.</summary>
public IReadOnlyList<XForm> RestWorld => _restWorld;
/// <summary>Bone name to index lookup.</summary>
public IReadOnlyDictionary<string, int> IndexByName => _indexByName;
/// <summary>The bone at <paramref name="index"/>.</summary>
public Bone this[int index] => _bones[index];
/// <summary>Returns the index of the named bone, or -1 when absent.</summary>
public int IndexOf(string name) => _indexByName.TryGetValue(name, out var index) ? index : -1;
/// <summary>
/// Builds a skeleton from bone definitions in any order: validates names and parent links,
/// topologically sorts (stable — input order is preserved among bones whose parents are
/// already placed), and computes rest world transforms.
/// </summary>
/// <exception cref="ArgumentException">
/// Thrown on duplicate bone names, unknown parent names, or parent cycles.
/// </exception>
public static Skeleton Create(IReadOnlyList<BoneDefinition> definitions)
{
ArgumentNullException.ThrowIfNull(definitions);
var count = definitions.Count;
var originalIndexByName = new Dictionary<string, int>(count, StringComparer.Ordinal);
for (var i = 0; i < count; i++)
{
var name = definitions[i].Name;
if (string.IsNullOrEmpty(name))
throw new ArgumentException($"Bone at position {i} has an empty name.", nameof(definitions));
if (!originalIndexByName.TryAdd(name, i))
throw new ArgumentException($"Duplicate bone name '{name}'.", nameof(definitions));
}
foreach (var def in definitions)
{
if (def.ParentName is not null && !originalIndexByName.ContainsKey(def.ParentName))
throw new ArgumentException(
$"Bone '{def.Name}' references unknown parent '{def.ParentName}'.", nameof(definitions));
if (def.ParentName == def.Name)
throw new ArgumentException($"Bone '{def.Name}' is its own parent.", nameof(definitions));
}
// Stable topological sort: repeatedly place, in input order, every bone whose parent
// is already placed. O(n^2) worst case — fine for skeleton-sized inputs (~100 bones).
var newIndexByOriginal = new int[count];
Array.Fill(newIndexByOriginal, -1);
var order = new List<int>(count);
while (order.Count < count)
{
var progressed = false;
for (var i = 0; i < count; i++)
{
if (newIndexByOriginal[i] >= 0)
continue;
var parentName = definitions[i].ParentName;
if (parentName is not null && newIndexByOriginal[originalIndexByName[parentName]] < 0)
continue;
newIndexByOriginal[i] = order.Count;
order.Add(i);
progressed = true;
}
if (!progressed)
throw new ArgumentException("Bone hierarchy contains a parent cycle.", nameof(definitions));
}
var bones = new Bone[count];
var restWorld = new XForm[count];
var indexByName = new Dictionary<string, int>(count, StringComparer.Ordinal);
for (var newIndex = 0; newIndex < count; newIndex++)
{
var def = definitions[order[newIndex]];
var parentIndex = def.ParentName is null ? -1 : newIndexByOriginal[originalIndexByName[def.ParentName]];
bones[newIndex] = new Bone(newIndex, def.Name, parentIndex, def.RestLocal);
restWorld[newIndex] = parentIndex < 0
? def.RestLocal
: XForm.Compose(restWorld[parentIndex], def.RestLocal);
indexByName.Add(def.Name, newIndex);
}
return new Skeleton(bones, restWorld, indexByName);
}
}
Game
library
#nullable enable annotations
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
namespace HumanoidMocapX.Core.Target;
/// <summary>A KV3 boolean.</summary>
public sealed class KvBool : KvValue
{
/// <summary>The boolean value.</summary>
public bool Value { get; }
/// <summary>Creates a boolean value.</summary>
public KvBool(bool value) => Value = value;
}
Game
library
using System.Diagnostics;
using System.Text;
using System.Text.Json;
using HumanoidMocapX.Core;
using HumanoidMocapX.Core.Formats.Fbx;
using HumanoidMocapX.Core.Motion;
using HumanoidMocapX.Core.Target;
using HumanoidMocapX.EditorTools;
using HumanoidMocapX.Inference;
using Sandbox;
using static Sandbox.Internal.GlobalGameNamespace;
namespace HumanoidMocapX.Checks;
/// <summary>Runs the full model sequence on a 5–10-second predecoded video fixture.
/// This developer check excludes production decoding and visual motion acceptance.</summary>
public static class ClipChecks
{
public sealed record Fixture(string RgbaPath, int Width, int Height, int Frames, string SourceVideo,
string FlowPath, string SamPath, string MhrPath, string OmegaPath, string DetectorPath, string SmplPath, string RigDirectory,
int CameraInterval = 10, FlowModel Model = FlowModel.Latest);
/// <summary>Writes per-stage evidence, motion documents and Human/Citizen FBX
/// exports. Requires installed released weights and a fresh writable result directory.</summary>
public static async Task<object> Run(string fixturePath, string directory, CancellationToken cancellation)
{
var fixture = JsonSerializer.Deserialize<Fixture>(await File.ReadAllTextAsync(fixturePath, cancellation))
?? throw new ArgumentException("Missing clip fixture.");
if (fixture.Frames is < 150 or > 300) throw new ArgumentException("Clip validation requires 5–10 seconds at 30 fps.");
if (Directory.Exists(directory)) throw new ArgumentException("Use a fresh validation directory to avoid overwriting evidence.");
Directory.CreateDirectory(directory);
await File.WriteAllTextAsync(Path.Combine(directory, "fixture.json"), JsonSerializer.Serialize(fixture), cancellation);
var processor = Processor(fixture); using var frames = new RawFrames(fixture);
await Task.Run(() => FlowCheckpoint.LoadDecoder(fixture.FlowPath, fixture.SmplPath, cancellation), cancellation);
var timer = Stopwatch.StartNew(); var times = new Dictionary<string, double>();
var person = await processor.DetectAsync(frames, cancellation); times["Detection"] = timer.Elapsed.TotalSeconds;
if (person.First != 0 || person.Count != frames.Count) throw new InvalidDataException("The validation fixture must show the person in every frame.");
var boxes = person.Boxes; await Save("boxes.json", boxes); await Save("timings.json", times); timer.Restart();
var camera = await processor.CameraAsync(frames, cancellation); times["Camera"] = timer.Elapsed.TotalSeconds;
await Save("rotations.json", camera.Rotations); await Save("intrinsics.json", camera.Intrinsics); await Save("travel.json", camera.Travel); await Save("timings.json", times); timer.Restart();
var (features, observations) = await processor.FeaturesAsync(frames, boxes, camera.Intrinsics, cancellation); times["Features"] = timer.Elapsed.TotalSeconds;
await Save("features.json", features); await Save("observations.json", observations); await Save("timings.json", times); timer.Restart();
var motion = await processor.GenerateAsync(features, camera.Rotations, frames.Count, cancellation); times["Generation"] = timer.Elapsed.TotalSeconds;
var exports = await Export(fixture, motion, new ObservedDetail(observations, camera.Rotations, camera.Travel), directory, cancellation); await Save("timings.json", times);
var result = new { Passed = true, PredecodedFixture = true, Frames = frames.Count, Seconds = frames.Count / 30.0, StageSeconds = times, Exports = exports };
await Save("result.json", result);
return result;
Task Save<T>(string name, T value) => File.WriteAllTextAsync(Path.Combine(directory, name), JsonSerializer.Serialize(value), cancellation);
}
/// <summary>Repeats generation/export from this check's saved features. For
/// developer diagnosis only: the caller must preserve the same fixture and weights.</summary>
public static async Task<object> Generate(string directory, CancellationToken cancellation)
{
var fixture = JsonSerializer.Deserialize<Fixture>(await File.ReadAllTextAsync(Path.Combine(directory, "fixture.json"), cancellation));
var features = JsonSerializer.Deserialize<float[]>(await File.ReadAllTextAsync(Path.Combine(directory, "features.json"), cancellation));
var rotations = JsonSerializer.Deserialize<float[]>(await File.ReadAllTextAsync(Path.Combine(directory, "rotations.json"), cancellation));
var motion = await Processor(fixture).GenerateAsync(features, rotations, fixture.Frames, cancellation);
var observed = Path.Combine(directory, "observations.json");
var traveled = Path.Combine(directory, "travel.json");
var travel = File.Exists(traveled) ? JsonSerializer.Deserialize<float>(await File.ReadAllTextAsync(traveled, cancellation)) : float.NaN;
var detail = File.Exists(observed) ? new ObservedDetail(JsonSerializer.Deserialize<float[]>(await File.ReadAllTextAsync(observed, cancellation)), rotations, travel) : null;
return await Export(fixture, motion, detail, directory, cancellation);
}
static FlowVideoProcessor Processor(Fixture fixture) => new(new(fixture.FlowPath, fixture.SamPath, fixture.MhrPath, fixture.OmegaPath, fixture.DetectorPath, fixture.SmplPath),
new() { CameraInterval = fixture.CameraInterval, Model = fixture.Model }, message => Log.Info(message));
static Task<object> Export(Fixture fixture, FlowMotion motion, ObservedDetail detail, string directory, CancellationToken cancellation) => Task.Run<object>(async () =>
{
motion.Video = fixture.SourceVideo; motion.Name = Path.GetFileNameWithoutExtension(fixture.SourceVideo); var document = motion.ToDocument(); detail?.Apply(document);
var bytes = Encoding.UTF8.GetBytes(document.ToJson()); await File.WriteAllBytesAsync(Path.Combine(directory, "capture.hmotion"), bytes, cancellation);
await File.WriteAllTextAsync(Path.Combine(directory, "flow-motion.json"), JsonSerializer.Serialize(motion), cancellation);
var results = new List<object>();
foreach (var citizen in new[] { false, true })
{
cancellation.ThrowIfCancellationRequested(); var name = citizen ? "citizen" : "human";
var rig = await File.ReadAllTextAsync(Path.Combine(fixture.RigDirectory, citizen ? "target_rig_sbox_citizen.json" : "target_rig_sbox.json"), cancellation);
var target = citizen ? RetargetTargetSpec.SboxCitizen(rig) : RetargetTargetSpec.SboxDefault(rig);
var result = Retargeter.Convert(new RetargetRequest
{
SourceData = bytes,
SourceFileName = "capture.hmotion",
FootPlantCleanup = true,
MocapCorrections = new TargetCorrectionSettings { FirstPerson = false, StabilizeFeet = true }
}, target);
if (!result.Success) throw new Exception("Clip retarget failed: " + string.Join("; ", result.Errors.Concat(result.Clips.Select(c => c.Error))));
var clip = result.Clips.Single();
foreach (var poses in clip.SolvedFrames) foreach (var pose in poses)
if (!float.IsFinite(pose.Pos.Length()) || !float.IsFinite(pose.Rot.Length())) throw new Exception("Non-finite target animation.");
var times = Enumerable.Range(0, clip.SolvedFrames.Count).Select(i => i / (double)clip.Fps).ToArray();
var exported = ModelDocFbxExport.Write(target.Rig.Skeleton, motion.Name, clip.SolvedFrames, times, clip.Fps, target.UpAxis, "WorldRelative");
var roundTrip = FbxImporter.Import(exported);
if (roundTrip.Clips.Count != 1 || roundTrip.Clips[0].Frames.Count != clip.SolvedFrames.Count)
throw new Exception("Target FBX lost animation frames.");
var path = Path.Combine(directory, name + ".fbx"); await File.WriteAllBytesAsync(path, exported, cancellation);
results.Add(new { Target = name, Frames = clip.SolvedFrames.Count, Bones = target.Rig.Skeleton.Count, FbxBytes = exported.Length, Path = path });
}
return results;
}, cancellation);
sealed class RawFrames : ICaptureFrames, IDisposable
{
readonly FileStream stream;
readonly int bytes;
public int Width { get; }
public int Height { get; }
public int Count { get; }
internal RawFrames(Fixture fixture)
{
Width = fixture.Width; Height = fixture.Height; Count = fixture.Frames;
if (Width < 1 || Height < 1 || (long)Width * Height > 33_177_600) throw new ArgumentException("Invalid RGBA fixture dimensions.");
bytes = checked(Width * Height * 4);
stream = new(fixture.RgbaPath, FileMode.Open, FileAccess.Read, FileShare.Read, 1024 * 1024, FileOptions.Asynchronous | FileOptions.RandomAccess);
if (stream.Length != (long)bytes * Count) { stream.Dispose(); throw new ArgumentException("RGBA fixture length does not match its declared frame count."); }
}
public async ValueTask ReadAsync(int frame, Memory<byte> rgba, CancellationToken cancellation)
{
if ((uint)frame >= (uint)Count || rgba.Length != bytes) throw new ArgumentException("Invalid fixture frame or destination size.");
cancellation.ThrowIfCancellationRequested(); stream.Position = (long)frame * bytes;
await stream.ReadExactlyAsync(rgba, cancellation);
}
public void Dispose() => stream.Dispose();
}
}
Game
library
using System.Diagnostics;
using System.Text.Json;
using HumanoidMocapX.EditorTools;
using HumanoidMocapX.Inference;
using Sandbox;
namespace HumanoidMocapX.Checks;
/// <summary>Runs released YOLOX, SAM DINOv3, body decoder and MHR on a real frame.
/// This is an isolated body-pass diagnostic, not full video or hand refinement.</summary>
public static class SamFrameChecks
{
/// <summary>Writes crop, keypoint and feature evidence to a JSON file. Requires
/// a visible person in an RGBA8 frame and installed, verified model files.</summary>
public static async Task<object> Run(string samPath, string mhrPath, string detectorPath, string rgbaPath,
int width, int height, string resultPath, CancellationToken cancellation)
{
var pixels = await File.ReadAllBytesAsync(rgbaPath, cancellation);
var box = await Detect(pixels, width, height, detectorPath, cancellation);
var shape = DinoShape.Sam(); var crop = new SamCrop(box.Left, box.Top, box.Right, box.Bottom, shape);
var focal = (float)Math.Sqrt((double)width * width + (double)height * height);
var intrinsics = new float[] { focal, 0, width / 2f, 0, focal, height / 2f, 0, 0, 1 }; var affine = new float[6]; crop.CopyAffine(affine);
var rgb = new float[shape.ImageElements]; var rayEncoder = new SamRays(shape); var rays = new float[rayEncoder.Elements];
await Task.Run(() => { crop.Prepare(pixels, width, height, rgb, cancellation); rayEncoder.Prepare(intrinsics, affine, rays, cancellation); }, cancellation);
var context = new float[shape.Patches * shape.Channels]; var positions = new float[context.Length];
var imageTimes = await Encode(samPath, shape, rgb, rays, context, positions, cancellation);
var model = await Task.Run(() => MhrCheckpoint.LoadBody(mhrPath, cancellation), cancellation);
var pose = await Task.Run(() => SamCheckpoint.LoadPoseHead(samPath, model, false, cancellation), cancellation);
var decoderShape = new SamDecoderShape(SamInitialTokens.Count, shape.Patches);
var camera = await Task.Run(() => new SamPredictionHead(VisionCheckpoint.LoadSamHead(samPath, false, cancellation), false, true), cancellation);
var keypoints = await Task.Run(() => new SamKeypointFeedback(VisionCheckpoint.LoadSamFeedback(samPath, false, cancellation), decoderShape, 32, 32), cancellation);
var projection = new SamProjection(intrinsics, affine, crop.CenterX, crop.CenterY, crop.Size, shape);
var feedback = new SamDecoderFeedback(decoderShape, pose, camera, keypoints); feedback.Prepare(projection, context);
var tokens = new float[decoderShape.TokenElements]; var tokenPositions = new float[tokens.Length]; var decoded = new float[tokens.Length];
await Task.Run(() => new SamInitialTokens(VisionCheckpoint.LoadSamInitialTokens(samPath, false, cancellation))
.Run(projection, tokens, tokenPositions, cancellation), cancellation);
var decoderWeights = await Task.Run(() => VisionCheckpoint.LoadSamDecoder(samPath, false, cancellation), cancellation);
await GameTask.MainThread(); var decoder = new SamGpuDecoder(decoderWeights, decoderShape); var timer = Stopwatch.StartNew();
try
{
await decoder.RunAsync(tokens, context, tokenPositions, positions, decoded, feedback.Update, cancellation);
await Task.Run(() => feedback.Finish(decoded, cancellation), cancellation);
}
finally { await GameTask.MainThread(); decoder.Dispose(); }
var decodeMilliseconds = timer.Elapsed.TotalMilliseconds;
var cropped = new float[140]; var depths = new float[70]; var translation = new float[3]; var imagePoints = new float[140];
projection.Run(pose.Keypoints, feedback.CameraParameters, cropped, depths, translation);
for (var i = 0; i < 70; i++)
{
imagePoints[i * 2] = ((cropped[i * 2] + .5f) * shape.Width - affine[2]) / affine[0];
imagePoints[i * 2 + 1] = ((cropped[i * 2 + 1] + .5f) * shape.Height - affine[5]) / affine[4];
}
var result = new
{
BodyPassOnly = true,
Width = width,
Height = height,
Box = box,
Crop = new { crop.CenterX, crop.CenterY, crop.Size },
ImageMilliseconds = imageTimes,
DecodeMilliseconds = decodeMilliseconds,
ImageKeypoints = imagePoints,
KeypointsMetres = pose.Keypoints.ToArray(),
Depths = depths,
Translation = translation,
BodyFeature = decoded.AsSpan(0, decoderShape.Width).ToArray(),
ModelParameters = pose.Model.ToArray()
};
await File.WriteAllTextAsync(resultPath, JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }), cancellation);
return new
{
Passed = true,
BodyPassOnly = true,
Box = box,
Keypoints = 70,
BodyFeatureChannels = decoderShape.Width,
ImageMilliseconds = imageTimes,
DecodeMilliseconds = decodeMilliseconds,
ResultPath = resultPath
};
}
static async Task<YoloBox> Detect(byte[] pixels, int width, int height, string path, CancellationToken cancellation)
{
var network = new YoloNetwork(); var weights = await Task.Run(() => YoloCheckpoint.Load(path, network, cancellation), cancellation);
await GameTask.MainThread(); var detector = new YoloGpuDetector(network, weights);
try
{
var boxes = new YoloBox[YoloNetwork.Predictions]; var count = await detector.DetectAsync(pixels, width, height, boxes, cancellation);
if (count < 1) throw new Exception("SAM frame fixture contains no detected person.");
return boxes[0];
}
finally { await GameTask.MainThread(); detector.Dispose(); }
}
static async Task<double[]> Encode(string path, DinoShape shape, float[] rgb, float[] rays, float[] context, float[] positions, CancellationToken cancellation)
{
var backbone = await Task.Run(() => VisionCheckpoint.LoadSamBackbone(path, cancellation), cancellation);
var body = await Task.Run(() => VisionCheckpoint.LoadSamImageConditioning(path, false, cancellation), cancellation);
var hand = await Task.Run(() => VisionCheckpoint.LoadSamImageConditioning(path, true, cancellation), cancellation);
await GameTask.MainThread(); var encoder = new SamGpuImageEncoder(backbone, body, hand, shape);
try
{
var timer = Stopwatch.StartNew(); await encoder.RunAsync(rgb, rays, context, false, cancellation);
var first = timer.Elapsed.TotalMilliseconds; var previous = context.ToArray();
await GameTask.MainThread(); timer.Restart(); await encoder.RunAsync(rgb, rays, context, false, cancellation);
var repeated = timer.Elapsed.TotalMilliseconds;
if (!previous.SequenceEqual(context)) throw new Exception("SAM image encoding did not repeat exactly.");
encoder.CopyPositions(positions); return new[] { first, repeated };
}
finally { await GameTask.MainThread(); encoder.Dispose(); }
}
}
Editor
library
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Editor;
using Sandbox;
namespace HumanoidMocapX.EditorTools;
/// <summary>The strip across the top of the X window. With a provider: avatar, name, status pills,
/// account ID and limits, then icon buttons for the queue, adding videos and sign-out. On a processing
/// machine: its SSH address. On this computer: the GPU. Before setup: a pointer to settings. Missing
/// names and pictures show as not provided; account details and setup live in Mocap adjustments › Processing.</summary>
sealed class AccountBar : Widget
{
readonly HostingSettings settings;
readonly QueueTracker queue;
readonly AccountAvatar avatar;
readonly Label name, details, problem;
readonly Layout pills;
readonly Button signIn;
readonly BadgeButton queueButton;
readonly IconButton add, signOut;
CancellationTokenSource pending;
/// <summary>Provider mode when <paramref name="queue"/> is set, otherwise the processing machine
/// or this computer, or a pointer to settings while processing is not <paramref name="configured"/>.</summary>
internal AccountBar(Widget parent, HostingSettings settings, SshSettings machine, QueueTracker queue, bool configured, Action openSetup) : base(parent)
{
this.settings = settings; this.queue = queue;
Layout = Layout.Row(); Layout.Margin = new Sandbox.UI.Margin(12, 10, 12, 10); Layout.Spacing = 10;
avatar = Layout.Add(new AccountAvatar(this, 34));
var text = Layout.AddColumn(1); text.Spacing = 2;
var top = text.AddRow(); top.Spacing = 8;
name = top.Add(Ui.Bold(new Label("", this), 14));
pills = top.AddRow(); pills.Spacing = 6;
top.AddStretchCell();
details = text.Add(Ui.Muted(new Label("", this), small: true));
problem = Layout.Add(Ui.Error(this));
if (!configured)
{
avatar.ShowIcon("settings");
name.Text = "Not set up";
details.Text = "Choose where videos are processed: settings (top right) › Processing › Setup.";
return;
}
if (queue is null && machine.Enabled)
{
avatar.ShowIcon("dns");
name.Text = "Processing machine";
details.Text = $"{machine.Address} over SSH · videos are copied there for processing";
return;
}
if (queue is null)
{
avatar.ShowIcon("memory");
name.Text = "This computer";
var gpu = Sandbox.Engine.SystemInfo.Gpu is { Length: > 0 } gpuName && gpuName != "unset" ? gpuName : "This GPU";
details.Text = $"{gpu} · up to {FlowVideoProcessor.LaneCapacity()} frames at once · videos never leave this computer";
return;
}
signIn = Layout.Add(Ui.Primary("Sign in", "login", () => _ = SignInAsync(openSetup)));
queueButton = Layout.Add(new BadgeButton(this, "format_list_bulleted", OpenQueue, "Queue: your jobs on the provider"));
add = Layout.Add(Ui.Icon(this, "playlist_add", () => QueuePopup.AddVideos(queue), "Add videos to the queue"));
signOut = Layout.Add(Ui.Icon(this, "logout", () => _ = SignOutAsync(), "Sign out"));
settings.Changed += Refresh; queue.Changed += Refresh;
Refresh();
}
void OpenQueue()
{
var popup = new QueuePopup(this, queue);
popup.OpenAt(queueButton.ScreenRect.BottomLeft + new Vector2(queueButton.Width - popup.FixedWidth, 6));
}
void Refresh()
{
if (!this.IsValid()) return;
var profile = settings.SignedIn ? settings.Profile : null;
var label = EditorCookie.Get(ProcessingSetup.ProviderLabelCookie, "");
signIn.Visible = profile is null; signIn.Text = label.Length > 0 ? "Sign in with " + label : "Sign in";
queueButton.Visible = add.Visible = signOut.Visible = profile is not null;
queueButton.Count = queue.Active;
pills.Clear(true);
if (profile is null)
{
name.Text = "Signed out"; details.Text = Host(); avatar.ShowIcon("person");
return;
}
var user = profile.User;
name.Text = Clip(user.Name ?? user.Username, 32) ?? "Name not provided";
var (status, color) = profile switch
{
{ Admin: true } => ("Admin", Ui.Accent),
{ CanProcess: true } => ("Whitelisted", Ui.Accent),
{ InGuild: false } => ("Join the Discord server", Theme.Yellow),
_ => ("Not whitelisted", Theme.Red)
};
pills.Add(new Pill(this, status, color));
if (profile.Limits is { Priority: true }) pills.Add(new Pill(this, "Priority", Theme.Yellow));
details.Text = $"{Clip(user.Id, 40)} · {Host()}" + (profile.Limits is { } limit ? " · " + Describe(limit) : "");
details.ToolTip = user.Id + ": your account ID on this provider. Give it to the provider to be whitelisted. Click to copy.";
avatar.ShowAccount(user.Avatar, user.Name ?? user.Username);
}
protected override void OnMouseClick(MouseEvent e)
{
base.OnMouseClick(e);
// The account ID line copies the ID, so users can send it to the provider.
if (e.LeftMouseButton && details.IsUnderMouse && settings.Profile?.User.Id is { } id) EditorUtility.Clipboard.Copy(id);
}
static string Describe(HostLimits limit)
=> (limit.MaximumClipSeconds is { } clip ? $"clips up to {HostedCapture.Duration((int)clip)}" : "clips up to the provider's limit")
+ $" · uploads up to {limit.MaximumUploadBytes / (1024 * 1024)} MiB";
string Host()
{
try { return settings.Endpoint().Host; } catch (ArgumentException) { return "No provider address"; }
}
static string Clip(string text, int length) => text is { } value && value.Length > length ? value[..(length - 1)] + "…" : text;
async Task SignInAsync(Action openSetup)
{
if (pending is not null) return;
if (ProcessingSetup.SavedProvider() is not { } provider) { openSetup(); return; }
using var cancellation = new CancellationTokenSource(); pending = cancellation; var token = cancellation.Token;
Ui.Show(problem, null); signIn.Enabled = false; signIn.Text = "Finish in your browser…";
try { await ProcessingSetup.SignInAsync(settings, provider, token); }
catch (Exception failure) when (ProcessingSetup.IsSignInFailure(failure))
{
await EditorPipeline.SwitchToMainThread();
if (!token.IsCancellationRequested) Ui.Show(problem, failure is OperationCanceledException ? "Sign-in timed out. Try again." : failure.Message);
}
finally
{
await EditorPipeline.SwitchToMainThread();
pending = null;
if (signIn.IsValid()) { signIn.Enabled = true; Refresh(); }
}
}
async Task SignOutAsync()
{
using var client = new HostSignIn();
await client.SignOutAsync(settings);
}
public override void OnDestroyed()
{
settings.Changed -= Refresh; pending?.Cancel();
if (queue is not null) queue.Changed -= Refresh;
base.OnDestroyed();
}
protected override void OnPaint()
{
Paint.SetPen(Ui.CardEdge, 1);
Paint.DrawLine(new Vector2(0, Height - .5f), new Vector2(Width, Height - .5f));
}
}
UnitTest
library
using HumanoidMocapX.EditorTools;
using HumanoidMocapX.Inference;
static class SamBackboneTests
{
internal static void Run()
{
Dimensions(); Encoder(); Checkpoint();
Console.WriteLine("PASS: 32-layer SAM DINOv3 encoder, SwiGLU gates, per-axis positions, selective checkpoint loading and allocation-free reuse");
}
static void Dimensions()
{
var shape = DinoShape.Sam();
MotionTests.Equal(512, shape.Width); MotionTests.Equal(512, shape.Height);
MotionTests.Equal(1280, shape.Channels); MotionTests.Equal(20, shape.Heads); MotionTests.Equal(32, shape.Depth);
MotionTests.Equal(1024, shape.Patches); MotionTests.Equal(1029, shape.Tokens);
MotionTests.Throws<ArgumentException>(() => DinoShape.Sam(511, 512));
MotionTests.Throws<ArgumentException>(() => DinoShape.Sam(channels: 1281));
MotionTests.Throws<ArgumentException>(() => DinoShape.Sam(heads: 3));
var positions = OmegaPosition.Factors(2, 3, 5, new float[] { 1 }, true);
// Row zero's Y coordinate is -1/2; column zero's X coordinate is -2/3.
MotionTests.Near(-1, positions[5 * 4], 1e-6);
MotionTests.Near(Math.Cos(-4 * Math.PI / 3), positions[5 * 4 + 2], 1e-6);
}
static void Encoder()
{
var shape = DinoShape.Sam(32, 48, 8, 2); var tensors = OmegaFixture.Sam(shape.Channels, shape.Heads);
var encoder = new DinoImageEncoder(new(tensors), shape); var output = new float[shape.Patches * shape.Channels];
var image = OmegaVisionTests.Image(shape.ImageElements, 0);
for (var pass = 0; pass < 2; pass++)
{
if (pass > 0) Array.Reverse(image);
encoder.Run(image, output);
var expected = OmegaVisionReference.Image(tensors, image, shape.Height, shape.Width, shape.Channels, shape.Heads, true);
for (var i = 0; i < output.Length; i++) MotionTests.Near(expected[i], output[i], 2e-4);
}
for (var warm = 0; warm < 3; warm++) encoder.Run(image, output);
var before = GC.GetAllocatedBytesForCurrentThread(); encoder.Run(image, output);
MotionTests.Equal(0L, GC.GetAllocatedBytesForCurrentThread() - before);
using var cancelled = new CancellationTokenSource(); cancelled.Cancel();
MotionTests.Throws<OperationCanceledException>(() => encoder.Run(image, output, cancelled.Token));
MotionTests.Throws<ArgumentException>(() => encoder.Run(image, image.AsSpan(0, output.Length)));
image[0] = 1.1f; MotionTests.Throws<ArgumentException>(() => encoder.Run(image, output));
image[0] = float.NaN; MotionTests.Throws<ArgumentException>(() => encoder.Run(image, output));
var block = new OmegaBlock(new(tensors), "backbone.encoder.blocks.0", 8, 2, false, true);
var values = new float[shape.Tokens * shape.Channels];
MotionTests.Throws<ArgumentException>(() => block.Run(values, new(shape.Tokens, 8), new(shape.Tokens, 8, 2, false), default));
}
static void Checkpoint()
{
var path = Path.Combine(Path.GetTempPath(), "sam-backbone-" + Guid.NewGuid().ToString("N") + ".pt");
try
{
var tensors = OmegaFixture.Sam(8, 2);
tensors.Add("decoder.unused", new("decoder.unused", new[] { float.NaN }, new[] { 1, 1, 1, 1, 1 }));
foreach (var component in new string[] { null, "state_dict" })
{
CheckpointFixture.Write(path, tensors, component, ""); var loaded = VisionCheckpoint.LoadSamBackbone(path);
foreach (var pair in tensors.Where(pair => pair.Key.StartsWith("backbone.encoder.")))
MotionTests.Check(loaded.Get(pair.Key, pair.Value.Shape).Data.AsSpan().SequenceEqual(pair.Value.Data), "SAM backbone checkpoint changed tensor layout");
_ = new DinoImageEncoder(loaded, DinoShape.Sam(16, 32, 8, 2));
using var cancelled = new CancellationTokenSource(); cancelled.Cancel();
MotionTests.Throws<OperationCanceledException>(() => VisionCheckpoint.LoadSamBackbone(path, cancelled.Token));
File.Delete(path);
}
tensors["backbone.encoder.blocks.31.mlp.w3.bias"].Data[0] = float.NaN;
CheckpointFixture.Write(path, tensors, "state_dict", "");
MotionTests.Throws<InvalidDataException>(() => VisionCheckpoint.LoadSamBackbone(path));
}
finally { File.Delete(path); }
}
}
UnitTest
library
using HumanoidMocapX.Inference;
static class CaptureInputTests
{
internal static void Run()
{
var person = new YoloBox(10, 20, 110, 220, .8f); var other = new YoloBox(250, 10, 290, 100, .95f);
var track = new PersonTrack(5); track.Add([other, person]); track.Add([person with { Left = 12, Right = 112 }, other]);
track.Add([]); track.Add([person with { Left = 16, Right = 116 }]); track.Add([person with { Left = 18, Right = 118 }]);
var span = track.Complete(); var boxes = span.Boxes;
MotionTests.Equal(0, span.First); MotionTests.Equal(5, span.Count); MotionTests.Check(span.Note(2, 5) is null, "A fully visible person needs no note");
for (var i = 0; i < 5; i++) { MotionTests.Equal(10f + i * 2, boxes[i * 4]); MotionTests.Equal(110f + i * 2, boxes[i * 4 + 2]); }
MotionTests.Throws<InvalidOperationException>(() => track.Add([person]));
var nobody = new PersonTrack(2); nobody.Add([]); nobody.Add([]); MotionTests.Throws<InvalidOperationException>(() => nobody.Complete());
var unfinished = new PersonTrack(2); unfinished.Add([person]); MotionTests.Throws<InvalidOperationException>(() => unfinished.Complete());
// The capture starts where the person first appears and ends at their last detection.
var late = new PersonTrack(6); late.Add([]); late.Add([]); late.Add([person]); late.Add([]); late.Add([person with { Left = 14, Right = 114 }]); late.Add([]);
var visible = late.Complete();
MotionTests.Equal(2, visible.First); MotionTests.Equal(3, visible.Count); MotionTests.Equal(12, visible.Boxes.Length);
MotionTests.Equal(12f, visible.Boxes[4]); MotionTests.Equal(14f, visible.Boxes[8]); MotionTests.Near(2 / 30d, visible.StartSeconds);
MotionTests.Equal("The person is in view from 1.07 s to 1.17 s of the video, so only that part was captured.", visible.Note(1, 6));
var uncertain = new PersonTrack(2); uncertain.Add([person]);
MotionTests.Throws<InvalidOperationException>(() => uncertain.Add([person, person with { Left = 11, Right = 111 }]));
// A person lost for more than half a second ends the capture; a later return is not joined on.
var lost = new PersonTrack(18); lost.Add([person]); for (var i = 0; i < 15; i++) lost.Add([]);
lost.Add([other]); lost.Add([person]);
var kept = lost.Complete(); MotionTests.Equal(0, kept.First); MotionTests.Equal(1, kept.Count);
MotionTests.Throws<ArgumentException>(() => new PersonTrack(1).Add([person with { Left = float.NaN }]));
var reuse = new PersonTrack(20); var input = new[] { person }; for (var i = 0; i < 4; i++) reuse.Add(input);
var before = GC.GetAllocatedBytesForCurrentThread(); reuse.Add(input); MotionTests.Equal(0L, GC.GetAllocatedBytesForCurrentThread() - before);
var noise = new float[491 * 501]; var repeated = new float[noise.Length]; FlowNoise.Fill(noise, 0); FlowNoise.Fill(repeated, 0);
MotionTests.Check(noise.SequenceEqual(repeated), "Initial noise must repeat for the same seed");
var mean = noise.Average(v => (double)v); var variance = noise.Average(v => (double)v * v) - mean * mean;
MotionTests.Near(0, mean, .01); MotionTests.Near(1, variance, .02);
MotionTests.Check(noise.Min() < -3 && noise.Max() > 3 && noise.All(float.IsFinite), "Normal noise must cover both tails and remain finite");
FlowNoise.Fill(repeated, 1); MotionTests.Check(!noise.SequenceEqual(repeated), "Different seeds must vary initial noise");
using var stop = new CancellationTokenSource(); stop.Cancel();
MotionTests.Throws<OperationCanceledException>(() => FlowNoise.Fill(noise, 0, stop.Token));
MotionTests.Throws<ArgumentException>(() => FlowNoise.Fill(new float[490], 0));
Console.WriteLine("PASS: person identity continuity, short-gap interpolation, ambiguous/lost-person rejection, allocation-free tracking and seeded standard-normal latents");
}
}
UnitTest
library
using System.Numerics;
using HumanoidMocapX.Core.Cleanup;
using HumanoidMocapX.Core.Maths;
using HumanoidMocapX.Core.Skeleton;
static class CcdIkTests
{
internal static void Run()
{
var skeleton = Skeleton.Create(new[]
{
new BoneDefinition("root", null, XForm.Identity),
new BoneDefinition("elbow", "root", new XForm(Vector3.UnitX, Quaternion.Identity)),
new BoneDefinition("hand", "elbow", new XForm(Vector3.UnitY, Quaternion.Identity)),
new BoneDefinition("finger", "hand", new XForm(Vector3.UnitY * .2f, Quaternion.CreateFromAxisAngle(Vector3.UnitX, .4f)))
});
var rest = skeleton.Bones.Select(b => b.RestLocal).ToArray();
var solver = new CcdIk(skeleton, new[] { 0, 1 }, 2);
var pose = (XForm[])rest.Clone();
var residual = solver.Solve(pose, new Vector3(.7f, 1.3f, .4f), iterations: 128, tolerance: 1e-5f);
MotionTests.Check(residual < 1e-4, $"CCD must reach a reachable target; residual {residual}");
Invariants(rest, pose);
pose = (XForm[])rest.Clone();
residual = solver.Solve(pose, new Vector3(20, 0, 0), iterations: 128);
MotionTests.Near(18, residual, .001);
Invariants(rest, pose);
var locked = (XForm[])rest.Clone();
var limits = new LockedJoint(rest);
solver.Solve(locked, new Vector3(-1, 1, 0), constrain: limits.Apply);
MotionTests.Near(0, MathQ.AngleBetween(rest[0].Rot, locked[0].Rot), 1e-6);
MotionTests.Check(limits.Calls > 0, "CCD must enforce the supplied joint constraint");
var offsetSolver = new CcdIk(skeleton, new[] { 0 }, 2);
var offset = new Vector3(.1f, .3f, 0);
var offsetGoal = Vector3.Transform(new Vector3(1, 1, 0) + offset,
Quaternion.CreateFromAxisAngle(Vector3.UnitZ, .4f));
pose = (XForm[])rest.Clone();
MotionTests.Check(offsetSolver.Solve(pose, offsetGoal, tipOffset: offset) < .001f, "CCD tip offset must reach the target");
Reject(() => new CcdIk(skeleton, new[] { 1, 0 }, 2));
Reject(() => new CcdIk(skeleton, new[] { -1 }, 2));
Reject(() => solver.Solve(rest, new Vector3(float.NaN, 0, 0)));
Reject(() => solver.Solve(rest, Vector3.Zero, iterations: 0));
Reject(() => solver.Solve(rest, Vector3.Zero, gain: float.NaN));
var invalid = (XForm[])rest.Clone(); invalid[1].Rot = default;
Reject(() => solver.Solve(invalid, Vector3.Zero));
pose = (XForm[])rest.Clone();
for (var i = 0; i < 10; i++) solver.Solve(pose, new Vector3(.7f, 1.3f, .4f));
var before = GC.GetAllocatedBytesForCurrentThread();
for (var i = 0; i < 100; i++) solver.Solve(pose, new Vector3(.7f, 1.3f, .4f));
MotionTests.Check(GC.GetAllocatedBytesForCurrentThread() == before, "CCD solve must not allocate");
Console.WriteLine("PASS: DigitalRune CCD adaptation, reach residuals, constraints, offsets, validation and zero-allocation solve");
}
static void Invariants(XForm[] before, XForm[] after)
{
for (var i = 0; i < before.Length; i++) MotionTests.Near(0, Vector3.Distance(before[i].Pos, after[i].Pos));
for (var i = 2; i < before.Length; i++) MotionTests.Near(0, MathQ.AngleBetween(before[i].Rot, after[i].Rot));
}
static void Reject(Action action)
{
try { action(); } catch (ArgumentException) { return; }
throw new Exception("CCD accepted an invalid chain or input");
}
sealed class LockedJoint(XForm[] reference)
{
internal int Calls;
internal void Apply(XForm[] pose, int bone) { Calls++; if (bone == 0) pose[bone].Rot = reference[bone].Rot; }
}
}
Editor
library
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Editor;
using HumanoidMocapX.EditorTools.Video;
using HumanoidMocapX;
using HumanoidMocapX.EditorTools;
using Sandbox;
namespace HumanoidMocapHost;
/// <summary>Opt-in processing host inside the s&box editor. Operator configuration
/// comes from MOCAP_* environment variables; see docs/editor-host.md.</summary>
public static class EditorHost
{
// The engine exposes static console/event entry points; session state stays here.
static readonly SemaphoreSlim Changes = new(1, 1);
static EditorHostLifetime active;
static CancellationTokenSource starting;
/// <summary>Verifies installed Base/Latest weights, then listens on loopback. Requires
/// Windows Media Foundation and a GPU budget. Startup failures propagate to the console.</summary>
[ConCmd("mocap_x_host_start", ConVarFlags.Protected, Help = "Start the configured in-editor Humanoid Mocap X processing host")]
public static async Task StartAsync()
{
await Changes.WaitAsync();
using var cancellation = new CancellationTokenSource();
starting = cancellation;
try
{
if (active is { IsRunning: true }) { Log.Info("Humanoid Mocap X host is already running."); return; }
if (active is not null) { await active.StopAsync(); active = null; }
if (LocalCapture.Availability is { } reason) throw new PlatformNotSupportedException(reason);
var config = HostConfiguration.Read(Environment.GetEnvironmentVariable,
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "HumanoidMocapX"));
await ModelStore.InstalledAsync(config.Models, FlowModel.Base, message => Log.Info(message), cancellation.Token);
await ModelStore.InstalledAsync(config.Models, FlowModel.Latest, message => Log.Info(message), cancellation.Token);
await GameTask.MainThread(); cancellation.Token.ThrowIfCancellationRequested();
if (!Graphics.IsAvailable || Graphics.VideoMemoryBudget == 0 && Sandbox.Engine.SystemInfo.GpuMemory == 0)
throw new InvalidOperationException("The host needs an initialized GPU with a reported video-memory budget.");
active = Start(config);
Log.Info($"Humanoid Mocap X host listening on http://127.0.0.1:{config.Port}/. Stop with mocap_x_host_stop.");
}
finally { starting = null; Changes.Release(); }
}
static EditorHostLifetime Start(HostConfiguration config)
{
var lanes = config.Lanes == CaptureLanes.Auto ? CaptureLanes.Maximum : config.Lanes;
var admission = new GpuAdmission(lanes);
admission.Update(Graphics.VideoMemoryBudget, Graphics.VideoMemoryUsed, Sandbox.Engine.SystemInfo.GpuMemory);
var storage = new FileJobStorage(config.Data);
try
{
var processor = new CaptureProcessor(storage.CaptureDirectory, config.UploadBytes,
(selected, workspace, progress) => new LocalCapture(new CaptureOptions
{ Model = selected.Model, CameraInterval = selected.CameraInterval, Lanes = lanes }, config.Models, workspace, progress),
ReadDurationAsync);
var queue = new CaptureQueue(storage, processor, TimeProvider.System, config.ActivePerOwner,
maximumUploadBytes: config.UploadBytes, maximumClipSeconds: config.ClipSeconds,
concurrentJobs: config.ConcurrentJobs, canStartJob: admission.CanStart);
var session = new HostSession($"http://127.0.0.1:{config.Port}/", config.Keys, storage, queue, ReportError);
return new EditorHostLifetime(session, admission);
}
catch { storage.Dispose(); throw; }
}
static Task<double> ReadDurationAsync(string video, CancellationToken token) => Task.Run(() =>
{
token.ThrowIfCancellationRequested();
using var decoder = IVideoDecoder.Open(video);
return decoder.Duration;
}, token);
/// <summary>Cancels startup, uploads and active captures, then waits for GPU cleanup.
/// Repeated calls are safe; completed job IDs are invalidated by the next start.</summary>
[ConCmd("mocap_x_host_stop", ConVarFlags.Protected, Help = "Stop and drain the in-editor Humanoid Mocap X processing host")]
public static async Task StopAsync()
{
try { starting?.Cancel(); } catch (ObjectDisposedException) { } // Startup just finished.
await Changes.WaitAsync();
try
{
await GameTask.MainThread();
if (active is not null) { await active.StopAsync(); active = null; }
}
finally { Changes.Release(); }
}
[EditorEvent.Hotload]
static void Hotloaded() => _ = StopAfterHotloadAsync();
static async Task StopAfterHotloadAsync()
{
try { await StopAsync(); }
catch (Exception error) { ReportError(error); }
}
static void ReportError(Exception error) => Log.Error($"Humanoid Mocap X host: {error}");
}
Editor
library
#nullable enable annotations
using System;
using System.Threading;
using System.Threading.Tasks;
namespace HumanoidMocapHost;
/// <summary>Owns one HTTP listener, queue loop and file-store lifetime. Shutdown drains
/// uploads and GPU captures before disposing their resources.</summary>
internal sealed class HostSession : IAsyncDisposable
{
readonly object gate = new();
readonly CancellationTokenSource stopping = new();
readonly FileJobStorage storage;
readonly CaptureQueue queue;
readonly HttpHost http;
readonly Task worker;
readonly Action<Exception> reportError;
Task? shutdown;
internal bool IsStopping { get { lock (gate) return shutdown is not null; } }
internal HostSession(string prefix, ApiKeys keys, FileJobStorage storage, CaptureQueue queue, Action<Exception> reportError)
{
this.storage = storage; this.queue = queue; this.reportError = reportError;
try { http = new HttpHost(prefix, keys, queue, reportError); }
catch { stopping.Dispose(); queue.Dispose(); storage.Dispose(); throw; }
worker = Task.Run(() => QueueLoop.RunAsync(queue, (id, error) => reportError(new InvalidOperationException($"Capture {id} failed.", error)), stopping.Token));
_ = ObserveAsync();
}
async Task ObserveAsync()
{
try { await worker; }
catch (Exception error) { reportError(error); }
finally
{
try { await StopAsync(); }
catch (Exception error) { reportError(error); }
}
}
/// <summary>Idempotently stops this instance. The returned task completes only after
/// cancellation and resource cleanup; callers must await it before starting a replacement.</summary>
internal Task StopAsync()
{
lock (gate) return shutdown ??= StopCoreAsync();
}
async Task StopCoreAsync()
{
await stopping.CancelAsync();
try { await http.DisposeAsync(); }
finally
{
try { await worker; }
finally
{
try { queue.Dispose(); }
finally
{
try { storage.DeleteRun(); }
finally { storage.Dispose(); stopping.Dispose(); }
}
}
}
}
public ValueTask DisposeAsync() => new(StopAsync());
}
Debug: View Raw JSON Response
{
"TotalCount": 609,
"Files": [
{
"Ident": "notpointless.chomnr_humanoid_mocap_x",
"Path": "FlowSampler.cs",
"FileName": "FlowSampler.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 439808,
"IsPrivate": false,
"Code": "// C# adaptation of FlowHMR's x1 Euler sampling. See THIRD_PARTY_NOTICES.md.\nusing System;\nusing System.Threading;\n\nnamespace HumanoidMocapX;\n\n/// <summary>Predicts the clean 491-channel motion latent at a normalized time in [0,1).\n/// Writes every output element; failures and cancellation propagate.</summary>\npublic delegate void FlowPrediction(ReadOnlySpan<float> state, float time, Span<float> output, CancellationToken cancellation);\n\n/// <summary>The released Base and Latest models use twenty Euler steps and x1 prediction.</summary>\npublic static class FlowSampler\n{\n public const int Channels = 491;\n public const int Steps = 20;\n\n /// <summary>Replaces caller-supplied initial noise with sampled motion using a reusable\n /// prediction buffer. Rejects invalid shapes and non-finite predictions. A failed or\n /// cancelled run may leave state partially advanced; it must not be exported.</summary>\n public static void Sample(float[] state, float[] prediction, FlowPrediction predict,\n CancellationToken cancellation = default)\n {\n ArgumentNullException.ThrowIfNull(state);\n ArgumentNullException.ThrowIfNull(prediction);\n ArgumentNullException.ThrowIfNull(predict);\n if (state.Length == 0 || state.Length % Channels != 0 || state.Length / Channels > FlowMotion.MaximumFrames\n || prediction.Length != state.Length || ReferenceEquals(state, prediction))\n throw new ArgumentException(\"Sampling needs separate, equally sized [frames,491] state and prediction buffers.\");\n CheckFinite(state);\n const float minimumRemaining = 1f / Steps;\n for (var step = 0; step < Steps; step++)\n {\n cancellation.ThrowIfCancellationRequested();\n var time = step / (float)Steps;\n var delta = (step + 1) / (float)Steps - time;\n predict(state, time, prediction, cancellation);\n cancellation.ThrowIfCancellationRequested();\n CheckFinite(prediction);\n var remaining = Math.Max(1f - time, minimumRemaining);\n for (var i = 0; i < state.Length; i++)\n state[i] += (prediction[i] - state[i]) / remaining * delta;\n CheckFinite(state);\n }\n }\n\n static void CheckFinite(ReadOnlySpan<float> values)\n {\n foreach (var value in values)\n if (!float.IsFinite(value)) throw new ArithmeticException(\"FlowHMR sampling produced or received a non-finite latent.\");\n }\n}\n"
},
{
"Ident": "notpointless.chomnr_humanoid_mocap_x",
"Path": "HostedCaptureOptions.cs",
"FileName": "HostedCaptureOptions.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 439808,
"IsPrivate": false,
"Code": "using System;\n\nnamespace HumanoidMocapX;\n\n/// <summary>Video-range and model settings sent to a processing host. Times are source seconds.</summary>\npublic sealed record HostedCaptureOptions(FlowModel Model, int CameraInterval, double Start, double? End)\n{\n /// <summary>Rejects unsupported models, sampling intervals and invalid time ranges.</summary>\n public void Validate()\n {\n new CaptureOptions { Model = Model, CameraInterval = CameraInterval }.Validate();\n if (!double.IsFinite(Start) || Start < 0 || End is double end && (!double.IsFinite(end) || end <= Start))\n throw new ArgumentException(\"The video range needs a finite, nonnegative start and a later end.\");\n }\n}\n\n/// <summary>Protocol version, processor readiness, the upload limit in bytes, how many unfinished\n/// jobs one account may have, how many jobs wait and run across all accounts, how many process at\n/// once, and the longest video range in seconds. A provider's site reports the signed-in account's\n/// own limits and whether its jobs wait in the priority queue.</summary>\npublic sealed record HostedCapabilities(int Protocol, bool ProcessingAvailable, long MaximumUploadBytes,\n int MaximumActiveJobs = 1, int QueueLength = 0, int Processing = 0, int ConcurrentJobs = 1,\n double? MaximumClipSeconds = null, bool Priority = false);\n\n/// <summary>Job state returned by the host. QueuePosition is one-based while queued, out of\n/// QueueLength waiting jobs; Progress (0 to 1) is set while processing. SecondsUntilStart and\n/// SecondsRemaining are the host's estimates. ExpiresAt is when a finished job and its result\n/// are deleted. Priority jobs wait ahead of the others. IDs are opaque; result URLs are derived locally.</summary>\n#nullable enable annotations\npublic sealed record HostedJob(Guid Id, string State, string Message, int? QueuePosition = null, string? Name = null,\n DateTimeOffset CreatedAt = default, int QueueLength = 0, double? Progress = null, int? SecondsUntilStart = null,\n int? SecondsRemaining = null, DateTimeOffset? ExpiresAt = null, bool Priority = false);\n#nullable restore\n"
},
{
"Ident": "notpointless.chomnr_humanoid_mocap_x",
"Path": "HumanoidMocap/Core/Formats/Fbx/FbxImportOptions.cs",
"FileName": "FbxImportOptions.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 439808,
"IsPrivate": false,
"Code": "#nullable enable annotations\n\nusing System;\nusing System.Collections.Generic;\nusing System.Numerics;\nusing System.Threading;\nusing HumanoidMocapX.Core.Maths;\nusing HumanoidMocapX.Core.Skeleton;\n\nnamespace HumanoidMocapX.Core.Formats.Fbx;\n\nusing Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidMocap/Assembly.cs)\n\n/// <summary>Options for <see cref=\"FbxImporter.Import\"/>.</summary>\npublic sealed class FbxImportOptions\n{\n /// <summary>Fixed resampling rate for all clips, frames per second.</summary>\n public float SampleFps { get; init; } = 30f;\n public long MaximumTransformSamples { get; init; } = long.MaxValue;\n public CancellationToken CancellationToken { get; init; }\n\n /// <summary>\n /// When the static rest pose is degenerate (Mixamo-style zeroed bind translations) and no\n /// usable BindPose node exists, sample frame 0 of the first clip as the rest pose.\n /// </summary>\n public bool RestFromFrame0WhenBindDegenerate { get; init; } = true;\n}\n"
},
{
"Ident": "notpointless.chomnr_humanoid_mocap_x",
"Path": "HumanoidMocap/Core/Formats/Fbx/FbxTransform.cs",
"FileName": "FbxTransform.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 439808,
"IsPrivate": false,
"Code": "#nullable enable annotations\n\nusing System;\nusing System.Numerics;\nusing HumanoidMocapX.Core.Maths;\n\nnamespace HumanoidMocapX.Core.Formats.Fbx;\n\nusing Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidMocap/Assembly.cs)\n\n/// <summary>\n/// Per-node FBX local-transform evaluation: the static pivot/pre-rotation data of one Model\n/// plus the full FBX transform formula.\n/// </summary>\n/// <remarks>\n/// The FBX SDK formula (column-vector convention, right-multiply children):\n/// <code>\n/// World = ParentWorld \u00b7 T \u00b7 Roff \u00b7 Rp \u00b7 Rpre \u00b7 R \u00b7 Rpost\u207b\u00b9 \u00b7 Rp\u207b\u00b9 \u00b7 Soff \u00b7 Sp \u00b7 S \u00b7 Sp\u207b\u00b9\n/// </code>\n/// System.Numerics matrices are row-vector (<c>p' = p\u00b7M</c>), so the local matrix is composed\n/// in reverse order with row-form factors:\n/// <code>\n/// Local = T(-Sp)\u00b7S\u00b7T(Sp)\u00b7T(Soff)\u00b7T(-Rp)\u00b7R(Rpre\u00b7R\u00b7Rpost\u207b\u00b9)\u00b7T(Rp)\u00b7T(Roff)\u00b7T(t)\n/// World = Local \u00b7 ParentWorld\n/// </code>\n/// <para><b>RotationOrder</b> (0=XYZ, 1=XZY, 2=YZX, 3=YXZ, 4=ZXY, 5=ZYX): the letters give the\n/// application order, first letter applied first \u2014 eEulerXYZ composes <c>R = Rz\u00b7Ry\u00b7Rx</c> in\n/// column convention (verified against Blender-imported ground truth of Mixamo and citizen\n/// rigs). Pre/Post rotations always use XYZ order and are only applied when\n/// <c>RotationActive</c> is set (matching Blender's importer; defaults to active when the\n/// property is absent everywhere).</para>\n/// <para><b>InheritType</b> (0=RrSs, 1=RSrs, 2=Rrs): scale is dropped to rigid transforms by\n/// this importer \u2014 world matrices are evaluated with full scale, then decomposed to pos+rot\n/// with scale folded into child translations \u2014 so types 0 and 1 are handled identically.\n/// Files that use non-uniform scale meaningfully with InheritType 2 (no parent scale\n/// propagation) are evaluated as type 0; this is a documented limitation.</para>\n/// </remarks>\npublic sealed class FbxTransform\n{\n /// <summary>Static \"Lcl Translation\" default (native units).</summary>\n public Vector3 LclTranslation { get; init; }\n\n /// <summary>Static \"Lcl Rotation\" default, euler degrees in <see cref=\"RotationOrder\"/>.</summary>\n public Vector3 LclRotationDeg { get; init; }\n\n /// <summary>Static \"Lcl Scaling\" default.</summary>\n public Vector3 LclScaling { get; init; } = Vector3.One;\n\n /// <summary>PreRotation as a quaternion (identity when inactive or absent).</summary>\n public Quaternion PreRotation { get; init; } = Quaternion.Identity;\n\n /// <summary>PostRotation as a quaternion (identity when inactive or absent).</summary>\n public Quaternion PostRotation { get; init; } = Quaternion.Identity;\n\n /// <summary>RotationOffset (Roff).</summary>\n public Vector3 RotationOffset { get; init; }\n\n /// <summary>RotationPivot (Rp).</summary>\n public Vector3 RotationPivot { get; init; }\n\n /// <summary>ScalingOffset (Soff).</summary>\n public Vector3 ScalingOffset { get; init; }\n\n /// <summary>ScalingPivot (Sp).</summary>\n public Vector3 ScalingPivot { get; init; }\n\n /// <summary>Euler application order code, 0=XYZ \u2026 5=ZYX (see class remarks).</summary>\n public int RotationOrder { get; init; }\n\n /// <summary>InheritType 0=RrSs, 1=RSrs, 2=Rrs (see class remarks for handling).</summary>\n public int InheritType { get; init; }\n\n /// <summary>Reads the static transform data of a Model from its (template-backed) properties.</summary>\n public static FbxTransform FromModel(FbxScene scene, FbxObject model)\n {\n ArgumentNullException.ThrowIfNull(scene);\n ArgumentNullException.ThrowIfNull(model);\n\n // Pre/Post rotation participate only when RotationActive is set (Blender-compatible).\n // When the property is absent on both the object and its template, default to active \u2014\n // exporters that strip RotationActive but keep PreRotation expect it applied.\n bool rotationActive = scene.FindProperty(model, \"RotationActive\") is { } ra\n ? ra.GetInt() != 0\n : true;\n\n var pre = scene.GetVector3(model, \"PreRotation\", Vector3.Zero);\n var post = scene.GetVector3(model, \"PostRotation\", Vector3.Zero);\n\n return new FbxTransform\n {\n LclTranslation = scene.GetVector3(model, \"Lcl Translation\", Vector3.Zero),\n LclRotationDeg = scene.GetVector3(model, \"Lcl Rotation\", Vector3.Zero),\n LclScaling = scene.GetVector3(model, \"Lcl Scaling\", Vector3.One),\n PreRotation = rotationActive ? EulerDegreesToQuaternion(pre, 0) : Quaternion.Identity,\n PostRotation = rotationActive ? EulerDegreesToQuaternion(post, 0) : Quaternion.Identity,\n RotationOffset = scene.GetVector3(model, \"RotationOffset\", Vector3.Zero),\n RotationPivot = scene.GetVector3(model, \"RotationPivot\", Vector3.Zero),\n ScalingOffset = scene.GetVector3(model, \"ScalingOffset\", Vector3.Zero),\n ScalingPivot = scene.GetVector3(model, \"ScalingPivot\", Vector3.Zero),\n RotationOrder = scene.GetInt(model, \"RotationOrder\", 0),\n InheritType = scene.GetInt(model, \"InheritType\", 0),\n };\n }\n\n /// <summary>\n /// Local matrix (row-vector layout) for given TRS values; pass the Lcl defaults for the\n /// rest pose or per-frame sampled values for animation.\n /// </summary>\n public Matrix4x4 LocalMatrix(Vector3 translation, Vector3 rotationDeg, Vector3 scaling)\n {\n // Column chain R-part: Rpre \u00b7 R \u00b7 Rpost\u207b\u00b9 \u2192 our quaternion convention (a*b applies\n // b first) composes the same way.\n var rotation = MathQ.Normalize(\n PreRotation *\n EulerDegreesToQuaternion(rotationDeg, RotationOrder) *\n Quaternion.Conjugate(PostRotation));\n\n // Row-vector composition, first-applied factor leftmost (see class remarks).\n var m = Matrix4x4.CreateTranslation(-ScalingPivot);\n m *= Matrix4x4.CreateScale(scaling);\n m *= Matrix4x4.CreateTranslation(ScalingPivot + ScalingOffset - RotationPivot);\n m *= Matrix4x4.CreateFromQuaternion(rotation);\n m *= Matrix4x4.CreateTranslation(RotationPivot + RotationOffset + translation);\n return m;\n }\n\n /// <summary>Local matrix from the static Lcl defaults (rest evaluation, no animation).</summary>\n public Matrix4x4 LocalMatrixDefault() => LocalMatrix(LclTranslation, LclRotationDeg, LclScaling);\n\n /// <summary>\n /// Converts FBX euler angles (degrees) to a quaternion for the given RotationOrder code.\n /// The order letters list the application sequence (XYZ = X first, then Y, then Z).\n /// </summary>\n public static Quaternion EulerDegreesToQuaternion(Vector3 degrees, int order)\n {\n const float degToRad = MathF.PI / 180f;\n var qx = Quaternion.CreateFromAxisAngle(Vector3.UnitX, degrees.X * degToRad);\n var qy = Quaternion.CreateFromAxisAngle(Vector3.UnitY, degrees.Y * degToRad);\n var qz = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, degrees.Z * degToRad);\n\n // q = third * second * first (our convention applies the right factor first).\n return order switch\n {\n 0 => qz * qy * qx, // XYZ\n 1 => qy * qz * qx, // XZY\n 2 => qx * qz * qy, // YZX\n 3 => qz * qx * qy, // YXZ\n 4 => qy * qx * qz, // ZXY\n 5 => qx * qy * qz, // ZYX\n 6 => qz * qy * qx, // eSphericXYZ \u2014 treated as XYZ\n _ => throw new FormatException($\"Unsupported FBX RotationOrder {order}.\"),\n };\n }\n\n /// <summary>\n /// Extracts the rigid part of a (possibly scaled) world matrix: translation taken directly,\n /// rotation from the orthonormalized basis. Scale is thereby folded into child translations\n /// when parent-relative transforms are derived from rigid worlds. Mirrored (negative\n /// determinant) bases fall back to the dominant proper rotation (best effort \u2014 mirrored\n /// skeletons are not meaningfully supported).\n /// </summary>\n public static XForm ToRigid(in Matrix4x4 world)\n {\n var pos = world.Translation;\n\n var x = new Vector3(world.M11, world.M12, world.M13);\n var y = new Vector3(world.M21, world.M22, world.M23);\n var z = new Vector3(world.M31, world.M32, world.M33);\n\n x = SafeNormalize(x, Vector3.UnitX);\n y = SafeNormalize(y, Vector3.UnitY);\n z = SafeNormalize(z, Vector3.UnitZ);\n\n // Mirrored basis: flip the third axis to restore a proper rotation.\n if (Vector3.Dot(Vector3.Cross(x, y), z) < 0f)\n z = -z;\n\n var m = new Matrix4x4(\n x.X, x.Y, x.Z, 0f,\n y.X, y.Y, y.Z, 0f,\n z.X, z.Y, z.Z, 0f,\n 0f, 0f, 0f, 1f);\n\n return new XForm(pos, MathQ.Normalize(Quaternion.CreateFromRotationMatrix(m)));\n }\n\n private static Vector3 SafeNormalize(Vector3 v, Vector3 fallback)\n {\n float lsq = v.LengthSquared();\n if (!float.IsFinite(lsq) || lsq < 1e-20f)\n return fallback;\n return v / MathF.Sqrt(lsq);\n }\n}\n"
},
{
"Ident": "notpointless.chomnr_humanoid_mocap_x",
"Path": "HumanoidMocap/Core/Formats/Fbx/ModelDocFbxExport.cs",
"FileName": "ModelDocFbxExport.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 439808,
"IsPrivate": false,
"Code": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing HumanoidMocapX.Core.Maths;\nusing HumanoidMocapX.Core.Skeleton;\n\nnamespace HumanoidMocapX.Core.Formats.Fbx;\nusing SkeletonModel = HumanoidMocapX.Core.Skeleton.Skeleton;\n\n/// <summary>FBX with the source-to-engine scale baked into every translation for direct ModelDoc import.</summary>\npublic static class ModelDocFbxExport\n{\n\t/// <summary>Uses Humanoid Retargeter's source scale for centimetre rigs; compiled engine rigs already use inches.</summary>\n\tpublic static byte[] Write( SkeletonModel skeleton, string name, IReadOnlyList<XForm[]> frames,\n\t\tIReadOnlyList<double> times, double fps, TargetUpAxis axis, string motionSpace )\n\t{\n\t\tArgumentNullException.ThrowIfNull( skeleton );\n\t\tArgumentNullException.ThrowIfNull( frames );\n\t\tif ( frames.Count is < 1 or > 108000 || skeleton.Count is < 1 or > 1024\n\t\t\t|| (long)frames.Count * skeleton.Count > FbxAnimationWriter.MaximumTransformSamples )\n\t\t\tthrow new ArgumentException( \"Animation exceeds the export budget. Select a shorter range.\" );\n\t\tif ( axis is not (TargetUpAxis.YUpCm or TargetUpAxis.ZUpEngine) )\n\t\t\tthrow new ArgumentOutOfRangeException( nameof( axis ) );\n\n\t\t// Like Retargeter's ModelModifier_ScaleAndMirror, this converts raw source values.\n\t\t// UnitScaleFactor alone is insufficient for ModelDoc's raw-unit import. Bake the\n\t\t// factor into bind positions, all local translations, root motion and appended props.\n\t\tif ( axis == TargetUpAxis.YUpCm )\n\t\t{\n\t\t\tXForm Scale( XForm pose ) => new( pose.Pos * RetargetTargetSpec.SboxSourceScale, pose.Rot );\n\t\t\tskeleton = SkeletonModel.Create( skeleton.Bones.Select( bone => new BoneDefinition(\n\t\t\t\tbone.Name, bone.ParentIndex < 0 ? null : skeleton[ bone.ParentIndex ].Name,\n\t\t\t\tScale( bone.RestLocal ) ) ).ToArray() );\n\t\t\tframes = frames.Select( frame => frame.Select( Scale ).ToArray() ).ToArray();\n\t\t}\n\n\t\treturn FbxAnimationWriter.Write( skeleton, name, frames, times, fps,\n\t\t\taxis == TargetUpAxis.YUpCm ? 1 : 2, 2.54, motionSpace );\n\t}\n}\n"
},
{
"Ident": "notpointless.chomnr_humanoid_mocap_x",
"Path": "HumanoidMocap/Core/InspectResult.cs",
"FileName": "InspectResult.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 439808,
"IsPrivate": false,
"Code": "#nullable enable annotations\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing HumanoidMocapX.Core.Mapping;\nusing HumanoidMocapX.Core.Maths;\nusing HumanoidMocapX.Core.Target;\nusing HumanoidMocapX.Core.Skeleton;\n\nnamespace HumanoidMocapX.Core;\n\n/// <summary>\n/// Result of <see cref=\"Retargeter.Inspect\"/>: the mapping report plus the file's take\n/// metadata, so UI listings can expand a multi-take file into one entry per take (each\n/// convertible independently via <see cref=\"RetargetRequest.TakeIndex\"/>).\n/// </summary>\npublic sealed class InspectResult\n{\n /// <summary>Mapping report from the same preset-then-auto cascade conversion uses.</summary>\n public required MappingReportInfo Mapping { get; init; }\n\n /// <summary>Names of the file's animation takes, in take-index order (the index is what\n /// <see cref=\"RetargetRequest.TakeIndex\"/> accepts). Empty when the file has no animation.</summary>\n public required IReadOnlyList<string> TakeNames { get; init; }\n\n /// <summary>Number of animation takes in the file.</summary>\n public int TakeCount => TakeNames.Count;\n}\n"
},
{
"Ident": "notpointless.chomnr_humanoid_mocap_x",
"Path": "HumanoidMocap/Core/Motion/ArmSettings.cs",
"FileName": "ArmSettings.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 439808,
"IsPrivate": false,
"Code": "using System;\nusing System.Numerics;\nusing HumanoidMocapX.Core.Maths;\n\nnamespace HumanoidMocapX.Core.Motion;\nusing Vector3 = System.Numerics.Vector3;\n\npublic sealed class ArmSettings\n{\n public Vector3 Shoulder { get; set; } = new(.18f,-.15f,0);\n public Vector3 ElbowTarget { get; set; } = new(.4f,-.4f,.2f);\n public float UpperLength { get; set; } = .28f;\n public float ForearmLength { get; set; } = .25f;\n public float MaximumReach { get; set; } = .995f;\n public float MinimumElbowDegrees { get; set; } = 5;\n public float MaximumElbowDegrees { get; set; } = 155;\n}\n"
},
{
"Ident": "notpointless.chomnr_humanoid_mocap_x",
"Path": "HumanoidMocap/Core/Motion/CaptureLegReach.cs",
"FileName": "CaptureLegReach.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 439808,
"IsPrivate": false,
"Code": "using System;\n\nnamespace HumanoidMocapX.Core.Motion;\n\nusing Vector3 = System.Numerics.Vector3;\n\n/// <summary>Leaves a small knee bend when moving the pelvis to reach captured foot targets.</summary>\ninternal static class CaptureLegReach\n{\n\t// Exact full extension is singular: a millimetre of ankle travel can unfold the knee\n\t// by centimetres. Reserve half a percent of reach and enter the correction smoothly.\n\tinternal const float ReachFraction = .995f;\n\n\tinternal static float Lowering( Vector3 hipToFoot, Vector3 up, float length )\n\t{\n\t\tvar vertical = Vector3.Dot( hipToFoot, up );\n\t\tvar horizontal = hipToFoot - up * vertical;\n\t\tvar reach = length * ReachFraction;\n\t\tif ( length <= 0 || vertical <= 0 || horizontal.LengthSquared() >= reach * reach ) return 0;\n\n\t\tvar deficit = vertical - MathF.Sqrt( reach * reach - horizontal.LengthSquared() );\n\t\tvar ease = length * .01f;\n\t\t// Smooth positive part, evaluated without subtracting two nearly equal numbers.\n\t\tvar diagonal = MathF.Sqrt( deficit * deficit + ease * ease );\n\t\treturn deficit >= 0 ? (deficit + diagonal) * .5f : ease * ease / (2 * (diagonal - deficit));\n\t}\n}\n"
},
{
"Ident": "notpointless.chomnr_humanoid_mocap_x",
"Path": "HumanoidMocap/Core/Preview/PreviewFrameBuffer.cs",
"FileName": "PreviewFrameBuffer.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 439808,
"IsPrivate": false,
"Code": "using System;\n\nnamespace HumanoidMocapX.Core.Preview;\n\n/// <summary>Owns a bounded RGBA preview frame. Reuses storage until the preview size\n/// changes; callers must synchronize concurrent readers and writers.</summary>\npublic sealed class PreviewFrameBuffer\n{\n const int MaximumWidth = 960, MaximumHeight = 540, PixelBytes = 4;\n byte[] pixels = Array.Empty<byte>();\n /// <summary>Width of the owned preview in pixels; zero before the first frame.</summary>\n public int Width { get; private set; }\n /// <summary>Height of the owned preview in pixels; zero before the first frame.</summary>\n public int Height { get; private set; }\n /// <summary>Increases after each successful copy so consumers can skip unchanged frames.</summary>\n public long Version { get; private set; }\n /// <summary>Borrowed pixels, valid until the next Copy call.</summary>\n public ReadOnlySpan<byte> Pixels => pixels;\n\n /// <summary>Copies a packed RGBA frame, reducing it to fit 960\u00d7540 without changing\n /// aspect ratio. Rejects nonpositive dimensions and an incorrect byte count.</summary>\n public void Copy(ReadOnlySpan<byte> source, int width, int height)\n {\n if (width < 1 || height < 1 || (long)width * height * PixelBytes != source.Length)\n throw new ArgumentException(\"Video preview requires a complete packed RGBA frame.\");\n var scale = Math.Min(1, Math.Min((double)MaximumWidth / width, (double)MaximumHeight / height));\n var targetWidth = Math.Max(1, (int)(width * scale));\n var targetHeight = Math.Max(1, (int)(height * scale));\n var length = targetWidth * targetHeight * PixelBytes;\n if (pixels.Length != length) pixels = new byte[length];\n if (targetWidth == width && targetHeight == height) source.CopyTo(pixels);\n else\n for (var y = 0; y < targetHeight; y++)\n for (var x = 0; x < targetWidth; x++)\n {\n var from = ((long)y * height / targetHeight * width + (long)x * width / targetWidth) * PixelBytes;\n var to = (y * targetWidth + x) * PixelBytes;\n source.Slice((int)from, PixelBytes).CopyTo(pixels.AsSpan(to, PixelBytes));\n }\n Width = targetWidth; Height = targetHeight; Version++;\n }\n}\n"
},
{
"Ident": "notpointless.chomnr_humanoid_mocap_x",
"Path": "HumanoidMocap/Core/Retargeter.cs",
"FileName": "Retargeter.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 439808,
"IsPrivate": false,
"Code": "#nullable enable annotations\n\nusing System;\nusing System.Collections.Generic;\nusing System.Numerics;\nusing System.Text;\nusing HumanoidMocapX.Core.Cleanup;\nusing HumanoidMocapX.Core.Formats;\nusing HumanoidMocapX.Core.Formats.Dmx;\nusing HumanoidMocapX.Core.Formats.Fbx;\nusing HumanoidMocapX.Core.Formats.Gltf;\nusing HumanoidMocapX.Core.Mapping;\nusing HumanoidMocapX.Core.Maths;\nusing HumanoidMocapX.Core.Skeleton;\nusing HumanoidMocapX.Core.Solve;\nusing HumanoidMocapX.Core.Target;\nusing SkeletonModel = HumanoidMocapX.Core.Skeleton.Skeleton;\n\nnamespace HumanoidMocapX.Core;\n\nusing Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidMocap/Assembly.cs)\n\n/// <summary>\n/// Engine-agnostic pipeline facade (design \u00a75): bytes in \u2192 DMX + vmdl text out. Composes\n/// import \u2192 mapping \u2192 solve \u2192 cleanup \u2192 IK baking \u2192 DMX \u2192 vmdl assembly. Batch is\n/// first-class: N files \u00d7 all takes per file \u2192 ONE combined vmdl, with per-request profile\n/// detection (a batch may mix Mixamo + ActorCore + BVH sources) and per-clip failure\n/// isolation. No file IO anywhere in this type \u2014 callers pass bytes/strings and write the\n/// returned strings.\n/// </summary>\n/// <remarks>\n/// Pipeline per clip:\n/// <list type=\"number\">\n/// <item><b>Import</b> \u2014 format by file extension (<c>.hmotion</c>/<c>.fbx</c>/<c>.glb</c>/\n/// <c>.gltf</c>/<c>.vrm</c>), content sniff as fallback \u2192 <see cref=\"SourceScene\"/> (cm,\n/// native axes).</item>\n/// <item><b>Mapping</b> \u2014 per request: <see cref=\"RetargetRequest.MappingOverride\"/> wins;\n/// else <see cref=\"ProfileDetector.Detect\"/> over the preset library (user presets are\n/// loaded Editor-side and arrive as overrides); else <see cref=\"AutoMapper.Map\"/> as best\n/// effort with <see cref=\"MappingReportInfo.NeedsUserDecision\"/> set when its confidence is\n/// below <see cref=\"ProfileDetector.DetectionThreshold\"/>.</item>\n/// <item><b>Solve</b> \u2014 <see cref=\"GeometricSolver\"/> per take (clip name = take name, or\n/// <see cref=\"RetargetRequest.ClipNameOverride\"/> + index).</item>\n/// <item><b>Cleanup</b> (target space) \u2014 <see cref=\"FootPlant\"/> when enabled (chains from\n/// the target rig's UpperLeg/LowerLeg/Foot/Toe roles, up axis from the TARGET character\n/// frame, fps from the clip); optional arm <see cref=\"EffectorIk\"/> (off by default \u2014 the\n/// solver already matches anatomical directions); <see cref=\"RootMotion\"/> per mode (see\n/// below).</item>\n/// <item><b>IK baking</b> \u2014 <see cref=\"IkBoneBaker.Bake\"/> when the rig has\n/// <see cref=\"BoneClass.IkBaked\"/> bones.</item>\n/// <item><b>DMX</b> \u2014 <see cref=\"DmxWriter\"/> over the target skeleton.</item>\n/// <item><b>Assembly</b> \u2014 <see cref=\"VmdlWriter.GenerateStandalone\"/> over all successful\n/// clips, plus <see cref=\"VmdlAugmenter.Augment\"/> when\n/// <see cref=\"BatchOptions.AugmentVmdlText\"/> is provided; clip-name collisions are\n/// auto-suffixed (<c>_2</c>, <c>_3</c>, \u2026) across the batch.</item>\n/// </list>\n/// <para><b>Root-motion \u2194 target mapping.</b> <see cref=\"RootMotionMode.Extract\"/> bakes the\n/// ground-projected hips path onto the target's dedicated root bone \u2014 defined as a parentless\n/// <see cref=\"BoneClass.Animated\"/> bone that is an ancestor of (and distinct from) the Hips\n/// bone. The s&box rig has NO such bone (pelvis is itself parentless; root_IK is IkBaked),\n/// so there Extract leaves the frames untouched, adds a report note, and instead sets\n/// <see cref=\"AnimEntry.ExtractMotion\"/> on the clip's vmdl entry: Source 2 extracts the\n/// ground-plane translation from <see cref=\"RetargetTargetSpec.DefaultRootBone\"/> at compile\n/// time, which is the engine-native equivalent. The ExtractMotion flag is set for every\n/// Extract request either way. <see cref=\"RootMotionMode.InPlace\"/> always operates on the\n/// hips channels directly and needs no dedicated root.</para>\n/// </remarks>\npublic static class Retargeter\n{\n\t/// <summary>\n\t/// Applies the same mapping, diagnostics, and mid-pose bind-rest selection used by\n\t/// <see cref=\"ConvertBatch\"/>, while leaving the sampled clips available to an editor\n\t/// integration that needs an in-memory target rather than DMX output.\n\t/// </summary>\n\tpublic static ResolvedSource ResolveSource( SourceScene scene, MappingResult? mappingOverride = null,\n\t\tFunc<string, MappingResult?>? userPresetLookup = null )\n\t{\n\t\tArgumentNullException.ThrowIfNull( scene );\n\t\tvar (map, report) = ResolveMapping( scene.Skeleton, mappingOverride, userPresetLookup, scene.AuthoredMapping );\n\t\tforeach ( var note in scene.Notes ) AddNote( report, note );\n\t\tscene = MaybeAdoptBindRest( scene, map, report );\n\t\treturn new( scene, map, report );\n\t}\n\n /// <summary>Converts one source file \u2014 all takes, or only\n /// <see cref=\"RetargetRequest.TakeIndex\"/> when set. Equivalent to a one-request batch.</summary>\n public static RetargetResult Convert(RetargetRequest request, RetargetTargetSpec target)\n {\n ArgumentNullException.ThrowIfNull(request);\n var batch = ConvertBatch(new[] { request }, target);\n return new RetargetResult\n {\n Clips = batch.Clips,\n StandaloneVmdl = batch.StandaloneVmdl,\n Errors = batch.Errors,\n };\n }\n\n /// <summary>\n /// Converts a batch of source files against one target. Per-request profile detection,\n /// per-clip failure isolation (a bad file yields a failed <see cref=\"ClipResult\"/> and\n /// the batch continues), one standalone vmdl with every successful clip, optional\n /// augmentation of an existing vmdl.\n /// </summary>\n public static RetargetBatchResult ConvertBatch(\n IReadOnlyList<RetargetRequest> requests, RetargetTargetSpec target, BatchOptions? options = null)\n {\n ArgumentNullException.ThrowIfNull(requests);\n ArgumentNullException.ThrowIfNull(target);\n options ??= new BatchOptions();\n\n var context = new TargetContext(target.Rig);\n var result = new RetargetBatchResult();\n var usedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n var entries = new List<AnimEntry>();\n\n // Augment mode: names already taken in the existing vmdl's AnimationList must not be\n // silently repointed \u2014 seed the collision set with every existing node name EXCEPT\n // our own AnimFiles (source_filename under DmxFolderRelative), which are replaceable\n // so re-running the same batch stays idempotent.\n if (options.AugmentVmdlText is not null)\n SeedUsedNamesFromExistingVmdl(options.AugmentVmdlText, options.DmxFolderRelative, usedNames);\n\n var mappedPinky = false;\n foreach (var request in requests)\n {\n if (request is null)\n continue;\n mappedPinky |= ProcessRequest(\n request, target, context, options, usedNames, result.Clips, entries);\n }\n\n foreach (var clip in result.Clips)\n {\n if (!clip.Success)\n result.Errors.Add($\"{clip.SourceFileName}: {clip.Error}\");\n }\n\n // Locomotion families are detected on the FINAL (collision-suffixed) clip names so\n // the blend grids reference exactly what the vmdl registers; folder/blend names are\n // made unique against the same name set the clips used.\n IReadOnlyList<LocomotionSetSpec>? locomotionSets = null;\n if (options.DetectLocomotionSets)\n {\n var (sets, reports) = LocomotionSetDetector.Detect(\n entries, usedNames, options.AutoSuffixCollisions);\n result.LocomotionSets.AddRange(reports);\n if (sets.Count > 0)\n locomotionSets = sets;\n }\n\n // The target's OWN embedded animations ride along every generated vmdl (an FBX\n // picked as target keeps its animation next to the retargeted ones). Skipped when\n // a same-named entry is already registered (idempotent re-runs / augment targets\n // that carry them from an earlier conversion); the retargeted clips claimed their\n // names first, so an embedded take never displaces a converted clip.\n if (target.ExtraAnimFiles is { Count: > 0 })\n {\n foreach (var extra in target.ExtraAnimFiles)\n {\n if (usedNames.Add(extra.Name))\n entries.Add(extra);\n }\n }\n\n result.StandaloneVmdl = VmdlWriter.GenerateStandalone(\n target.BaseModelPath, entries, target.VmdlScale, target.DefaultRootBone,\n locomotionSets, target.MeshFilePath, target.MeshImportScale, target.MaterialRemaps,\n target.MeshImportNames);\n\n if (options.AugmentVmdlText is not null)\n {\n try\n {\n var removedSequences = new List<string>();\n result.AugmentedVmdl = VmdlAugmenter.Augment(\n options.AugmentVmdlText, entries, out _,\n new AugmentOptions\n {\n DefaultRootBone = target.DefaultRootBone,\n // The citizen base model copies ring\u2192pinky via its CopyPinky\n // constraints; when this batch actually drives the pinky, the\n // augmented vmdl must neutralize them or the exported pinky\n // channels get overridden at runtime.\n NeutralizePinkyConstraints = mappedPinky,\n LocomotionSets = locomotionSets,\n // Same ownership rule the name seeding above used: pipeline-owned\n // locomotion folders (every AnimFile sourced under our DMX folder)\n // are replaceable wholesale on re-runs.\n DmxFolderRelative = options.DmxFolderRelative,\n // Stale entries whose DMX the caller found missing on disk: keep\n // them and the WHOLE vmdl stops compiling (\"Node 'X' resolve\n // failure\"), new sequences included.\n MissingSourceFiles = options.MissingAnimSources,\n },\n removedSequences);\n foreach (var name in removedSequences)\n {\n result.Warnings.Add(\n $\"removed stale sequence '{name}' from the augmented vmdl: its \"\n + \"animation source file no longer exists on disk (a missing source \"\n + \"fails the whole model recompile)\");\n }\n }\n catch (Exception e)\n {\n result.Errors.Add($\"vmdl augmentation failed: {e.Message}\");\n }\n }\n\n return result;\n }\n\n /// <summary>Collects names already present in the existing vmdl's AnimationList that the\n /// batch must not reuse (everything except our own replaceable AnimFiles).</summary>\n private static void SeedUsedNamesFromExistingVmdl(\n string vmdlText, string dmxFolderRelative, HashSet<string> usedNames)\n {\n Kv3Document doc;\n try\n {\n doc = Kv3.Parse(vmdlText);\n }\n catch (FormatException)\n {\n return; // the augmentation step itself surfaces the parse error\n }\n\n if (doc.Root is not KvObject root || root.GetOrNull(\"rootNode\") is not KvObject rootNode\n || rootNode.GetOrNull(\"children\") is not KvArray children)\n return;\n var animList = children.Items.OfType<KvObject>()\n .FirstOrDefault(o => o.GetString(\"_class\") == \"AnimationList\");\n if (animList?.GetOrNull(\"children\") is not KvArray items)\n return;\n\n var folder = dmxFolderRelative.Replace('\\\\', '/').TrimEnd('/');\n SeedNames(items, folder, usedNames);\n }\n\n /// <summary>\n /// Recursive seeding step: every named AnimationList node reserves its name EXCEPT our\n /// own replaceable output \u2014 AnimFiles whose source lives in the batch's DMX folder, and\n /// locomotion Folders consisting entirely of such AnimFiles (a re-run replaces the whole\n /// folder, so neither its name nor its content may block the new batch's names). Foreign\n /// folders are seeded by name and their content seeded recursively.\n /// </summary>\n private static void SeedNames(KvArray items, string dmxFolder, HashSet<string> usedNames)\n {\n foreach (var node in items.Items.OfType<KvObject>())\n {\n var name = node.GetString(\"name\");\n var cls = node.GetString(\"_class\");\n\n if (cls == \"AnimFile\")\n {\n if (!string.IsNullOrEmpty(name) && !IsOurAnimFile(node, dmxFolder))\n usedNames.Add(name);\n continue;\n }\n\n if (cls == \"Folder\")\n {\n if (IsOurLocomotionFolder(node, dmxFolder))\n continue; // replaceable wholesale \u2014 nothing inside reserves a name\n if (!string.IsNullOrEmpty(name))\n usedNames.Add(name);\n if (node.GetOrNull(\"children\") is KvArray nested)\n SeedNames(nested, dmxFolder, usedNames);\n continue;\n }\n\n if (!string.IsNullOrEmpty(name))\n usedNames.Add(name);\n }\n }\n\n /// <summary>Whether an AnimFile was written by this pipeline: its source_filename sits\n /// inside the batch's DMX folder.</summary>\n private static bool IsOurAnimFile(KvObject node, string dmxFolder)\n {\n var source = (node.GetString(\"source_filename\") ?? \"\").Replace('\\\\', '/');\n return dmxFolder.Length == 0\n ? !source.Contains('/')\n : source.StartsWith(dmxFolder + \"/\", StringComparison.OrdinalIgnoreCase);\n }\n\n /// <summary>Whether a Folder is a locomotion group this pipeline spliced: it contains at\n /// least one AnimFile and every AnimFile inside it (recursively) is ours.</summary>\n private static bool IsOurLocomotionFolder(KvObject folderNode, string dmxFolder)\n {\n var sawAnimFile = false;\n return Walk(folderNode) && sawAnimFile;\n\n bool Walk(KvObject node)\n {\n if (node.GetString(\"_class\") == \"AnimFile\")\n {\n sawAnimFile = true;\n return IsOurAnimFile(node, dmxFolder);\n }\n if (node.GetOrNull(\"children\") is KvArray children)\n {\n foreach (var child in children.Items.OfType<KvObject>())\n {\n if (!Walk(child))\n return false;\n }\n }\n return true;\n }\n }\n\n /// <summary>\n /// Detection-only entry point for UI listings: imports the file and runs the same\n /// preset-then-auto mapping as conversion, without solving anything. Also reports the\n /// file's take metadata (clip names, in take-index order) so listings can expand a\n /// multi-take file into one entry per take. Throws <see cref=\"FormatException\"/> when\n /// the file is unreadable.\n /// </summary>\n public static InspectResult Inspect(byte[] sourceData, string fileName)\n {\n ArgumentNullException.ThrowIfNull(sourceData);\n ArgumentNullException.ThrowIfNull(fileName);\n var scene = ImportSource(sourceData, fileName);\n var takeNames = new List<string>(scene.Clips.Count);\n foreach (var clip in scene.Clips)\n takeNames.Add(clip.Name);\n return new InspectResult\n {\n Mapping = ResolveMapping(scene.Skeleton, authoredMapping: scene.AuthoredMapping).Report,\n TakeNames = takeNames,\n };\n }\n\n // ================================================================ per-request pipeline\n\n /// <summary>Runs one request end to end. Returns true when at least one clip converted\n /// successfully with a mapping that drives a pinky role (CopyPinky handling).</summary>\n private static bool ProcessRequest(\n RetargetRequest request, RetargetTargetSpec target, TargetContext context,\n BatchOptions options, HashSet<string> usedNames,\n List<ClipResult> clips, List<AnimEntry> entries)\n {\n var sourceId = request.SourceId ?? request.SourceFileName;\n SourceScene scene;\n MappingReportInfo report;\n MappingResult map;\n try\n {\n scene = ImportSource(\n request.SourceData, request.SourceFileName, request.SampleFps,\n request.ExternalBufferResolver);\n (map, report) = ResolveMapping(\n scene.Skeleton, request.MappingOverride, authoredMapping: scene.AuthoredMapping);\n // Import diagnostics (mid-pose exports, cross-stack static disagreements, ...)\n // ride the mapping report so UIs can surface them next to the conversion result.\n foreach (var note in scene.Notes)\n AddNote(report, note);\n scene = MaybeAdoptBindRest(scene, map, report);\n }\n catch (Exception e)\n {\n clips.Add(new ClipResult\n {\n ClipName = FileStem(request.SourceFileName),\n SourceFileName = request.SourceFileName,\n SourceId = sourceId,\n Success = false,\n Error = e.Message,\n });\n return false;\n }\n\n if (scene.Clips.Count == 0)\n {\n clips.Add(new ClipResult\n {\n ClipName = FileStem(request.SourceFileName),\n SourceFileName = request.SourceFileName,\n SourceId = sourceId,\n Mapping = report,\n Success = false,\n Error = \"Source file contains no animation takes.\",\n });\n return false;\n }\n\n var mapsPinky = MapsPinkyRole(map);\n if (mapsPinky && options.AugmentVmdlText is null\n && target.BaseModelPath == RetargetTargetSpec.SboxHumanMalePath)\n {\n // A standalone child vmdl cannot override the base model's AnimConstraintList:\n // the citizen's CopyPinky (ring \u2192 pinky orient copy) keeps driving the pinky at\n // runtime even though the DMX carries real pinky channels.\n AddNote(report,\n \"Pinky channels are exported, but the base model's CopyPinky constraints \"\n + \"(ring \u2192 pinky copy) cannot be overridden from a standalone vmdl, so the \"\n + \"pinky will visibly mirror the ring finger's motion in engine \u2014 use \"\n + \"augment mode (add to the existing vmdl) to neutralize them.\");\n }\n if (target.UpAxis == TargetUpAxis.ZUpEngine)\n {\n AddNote(report,\n \"Target rig is engine-space (Z-up, inches): the DMX declares a Z-up axis \"\n + \"system so resourcecompiler performs no Y-up conversion (best-effort \u2014 \"\n + \"verify the compiled sequence on engine-space targets).\");\n }\n\n // External clip definitions (Unity .fbx.meta clipAnimations): one output clip per\n // DEFINITION, sliced out of its take; TakeIndex addresses the definitions then\n // (see RetargetRequest.ClipDefinitions).\n if (request.ClipDefinitions is { Count: > 0 } clipDefs)\n {\n return ProcessClipDefinitions(\n request, target, context, options, usedNames, clips, entries,\n scene, map, report, sourceId, mapsPinky, clipDefs);\n }\n\n // TakeIndex narrows the conversion to a single take (UI per-take entries submit one\n // request per selected take); null keeps the historical convert-all-takes behavior.\n var takeStart = 0;\n var takeEnd = scene.Clips.Count;\n if (request.TakeIndex is { } takeIndex)\n {\n if (takeIndex < 0 || takeIndex >= scene.Clips.Count)\n {\n clips.Add(new ClipResult\n {\n ClipName = FileStem(request.SourceFileName),\n SourceFileName = request.SourceFileName,\n SourceId = sourceId,\n Mapping = report,\n Success = false,\n Error = $\"Take index {takeIndex} is out of range: the source file \"\n + $\"contains {scene.Clips.Count} take(s).\",\n });\n return false;\n }\n takeStart = takeIndex;\n takeEnd = takeIndex + 1;\n }\n\n var anyPinkySuccess = false;\n for (var take = takeStart; take < takeEnd; take++)\n {\n var clipName = UniqueClipName(\n SanitizeClipName(RequestedClipName(request, scene, take)),\n usedNames, options.AutoSuffixCollisions);\n anyPinkySuccess |= ConvertOne(\n request, target, context, options, usedNames, clips, entries,\n scene, map, report, sourceId, mapsPinky, take, clipName);\n }\n\n return anyPinkySuccess;\n }\n\n /// <summary>\n /// The definitions variant of the take loop: each <see cref=\"ExternalClipDef\"/> becomes\n /// its own clip result \u2014 the definition's take is located by\n /// <see cref=\"ExternalClipDef.TakeName\"/> (falling back to the file's first take), sliced\n /// to the definition's native-frame range via <see cref=\"UnityMeta.Slice\"/> and solved as\n /// a single-clip scene. Returns true when at least one clip converted successfully with a\n /// pinky-driving mapping.\n /// </summary>\n private static bool ProcessClipDefinitions(\n RetargetRequest request, RetargetTargetSpec target, TargetContext context,\n BatchOptions options, HashSet<string> usedNames,\n List<ClipResult> clips, List<AnimEntry> entries,\n SourceScene scene, MappingResult map, MappingReportInfo report,\n string sourceId, bool mapsPinky, IReadOnlyList<ExternalClipDef> clipDefs)\n {\n var defStart = 0;\n var defEnd = clipDefs.Count;\n if (request.TakeIndex is { } defIndex)\n {\n if (defIndex < 0 || defIndex >= clipDefs.Count)\n {\n clips.Add(new ClipResult\n {\n ClipName = FileStem(request.SourceFileName),\n SourceFileName = request.SourceFileName,\n SourceId = sourceId,\n Mapping = report,\n Success = false,\n Error = $\"Clip-definition index {defIndex} is out of range: the request \"\n + $\"carries {clipDefs.Count} clip definition(s).\",\n });\n return false;\n }\n defStart = defIndex;\n defEnd = defIndex + 1;\n }\n\n var anyPinkySuccess = false;\n for (var d = defStart; d < defEnd; d++)\n {\n var def = clipDefs[d];\n var requestedName = !string.IsNullOrWhiteSpace(def.Name)\n ? def.Name\n : clipDefs.Count > 1 ? $\"{FileStem(request.SourceFileName)}_{d + 1}\" : FileStem(request.SourceFileName);\n var clipName = UniqueClipName(\n SanitizeClipName(requestedName), usedNames, options.AutoSuffixCollisions);\n\n var take = TakeForDefinition(scene, def);\n var sliced = UnityMeta.Slice(scene.Clips[take], def, clipName);\n\n // Same skeleton/axes, ONE clip: the regular pipeline then solves \"take 0\" of it.\n var defScene = new SourceScene(\n scene.Skeleton, new[] { sliced }, scene.UnitScaleCm,\n scene.UpAxis, scene.UpAxisSign,\n scene.FrontAxis, scene.FrontAxisSign,\n scene.CoordAxis, scene.CoordAxisSign,\n scene.OriginalUpAxis, scene.Notes)\n {\n AuthoredMapping = scene.AuthoredMapping,\n RestPlacementAuthored = scene.RestPlacementAuthored,\n };\n\n anyPinkySuccess |= ConvertOne(\n request, target, context, options, usedNames, clips, entries,\n defScene, map, report, sourceId, mapsPinky, take: 0, clipName);\n }\n\n return anyPinkySuccess;\n }\n\n /// <summary>The take a definition's frame range refers to: matched by take name when the\n /// definition records one (Unity's <c>takeName</c>, e.g. <c>root|Animation</c>), else \u2014\n /// and when nothing matches \u2014 the file's first take.</summary>\n private static int TakeForDefinition(SourceScene scene, ExternalClipDef def)\n {\n if (!string.IsNullOrWhiteSpace(def.TakeName))\n {\n for (var i = 0; i < scene.Clips.Count; i++)\n {\n if (string.Equals(scene.Clips[i].Name, def.TakeName, StringComparison.Ordinal))\n return i;\n }\n }\n return 0;\n }\n\n /// <summary>Solves ONE clip (take <paramref name=\"take\"/> of <paramref name=\"scene\"/>)\n /// end to end \u2014 solve + cleanup + DMX, plus the optional mirrored twin\n /// (<see cref=\"RetargetRequest.CreateMirroredVariant\"/>) \u2014 appending success or failure\n /// <see cref=\"ClipResult\"/>s (failures never abort the batch). Returns true on success\n /// with a pinky-driving mapping.</summary>\n private static bool ConvertOne(\n RetargetRequest request, RetargetTargetSpec target, TargetContext context,\n BatchOptions options, HashSet<string> usedNames,\n List<ClipResult> clips, List<AnimEntry> entries,\n SourceScene scene, MappingResult map, MappingReportInfo report,\n string sourceId, bool mapsPinky, int take, string clipName)\n {\n Clip clip;\n try\n {\n clip = SolveAndClean(request, target, context, scene, map, report, take, clipName);\n EmitClip(request, target, context, options, usedNames, clips, entries, report,\n sourceId, clipName, clip.Frames, clip.Fps, clip.Looping, isMirrored: false);\n }\n catch (Exception e)\n {\n clips.Add(new ClipResult\n {\n ClipName = clipName,\n SourceFileName = request.SourceFileName,\n SourceId = sourceId,\n Mapping = report,\n Success = false,\n Error = e.Message,\n });\n return false;\n }\n\n if (request.CreateMirroredVariant)\n {\n // The twin gets its own collision-suffixed name and its own failure isolation:\n // a mirror problem (asymmetric rig) fails ONLY the twin, never the primary clip.\n var mirroredName = UniqueClipName(\n SanitizeClipName(clipName + \"_M\"), usedNames, options.AutoSuffixCollisions);\n try\n {\n // SOURCE-SIDE MIRROR (southpaw G8 mirror fix, gate3_review.md 3.4 and the\n // G8 bisect evidence): the SOURCE clip is mirrored across the source\n // character's sagittal plane and the twin then runs through the COMPLETE\n // unmodified primary pipeline (solver, cleanups, ground alignment, IK\n // bake, orphan re-anchor). The emitted _M clip is a first-class primary\n // clip in every representational respect, indistinguishable from a clip\n // an animator authored mirrored. Target-space channel mirroring, however\n // geometrically exact, produced data the ENGINE's render-side sequence\n // evaluation mangles (measured: a mirrored pelvis channel alone renders\n // the whole model upside down while every CPU-side bone API reports a\n // perfect upright mirror); no data conjugation convention survived that\n // stage, so the mirror moved to the source side where no alien data\n // shape can exist.\n var mirroredScene = SourceMirror.MirrorTake(scene, map, take);\n var mirroredClip = SolveAndClean(\n request, target, context, mirroredScene, map, report, take, mirroredName);\n EmitClip(request, target, context, options, usedNames, clips, entries, report,\n sourceId, mirroredName, mirroredClip.Frames, mirroredClip.Fps,\n clip.Looping, isMirrored: true);\n }\n catch (Exception e)\n {\n clips.Add(new ClipResult\n {\n ClipName = mirroredName,\n SourceFileName = request.SourceFileName,\n SourceId = sourceId,\n Mapping = report,\n Success = false,\n Error = $\"mirrored variant: {e.Message}\",\n });\n }\n }\n\n return mapsPinky;\n }\n\n /// <summary>Shared tail of clip production (primary and mirrored twin): optional footstep\n /// events, DMX serialization, the <see cref=\"ClipResult\"/> and the vmdl\n /// <see cref=\"AnimEntry\"/> \u2014 plus the additive (<c>_delta</c>) companion entry when\n /// <see cref=\"RetargetRequest.CreateAdditiveVariant\"/> is on (a second AnimFile REUSING\n /// the clip's DMX with an AnimSubtract child, shipped-citizen shape; no separate\n /// <see cref=\"ClipResult\"/> since no separate DMX exists).</summary>\n private static void EmitClip(\n RetargetRequest request, RetargetTargetSpec target, TargetContext context,\n BatchOptions options, HashSet<string> usedNames,\n List<ClipResult> clips, List<AnimEntry> entries,\n MappingReportInfo report, string sourceId,\n string clipName, List<XForm[]> frames, float fps, bool looping, bool isMirrored)\n {\n var events = GenerateFootsteps(request, target, context, report, frames, fps);\n var dmxFileName = SanitizeFileName(clipName) + \".dmx\";\n // Embedded-mesh vmdls and compiled Z-up targets compile root channels 90\u00b0 about the declared\n // up axis away from the mesh bind; child channels are unaffected. Compensate the\n // serialized copy only so compiled playback matches the solved preview.\n var dmxFrames = !string.IsNullOrEmpty(target.MeshFilePath) || target.UpAxis == TargetUpAxis.ZUpEngine\n ? CompensateEmbeddedMeshRootYaw(frames, target.Rig, target.UpAxis)\n : frames;\n var dmx = DmxWriter.Write(\n target.Rig.Skeleton, new Clip(clipName, fps, looping, dmxFrames), new DmxWriteOptions\n {\n Name = clipName,\n SourceNote = isMirrored ? request.SourceFileName + \" (mirrored)\" : request.SourceFileName,\n // Design \u00a73: ConstraintDriven (twist/helper) bones keep their joints +\n // bind in the DMX but get NO channels \u2014 the model's AnimConstraintList\n // drives them. Face bones are exempt from the exclusion (rest-local\n // channels): nothing drives them in a compiled sequence, and channel-less\n // face joints bake statically (eyes out of sockets in ModelDoc). Custom\n // rigs (FromSkeleton) have NO constraint list \u2014 their helpers keep baked\n // channels or twist bones freeze (candy-wrapped wrists).\n // MIRRORED clips use the IDENTICAL exclusion set (data-shape parity with\n // primary clips, southpaw G8 mirror fix): a channel-less constraint-driven\n // bone on a mirrored-body clip is no different from one on any leftward\n // primary pose; the model's AnimConstraintList drives it at runtime either\n // way. (ClipMirror.MirrorSafeExclusions remains available for consumers\n // that need reflection-exact data for excluded bones.)\n ChannelExcludedBones = target.Rig.HelpersAreConstraintDriven\n ? context.ConstraintDrivenBones\n : null,\n UpAxisY = target.UpAxis == TargetUpAxis.YUpCm,\n });\n\n var extractMotion = request.RootMotion == RootMotionMode.Extract;\n\n // Additive variant: '<clip>_delta' (shipped naming), collision-suffixed like every\n // other batch name. Only the NAME is produced here \u2014 the vmdl entry below reuses\n // the base clip's DMX (resourcecompiler does the reference-frame subtraction).\n var deltaName = request.CreateAdditiveVariant\n ? UniqueClipName(\n SanitizeClipName(clipName + \"_delta\"), usedNames, options.AutoSuffixCollisions)\n : null;\n\n clips.Add(new ClipResult\n {\n ClipName = clipName,\n SourceFileName = request.SourceFileName,\n SourceId = sourceId,\n DmxFileName = dmxFileName,\n DmxContent = dmx,\n Mapping = report,\n Success = true,\n SolvedFrames = frames,\n Fps = fps,\n Looping = looping,\n ExtractMotion = extractMotion,\n FootstepEvents = events,\n IsMirroredVariant = isMirrored,\n HasAdditiveVariant = deltaName is not null,\n AdditiveVariantName = deltaName,\n });\n var sourceFilename = JoinAssetPath(options.DmxFolderRelative, dmxFileName);\n entries.Add(new AnimEntry\n {\n Name = clipName,\n SourceFilename = sourceFilename,\n Looping = looping,\n ExtractMotion = extractMotion,\n Events = events,\n });\n if (deltaName is not null)\n {\n // The shipped _delta sequences carry the AnimSubtract child and nothing else\n // (no motion extraction, no events) \u2014 an additive layer fires no footsteps and\n // extracting root motion from a delta makes no sense.\n entries.Add(new AnimEntry\n {\n Name = deltaName,\n SourceFilename = sourceFilename,\n Looping = looping,\n SubtractAnimName = clipName,\n SubtractFrame = 0,\n });\n }\n }\n\n /// <summary>\n /// Generates the <c>AE_FOOTSTEP</c> events for a solved clip when the request asks for\n /// them (<see cref=\"RetargetRequest.GenerateFootstepEvents\"/>): plant intervals are\n /// detected on the SOLVED TARGET frames (this is where the engine plays the clip, so\n /// touchdowns must be measured here), each interval start = one footstep\n /// (<see cref=\"FootstepEvents\"/>). Empty when the feature is off or the target rig lacks\n /// the leg chains / character up (noted on the report then).\n /// </summary>\n private static IReadOnlyList<AnimEventEntry> GenerateFootsteps(\n RetargetRequest request, RetargetTargetSpec target, TargetContext context,\n MappingReportInfo report, List<XForm[]> frames, float fps)\n {\n if (!request.GenerateFootstepEvents)\n return Array.Empty<AnimEventEntry>();\n if (context.Up is not { } up || context.FootChains is not { } feet)\n {\n AddNote(report, \"Footstep events skipped: \" + context.UpOrChainProblem);\n return Array.Empty<AnimEventEntry>();\n }\n return FootstepEvents.Generate(\n frames, target.Rig.Skeleton, feet.Left, feet.Right, up, fps,\n ScaledPlantOptions(target));\n }\n\n /// <summary>Default plant thresholds are cm-tuned; engine-space rigs are in inches, so\n /// they are scaled by the cm\u2192inch factor to keep the same physical sensitivity (shared by\n /// the foot-plant cleanup and the footstep-event detection).</summary>\n private static FootPlantOptions ScaledPlantOptions(RetargetTargetSpec target)\n {\n var options = new FootPlantOptions();\n if (target.UpAxis == TargetUpAxis.ZUpEngine)\n {\n options.SpeedThresholdCmPerSec *= RetargetTargetSpec.SboxSourceScale;\n options.HeightThresholdCm *= RetargetTargetSpec.SboxSourceScale;\n }\n return options;\n }\n\n private static bool MapsPinkyRole(MappingResult map)\n {\n foreach (var role in map.RoleToBone.Keys)\n {\n if (role.ToString().StartsWith(\"Pinky\", StringComparison.Ordinal))\n return true;\n }\n return false;\n }\n\n /// <summary>Solve one take and run the target-space cleanup + IK baking passes.</summary>\n private static Clip SolveAndClean(\n RetargetRequest request, RetargetTargetSpec target, TargetContext context,\n SourceScene scene, MappingResult map, MappingReportInfo report, int take, string clipName)\n {\n var requested = request.Solve ?? new SolveOptions();\n if(request.MocapCorrections is {} wristSettings)\n {\n Motion.WristPositionOffsets.Validate(wristSettings.WristOffsets);\n if(wristSettings.WristOffsets.Any(e=>e.Enabled))\n throw new NotSupportedException(\"Manual wrist offsets currently require camera-relative hand capture.\");\n }\n if(scene.CaptureContacts?.Contacts.Any(c=>c.Review==Motion.ContactReview.Confirmed&&!string.IsNullOrEmpty(c.Object))==true)\n throw new NotSupportedException(\"Confirmed prop constraints currently require camera-relative hand capture. Disable those constraints to preview body motion; object tracks remain available in captured-skeleton export.\");\n var captureClavicleDirections = scene.CaptureSpace is not null && requested.TransferModes is null;\n if (captureClavicleDirections)\n {\n // Body-model collar joints are not the target's anatomical rest carriage.\n // SMPL's zero-pose collar->shoulder slopes upward; its reconstructed neutral\n // pose rotates that line down to level. Replaying the same delta onto the\n // Human's already-level collar line lowers its shoulders a second time.\n AddNote(report, \"Full-body capture transfers observed clavicle directions while retaining target bone lengths and attachment positions.\");\n }\n var solved = new GeometricSolver().Solve(scene, map, target.Rig, new SolveOptions\n {\n GroundedLegDirections = request.FootPlantCleanup,\n HipScaleHorizontal = requested.HipScaleHorizontal,\n HipScaleVertical = requested.HipScaleVertical,\n TransferFingers = requested.TransferFingers,\n TransferModes = requested.TransferModes,\n CaptureClavicleDirections = captureClavicleDirections,\n NeutralSourceRest = captureClavicleDirections,\n ClipIndex = take,\n ClipName = clipName,\n });\n var frames = solved.Frames;\n\n // ---- embedded-take fidelity: authored local translations pass through ----------\n if (request.PreserveSourceTranslations)\n PassThroughSourceTranslations(scene, target.Rig, frames, take, report);\n\n // ---- foot-plant cleanup (target space; up axis from the TARGET character frame) ----\n if (request.FootPlantCleanup)\n {\n if (context.Up is { } up && context.FootChains is { } feet)\n {\n // Grounded stance alignment first: levels planted soles against the ground\n // (removes the stance offset a non-stance source rest leaves in the solver's\n // rest-relative foot transfer). Plants are detected on the SOURCE clip \u2014\n // source-space candidates; hip-height rescaling can push the solved target trajectories\n // outside the cm-tuned Kovar thresholds. Composes with the position pinning\n // below (this rotates feet about their own joints; the pinning preserves\n // foot world rotations).\n // Reconstructed bodies: restore the performer's stance width on differently\n // proportioned hips before anything is levelled or anchored.\n if(scene.CaptureSpace is not null)\n {\n var stance=Motion.CaptureStanceProportion.Apply(frames,scene,map,target.Rig,feet.Left,feet.Right);\n if(stance.Samples>0)AddNote(report,$\"Stance proportion: each foot moved {stance.HalfWidthCorrection:F2} target units toward the body's centre line, because this target's hip joints are wider relative to its legs than the performer's. Leg lengths and foot orientation are preserved.\");\n }\n GroundAlignFeet(frames, scene, map, target.Rig.Skeleton, feet, up, solved.Fps, take);\n\n var plantOptions=ScaledPlantOptions(target);\n // Reconstructed contacts can be unreachable on different proportions.\n // Keep captured target limb lengths fixed instead of stretching to a guess.\n if(scene.CaptureSpace is not null)plantOptions.MaxStretch=0f;\n FootPlant.Apply(\n frames, target.Rig.Skeleton, feet.Left, feet.Right, up, solved.Fps,\n plantOptions);\n\n // Support ground alignment: one constant vertical offset per clip that\n // restores the SOURCE-authored foot-to-ground relationship (southpaw\n // final regeneration package, gate4_review.md 3.1 / DEFERRED.md f2).\n AlignSupportToGround(frames, scene, map, target.Rig, feet, up, report, take);\n MatchSourceFootHeights(frames, scene, map, target.Rig, feet, up, take);\n }\n else\n {\n AddNote(report, \"Foot-plant cleanup skipped: \" + context.UpOrChainProblem);\n }\n }\n\n // A reconstructed body has no finger tracks; give its hands a resting shape, not the bind pose.\n if(scene.CaptureSpace is not null)\n {\n if(Motion.CaptureSpineSpread.Apply(frames,target.Rig)>0)AddNote(report,\"Spine: each frame's bend from the hips to the chest is shared evenly between the spine bones; the network puts most of it in the lowest one. Chest, head and arms are unchanged.\");\n var shoulders=Motion.CaptureShoulderCarriage.Apply(frames,scene.Clips[take].Frames,scene.Skeleton,map,target.Rig);\n if(shoulders>0)AddNote(report,$\"Shoulders: {shoulders} clavicle samples carry the performer's change from their own rest shoulder line onto this target's rest shoulder line, instead of copying a differently built clavicle's direction. Arms keep their solved orientation.\");\n // After the shoulders are placed: restore the performer's hand spacing on differently proportioned shoulders.\n var arms=Motion.CaptureArmProportion.Apply(frames,scene.Skeleton,map,target.Rig);\n if(arms.Samples>0)AddNote(report,$\"Hand spacing: wrists in front of or across the body moved up to {arms.HalfWidthCorrection:F2} target units toward the centre line, because this target's shoulders are wider relative to its arms than the performer's. Arm lengths and hand orientation are preserved; outstretched arms are left alone.\");\n var together=Motion.CaptureHandRelation.Apply(frames,scene.Clips[take].Frames,scene.Skeleton,map,target.Rig);\n if(together>0)AddNote(report,$\"Hands together: in {together} frames the performer's hands were close (crossing, clapping or touching) and the character's hands were placed the same way from each other, scaled to its arms.\");\n var relaxed=Motion.RelaxedHands.Apply(frames,map,target.Rig);\n if(relaxed>0)AddNote(report,$\"Hands: this body capture has no finger tracks, so {relaxed} target finger joints hold one authored, slightly curled resting pose instead of the bind pose. It is not captured finger motion.\");\n }\n\n // ---- optional arm effector IK (default off: the solver already matches anatomical\n // directions; arm IK is only for reach-critical work) ----\n if (request.ArmEffectorIk)\n {\n var problem = ArmIkCleanup.Apply(frames, scene, map, target.Rig, context, take);\n if (problem is not null)\n AddNote(report, \"Arm effector IK skipped: \" + problem);\n }\n\n // ---- root motion (see class remarks for the Extract \u2194 ExtractMotion mapping) ----\n if(request.FootPlantCleanup&&(request.MocapCorrections?.StabilizeFeet??true))\n {\n var anchored=Motion.CaptureContactRoot.Apply(frames,scene,map,target.Rig,target.UpAxis);\n if(anchored>0)AddNote(report,FormattableString.Invariant($\"Travel from contacts: planted feet no longer slide; the body's travel was corrected by up to {anchored:F0} cm to match them.\"));\n var locking=Motion.CaptureFootLock.Apply(frames,scene,map,target.Rig,target.UpAxis);\n if(locking.ConstrainedSamples>0)AddNote(report,$\"Final target foot anchors: {locking.ConstrainedSamples} leg samples; maximum reach residual {locking.MaximumReachResidual:F4} target units; level-floor height drift of up to {locking.MaximumFloorDrift:F2} target units removed from the root. Backend static probabilities are contact suggestions, not measured ground truth.\");\n // Keep the whole clip on the floor, including camera-relative captures and dance with few plants.\n if(scene.CaptureSpace is not null)\n {\n var contactMask=Motion.CaptureContactRoot.ContactMask(frames,scene,map,target.Rig,target.UpAxis);\n var grounded=Motion.CaptureGround.Apply(frames,target.Rig,target.UpAxis,solved.Fps,contactMask);\n if(grounded>0)AddNote(report,FormattableString.Invariant($\"Floor followed through the clip: up to {grounded:F1} cm of drift toward or away from the floor removed.\"));\n var hovering=Motion.CaptureHover.Apply(frames,target.Rig,target.UpAxis,solved.Fps,contactMask);\n if(hovering>0)AddNote(report,$\"Kept on the floor in {hovering} frames: brief hovering above it or sinking into it removed.\");\n var seatedFrames=Motion.CaptureSeat.Apply(frames,scene,map,target.Rig,target.UpAxis);\n if(seatedFrames>0)AddNote(report,$\"Seated on the floor in {seatedFrames} frames: hips lowered onto the floor, feet and resting hands held in place.\");\n var handsHeld=Motion.CaptureHandPlant.Apply(frames,target.Rig,target.UpAxis,solved.Fps,PlantedHands(scene,map,frames.Count),HandSpread(scene,map,take));\n if(handsHeld>0)AddNote(report,$\"Hands on the floor held in place in {handsHeld} hand samples: palms resting on the floor no longer slide or hover.\");\n }\n }\n // Last of the leg work: no thigh, shin or foot turns far more in one frame than around it.\n if(scene.CaptureSpace is not null&&Motion.CaptureLegSnaps.Apply(frames,target.Rig,solved.Fps,target.UpAxis==TargetUpAxis.ZUpEngine?39.3700787f:100f,PlantedFeet(scene,map,frames.Count)) is var snaps&&snaps>0)\n AddNote(report,$\"Leg continuity corrected in {snaps} samples: isolated rotation or knee-position jolts were spread over neighbouring frames, preserving foot anchors where the constraints allowed.\");\n ApplyRootMotion(request.RootMotion, frames, context, report);\n\n if (request.MocapCorrections is { } corrections)\n {\n Motion.TargetCorrections.Apply(frames, target.Rig, target.UpAxis, corrections);\n if (corrections.FirstPerson) AddNote(report, \"Shoulders and elbows are generated target-rig IK, not measured joints.\");\n }\n\n if (scene.CaptureSpace is not null)\n {\n var clearance = Motion.CaptureArmClearance.Apply(frames, scene.Clips[take].Frames, scene.Skeleton, map,\n target.Rig, solved.Fps, out var unresolvedContacts, out var maximumPenetration);\n if (clearance.Samples > 0)\n AddNote(report, FormattableString.Invariant($\"Torso contact: DigitalRune CCD adjusted {clearance.Samples} hand samples by up to {clearance.Largest:F2} target units, preserving wrist attitude and finger articulation. Clearance uses a bone-derived body proxy.\"));\n if (unresolvedContacts > 0)\n AddNote(report, FormattableString.Invariant($\"Torso contact remains unresolved in {unresolvedContacts} hand samples (up to {maximumPenetration:F3} target units). Fixed arm lengths and wrist attitude prevented a clear proxy pose; review these contacts.\"));\n }\n\n // Last of the pose cleanup: whatever snap a step above left (an IK flip, a contact pushed clear, a hold handed\n // over) is pulled back onto its neighbours' path, keeping planted hands and feet where they are.\n if (scene.CaptureSpace is not null && Motion.CaptureDespike.Apply(frames, target.Rig, target.UpAxis == TargetUpAxis.ZUpEngine ? 1 / 2.54f : 1f) is var smoothedSnaps && smoothedSnaps > 0)\n AddNote(report, $\"Snaps removed after cleanup: {smoothedSnaps} joint samples that left the smooth path through their neighbours were pulled back onto it.\");\n\n // ---- IK helper bones (root_IK, IK targets, ikrule) need real baked channels ----\n if (context.HasIkBakedBones)\n IkBoneBaker.Bake(frames, target.Rig);\n\n // ---- unmapped limb twist bones follow their joint's roll ------------------------\n // Mocap exports a portable baked armature, so it cannot rely on the target\n // model's runtime constraints. Its preview overrides those helpers too.\n // Keep runtime-owned helpers only for the ordinary model-constraint path.\n var twistCount = TwistBoneFollow.Apply(frames, target.Rig,\n request.MocapCorrections is null && target.Rig.HelpersAreConstraintDriven ? context.ConstraintDrivenBones : null);\n if (twistCount > 0)\n AddNote(report, $\"{twistCount} limb deform helper bone(s) follow their \"\n + \"neighboring joints (left at rest they pinch or candy-wrap the skin).\");\n\n // ---- orphan helper bones ride the hips (LAST: anchored to the FINAL hips, after\n // foot-plant pinning / root motion have settled the trajectory) -------------------\n FollowHipsWithOrphanBones(target.Rig, frames, report);\n\n var looping = request.LoopingOverride ?? solved.Looping;\n return new Clip(clipName, solved.Fps, looping, frames);\n }\n\n /// <summary>Per source frame, how far apart the performer's hands are across the floor, in shoulder widths; null when the\n /// source lacks hands or shoulders. The capture is Y-up.</summary>\n static float[] HandSpread(SourceScene scene,MappingResult map,int take)\n {\n if(!map.RoleToBone.TryGetValue(BoneRole.HandL,out var handL)||!map.RoleToBone.TryGetValue(BoneRole.HandR,out var handR)\n ||!map.RoleToBone.TryGetValue(BoneRole.UpperArmL,out var armL)||!map.RoleToBone.TryGetValue(BoneRole.UpperArmR,out var armR))return null;\n var source=scene.Clips[take].Frames;var world=new XForm[scene.Skeleton.Count];var spread=new float[source.Count];\n for(var f=0;f<source.Count;f++)\n {\n FkUtil.ToWorld(source[f],scene.Skeleton,world);\n var apart=world[handL].Pos-world[handR].Pos;apart.Y=0;\n var shoulders=Vector3.Distance(world[armL].Pos,world[armR].Pos);\n spread[f]=shoulders>1e-6f?apart.Length()/shoulders:0;\n }\n return spread;\n }\n\n /// <summary>Frames where the capture marks each hand on the floor (stationary probability at least one half).</summary>\n static Dictionary<BoneRole,bool[]> PlantedHands(SourceScene scene,MappingResult map,int count)\n =>new[]{BoneRole.HandL,BoneRole.HandR}.ToDictionary(role=>role,role=>\n {\n var result=new bool[count];\n if(map.RoleToBone.TryGetValue(role,out var bone)&&scene.CaptureStationaryJoints?.TryGetValue(scene.Skeleton[bone].Name,out var p)==true&&p.Length==count)\n for(var f=0;f<count;f++)result[f]=p[f]>=.5f;\n return result;\n });\n\n /// <summary>Frames where the capture marks each joint planted (stationary probability at least one half): left ankle, left\n /// toe, right ankle, right toe.</summary>\n static bool[][] PlantedFeet(SourceScene scene,MappingResult map,int count)\n =>new[]{BoneRole.FootL,BoneRole.ToeL,BoneRole.FootR,BoneRole.ToeR}.Select(role=>\n {\n var result=new bool[count];\n if(map.RoleToBone.TryGetValue(role,out var bone)&&scene.CaptureStationaryJoints?.TryGetValue(scene.Skeleton[bone].Name,out var p)==true&&p.Length==count)\n for(var f=0;f<count;f++)result[f]=p[f]>=.5f;\n return result;\n }).ToArray();\n\n /// <summary>\n /// Unmapped bone subtrees OUTSIDE the mapped skeleton (no mapped ancestor \u2014 cloth and\n /// physics helpers parented to the scene root; real case: a skirt of 48 kilt bones on\n /// a Sketchfab catgirl, siblings of the body root) ride the character rigidly. Left\n /// at their rest locals they freeze at the bind spot while the body animates away\n /// (\"one part of the body doesn't move at all\") and the mesh skinned across the\n /// boundary shears (\"completely stretched\"). Each subtree's topmost bone anchors to\n /// the HIPS \u2014 or to an extremity (hand/foot/head) when its rest sits clearly closer\n /// to it (a sword resting in the hand follows the hand, not the pelvis). Descendants\n /// ride along on their rest locals; bones on the hips' own ancestor path stay\n /// solver-owned (trajectory). A mapped body branch on a separate control tree is\n /// similarly anchored to the nearest hips-connected mapped joint; its solved rotation\n /// remains unchanged.\n /// </summary>\n private static void FollowHipsWithOrphanBones(\n TargetRig rig, List<XForm[]> frames, MappingReportInfo report)\n {\n var skeleton = rig.Skeleton;\n if (rig.BoneForRole(BoneRole.Hips) is not { } hips || frames.Count == 0)\n return;\n\n var hipsPath = new HashSet<int>();\n for (var b = hips; b >= 0; b = skeleton[b].ParentIndex)\n hipsPath.Add(b);\n\n bool HasMappedAncestor(int bone)\n {\n for (var b = skeleton[bone].ParentIndex; b >= 0; b = skeleton[b].ParentIndex)\n {\n if (rig.RoleOf(b) is not null)\n return true;\n }\n return false;\n }\n\n var rest = skeleton.RestWorld;\n\n bool DescendsFrom(int bone, int ancestor)\n {\n for (var b = bone; b >= 0; b = skeleton[b].ParentIndex)\n if (b == ancestor)\n return true;\n return false;\n }\n\n // Extremity anchors: adopted only when the orphan rests at less than half its\n // hips distance \u2014 near-torso helpers (skirts, tails, capes) stay on the hips,\n // which never mistakes a skirt panel brushing a thigh for hand luggage.\n var extremities = new List<int>();\n foreach (var role in new[]\n {\n BoneRole.HandL, BoneRole.HandR, BoneRole.FootL, BoneRole.FootR, BoneRole.Head,\n })\n {\n if (rig.BoneForRole(role) is { } bone)\n extremities.Add(bone);\n }\n\n // Topmost orphans: unmapped, off the hips' ancestor path, no mapped ancestor,\n // and the parent is either the scene root path or absent (children of deeper\n // orphans ride along unchanged).\n var tops = new List<(int Bone, int Anchor)>();\n for (var i = 0; i < skeleton.Count; i++)\n {\n if (rig.RoleOf(i) is not null || hipsPath.Contains(i) || HasMappedAncestor(i))\n continue;\n var parent = skeleton[i].ParentIndex;\n if (parent >= 0 && !hipsPath.Contains(parent))\n continue;\n\n var anchor = hips;\n var hipsDistance = (rest[i].Pos - rest[hips].Pos).Length();\n var best = hipsDistance * 0.5f;\n foreach (var candidate in extremities)\n {\n var distance = (rest[i].Pos - rest[candidate].Pos).Length();\n if (distance < best)\n {\n best = distance;\n anchor = candidate;\n }\n }\n tops.Add((i, anchor));\n }\n\n // A few constraint-oriented exports place a real mapped body branch on a parallel\n // control hierarchy. The common case is a skinned head under CONTROL_NECK while\n // the deform cervical chain is under the hips. glTF/FBX does not preserve the DCC\n // constraint that joined those trees, so rotations solve but the branch stays at\n // its bind position. Anchor each topmost detached mapped branch to the nearest\n // mapped joint that really descends from the hips, preserving its solved rotation.\n var connectedMapped = Enumerable.Range(0, skeleton.Count)\n .Where(i => rig.RoleOf(i) is not null && DescendsFrom(i, hips))\n .ToList();\n var detachedMapped = new List<(int Bone, int Anchor)>();\n for (var i = 0; i < skeleton.Count; i++)\n {\n if (rig.RoleOf(i) is null || hipsPath.Contains(i) || DescendsFrom(i, hips)\n || HasMappedAncestor(i) || connectedMapped.Count == 0)\n continue;\n var anchor = connectedMapped\n .OrderBy(candidate => (rest[i].Pos - rest[candidate].Pos).LengthSquared())\n .First();\n detachedMapped.Add((i, anchor));\n }\n\n // A detached deform head may retain a small control-space bind pitch after the DCC\n // constraint is lost. Infer its gaze only when one local axis already agrees closely\n // with character-forward, then remove the small vertical component. Applying the\n // correction in rest-local space preserves every solved animation delta.\n var neutralHead = -1;\n var neutralHeadCorrection = Quaternion.Identity;\n foreach (var (bone, _) in detachedMapped)\n {\n if (rig.RoleOf(bone) == BoneRole.Head\n && TryDetachedHeadNeutralCorrection(rig, bone, out neutralHeadCorrection))\n {\n neutralHead = bone;\n break;\n }\n }\n\n if (tops.Count == 0 && detachedMapped.Count == 0)\n return;\n\n var anchorRestInverse = new Dictionary<int, XForm>();\n foreach (var (_, anchor) in tops)\n anchorRestInverse[anchor] = rest[anchor].Inverse();\n foreach (var (_, anchor) in detachedMapped)\n anchorRestInverse[anchor] = rest[anchor].Inverse();\n\n foreach (var frame in frames)\n {\n var world = new Skeleton.Pose(frame).ToWorld(skeleton);\n foreach (var (top, anchor) in tops)\n {\n var delta = XForm.Compose(world[anchor], anchorRestInverse[anchor]);\n var parent = skeleton[top].ParentIndex;\n var parentWorld = parent >= 0 ? world[parent] : XForm.Identity;\n frame[top] = XForm.ToLocal(\n parentWorld, XForm.Compose(delta, rest[top]));\n }\n\n if (detachedMapped.Count == 0)\n continue;\n\n // Re-evaluate after moving orphan control roots (the detached branch's actual\n // parent may be inside one). Only position is anchored: its mapped rotation is\n // the solver's result and must remain authoritative.\n world = new Skeleton.Pose(frame).ToWorld(skeleton);\n foreach (var (bone, anchor) in detachedMapped)\n {\n var delta = XForm.Compose(world[anchor], anchorRestInverse[anchor]);\n var rotation = bone == neutralHead\n ? MathQ.Normalize(world[bone].Rot * neutralHeadCorrection)\n : world[bone].Rot;\n var desired = new XForm(\n XForm.Compose(delta, rest[bone]).Pos,\n rotation);\n var parent = skeleton[bone].ParentIndex;\n frame[bone] = parent < 0\n ? desired\n : XForm.ToLocal(world[parent], desired);\n }\n }\n if (tops.Count > 0)\n AddNote(report,\n $\"{tops.Count} helper bone subtree(s) outside the mapped skeleton ride the \"\n + \"character (cloth/physics/prop bones would otherwise freeze at their bind position).\");\n if (detachedMapped.Count > 0)\n AddNote(report,\n $\"{detachedMapped.Count} mapped body branch(es) on separate control hierarchies \"\n + \"follow the connected skeleton (exported DCC constraints are not available at runtime).\");\n if (neutralHead >= 0)\n AddNote(report,\n \"Detached head bind pitch was leveled to the character facing plane while preserving animation.\");\n }\n\n private static void MatchSourceFootHeights(\n List<XForm[]> frames, SourceScene scene, MappingResult map, TargetRig rig,\n (FootChain Left, FootChain Right) feet, Vector3 up, int take)\n {\n if (frames.Count == 0 || !map.RoleToBone.TryGetValue(BoneRole.Hips, out var srcHips)\n || rig.BoneForRole(BoneRole.Hips) is not int tgtHips)\n return;\n var source = scene.Skeleton;\n var target = rig.Skeleton;\n if (!scene.RestPlacementAuthored && !RestNormalizer.IsAnatomicalRest(source, map, source.RestWorld))\n return; // Bone-length-only rests cannot calibrate an anatomical support height.\n var srcFrames = scene.Clips[take].Frames;\n if (srcFrames.Count != frames.Count)\n return;\n var srcUp = scene.UpAxis switch\n {\n 0 => new Vector3(scene.UpAxisSign, 0, 0),\n 2 => new Vector3(0, 0, scene.UpAxisSign),\n _ => new Vector3(0, scene.UpAxisSign, 0),\n };\n up = GroundUp(up);\n var worlds = new XForm[frames.Count][];\n var restGround = float.PositiveInfinity;\n var motionGround = float.PositiveInfinity;\n foreach (var rest in source.RestWorld)\n restGround = MathF.Min(restGround, Vector3.Dot(rest.Pos, srcUp));\n for (var f = 0; f < frames.Count; f++)\n {\n worlds[f] = new Skeleton.Pose(srcFrames[f]).ToWorld(source);\n foreach (var bone in worlds[f])\n motionGround = MathF.Min(motionGround, Vector3.Dot(bone.Pos, srcUp));\n }\n var placement = scene.RestPlacementAuthored ? 0f : motionGround - restGround;\n var corrections = new List<(FootChain Chain, Vector3[] Goals)>();\n var lowerPelvis = new float[frames.Count];\n foreach (var (footRole, toeRole, chain) in new[]\n {\n (BoneRole.FootL, BoneRole.ToeL, feet.Left), (BoneRole.FootR, BoneRole.ToeR, feet.Right),\n })\n {\n if (!map.RoleToBone.TryGetValue(footRole, out var foot)\n || !map.RoleToBone.TryGetValue(toeRole, out var toe) || chain.Toe is not int tgtToe)\n continue;\n float Support(IReadOnlyList<XForm> pose, int ankle, int tip, Vector3 vertical)\n => MathF.Min(Vector3.Dot(pose[ankle].Pos, vertical), Vector3.Dot(pose[tip].Pos, vertical));\n var srcRest = Support(source.RestWorld, foot, toe, srcUp);\n var tgtRest = Support(target.RestWorld, chain.Ankle, tgtToe, up);\n // Animation-only FBXs can store a mid-stride rest, with one foot lifted.\n // That raised foot is not a ground reference: subtracting it drives its\n // planted frames below the floor. An authored rest never needs a higher\n // support baseline than the lowest support actually reached by that foot.\n if (scene.RestPlacementAuthored)\n foreach (var pose in worlds)\n srcRest = MathF.Min(srcRest, Support(pose, foot, toe, srcUp));\n var srcHeight = Vector3.Dot(source.RestWorld[srcHips].Pos, srcUp) - srcRest;\n if (srcHeight <= .001f)\n continue;\n var ratio = Math.Clamp((Vector3.Dot(target.RestWorld[tgtHips].Pos, up) - tgtRest) / srcHeight, .25f, 4f);\n var goals = new Vector3[frames.Count];\n for (var f = 0; f < frames.Count; f++)\n {\n var world = new Skeleton.Pose(frames[f]).ToWorld(target);\n var desired = tgtRest + (Support(worlds[f], foot, toe, srcUp) - srcRest - placement) * ratio;\n goals[f] = world[chain.Ankle].Pos + up * (desired - Support(world, chain.Ankle, tgtToe, up));\n var reach = Vector3.Distance(world[chain.Hip].Pos, world[chain.Knee].Pos)\n + Vector3.Distance(world[chain.Knee].Pos, world[chain.Ankle].Pos);\n var toHip = world[chain.Hip].Pos - goals[f];\n if (scene.CaptureSpace is not null)\n {\n lowerPelvis[f] = MathF.Max(lowerPelvis[f], Motion.CaptureLegReach.Lowering(toHip, up, reach));\n continue;\n }\n var vertical = Vector3.Dot(toHip, up);\n var horizontal = toHip - up * vertical;\n if (horizontal.LengthSquared() < reach * reach)\n lowerPelvis[f] = MathF.Max(lowerPelvis[f], vertical\n - MathF.Sqrt(reach * reach - horizontal.LengthSquared()));\n }\n corrections.Add((chain, goals));\n }\n // At full extension, lower the pelvis only as far as needed to reach BOTH\n // contacts. Stretching bones or accepting an unreachable goal leaves a hover.\n for (var f = 0; f < frames.Count; f++)\n {\n if (lowerPelvis[f] <= 0f) continue;\n var parent = target[tgtHips].ParentIndex;\n var shift = -up * lowerPelvis[f];\n if (parent >= 0)\n shift = Vector3.Transform(shift, Quaternion.Conjugate(\n new Skeleton.Pose(frames[f]).ToWorld(target)[parent].Rot));\n frames[f][tgtHips].Pos += shift;\n }\n foreach (var (chain, goals) in corrections)\n {\n // Vertical IK preserves horizontal travel and foot rotation; it never scales bones.\n EffectorIk.ApplyGoals(frames, target,\n new LimbChain { Upper = chain.Hip, Lower = chain.Knee, End = chain.Ankle },\n goals, ArmIkCleanup.RestBendAxis(target, chain.Hip, chain.Knee, chain.Ankle), soften: 0f);\n }\n }\n\n private static bool TryDetachedHeadNeutralCorrection(\n TargetRig rig, int head, out Quaternion localCorrection)\n {\n localCorrection = Quaternion.Identity;\n CharacterFrame frame;\n try\n {\n frame = CharacterFrame.Compute(\n rig.Skeleton, rig.ToMappingResult(), rig.Skeleton.RestWorld);\n }\n catch (ArgumentException)\n {\n return false;\n }\n\n var restRotation = MathQ.Normalize(rig.Skeleton.RestWorld[head].Rot);\n var gaze = Vector3.Zero;\n var agreement = float.NegativeInfinity;\n foreach (var localAxis in new[] { Vector3.UnitX, Vector3.UnitY, Vector3.UnitZ })\n {\n var axis = Vector3.Transform(localAxis, restRotation);\n var dot = Vector3.Dot(axis, frame.Forward);\n if (MathF.Abs(dot) <= agreement)\n continue;\n agreement = MathF.Abs(dot);\n gaze = dot < 0f ? -axis : axis;\n }\n\n var vertical = Vector3.Dot(gaze, frame.Up);\n const float minAgreement = 0.94f; // local axis must be within 20\u00b0 of facing\n const float minPitch = 0.5f * MathF.PI / 180f;\n const float maxPitch = 12f * MathF.PI / 180f;\n var pitch = MathF.Asin(Math.Clamp(vertical, -1f, 1f));\n if (agreement < minAgreement || MathF.Abs(pitch) < minPitch || MathF.Abs(pitch) > maxPitch)\n return false;\n\n var levelGaze = gaze - frame.Up * vertical;\n if (levelGaze.LengthSquared() < 1e-8f)\n return false;\n levelGaze = Vector3.Normalize(levelGaze);\n\n var worldCorrection = MathQ.FromTo(gaze, levelGaze);\n localCorrection = MathQ.Normalize(\n Quaternion.Conjugate(restRotation) * worldCorrection * restRotation);\n return true;\n }\n\n /// <summary>\n /// The engine-side yaw correction for embedded meshes and compiled Z-up targets (see\n /// <see cref=\"EmitClip\"/>): the compiler's source-axis conversion for ROOT-LEVEL\n /// animation channels lands 90\u00b0 about up away from where it puts the mesh bind.\n /// Pre-rotating root locals by the inverse importer yaw (\u221290\u00b0 for Y-up,\n /// +90\u00b0 for Z-up) makes compiled playback match the bind; children are parent-relative.\n /// </summary>\n private static List<XForm[]> CompensateEmbeddedMeshRootYaw(\n IReadOnlyList<XForm[]> frames, TargetRig rig, TargetUpAxis upAxis)\n {\n var yUp = upAxis == TargetUpAxis.YUpCm;\n var axis = yUp ? Vector3.UnitY : Vector3.UnitZ;\n var yaw = Quaternion.CreateFromAxisAngle(axis, (yUp ? -1f : 1f) * MathF.PI * 0.5f);\n var skeleton = rig.Skeleton;\n var result = new List<XForm[]>(frames.Count);\n foreach (var frame in frames)\n {\n var copy = new XForm[frame.Length];\n for (var i = 0; i < frame.Length; i++)\n {\n copy[i] = i < skeleton.Count && skeleton[i].ParentIndex < 0\n ? new XForm(\n Vector3.Transform(frame[i].Pos, yaw),\n MathQ.Normalize(yaw * frame[i].Rot))\n : frame[i];\n }\n result.Add(copy);\n }\n return result;\n }\n\n /// <summary>Test seam for <see cref=\"CompensateEmbeddedMeshRootYaw\"/> (gate bisects\n /// re-serialize mutated frames with the same compensation the pipeline applies).</summary>\n public static List<XForm[]> TestHook_CompensateEmbeddedMeshRootYaw(\n IReadOnlyList<XForm[]> frames, RetargetTargetSpec target)\n => !string.IsNullOrEmpty(target.MeshFilePath) || target.UpAxis == TargetUpAxis.ZUpEngine\n ? CompensateEmbeddedMeshRootYaw(frames, target.Rig, target.UpAxis)\n : frames.ToList();\n\n /// <summary>Test seam for <see cref=\"FollowHipsWithOrphanBones\"/>.</summary>\n public static void TestHook_FollowOrphans(TargetRig rig, List<XForm[]> frames)\n => FollowHipsWithOrphanBones(rig, frames, new MappingReportInfo\n {\n ProfileName = \"test\",\n Source = MappingSource.Manual,\n Confidence = 1f,\n NeedsUserDecision = false,\n MappedRoleCount = 0,\n SkeletonSignature = \"\",\n });\n\n /// <summary>\n /// <see cref=\"RetargetRequest.PreserveSourceTranslations\"/>: per-frame source local\n /// translations onto same-named target bones. Names are compared engine-sanitized\n /// (non-alphanumerics \u2192 <c>_</c>: the compiled-model rig the editor rebuilds carries\n /// <c>Bip01_R_Hand</c> for the file's <c>Bip01 R Hand</c>). A matched bone whose REST\n /// translation disagrees between the rigs is skipped \u2014 the name collision is then\n /// coincidence, not the same joint. The hips and its ancestors always stay\n /// solver-owned (trajectory, root-motion modes, hip rescale).\n /// </summary>\n private static void PassThroughSourceTranslations(\n SourceScene scene, TargetRig rig, List<XForm[]> frames, int take,\n MappingReportInfo report)\n {\n var src = scene.Skeleton;\n var tgt = rig.Skeleton;\n var srcClip = scene.Clips[take];\n if (srcClip.Frames.Count != frames.Count)\n {\n AddNote(report, \"Source translations not preserved: frame counts differ \"\n + $\"({srcClip.Frames.Count} source vs {frames.Count} solved).\");\n return;\n }\n\n static string Key(string name)\n {\n Span<char> c = stackalloc char[name.Length];\n for (var i = 0; i < name.Length; i++)\n c[i] = char.IsLetterOrDigit(name[i]) ? char.ToLowerInvariant(name[i]) : '_';\n return new string(c);\n }\n\n var srcByName = new Dictionary<string, int>();\n for (var i = 0; i < src.Count; i++)\n {\n // Duplicate keys are ambiguous - poison them rather than guess.\n var key = Key(src[i].Name);\n srcByName[key] = srcByName.ContainsKey(key) ? -1 : i;\n }\n\n var excluded = new HashSet<int>();\n if (rig.BoneForRole(BoneRole.Hips) is { } hips)\n for (var b = hips; b >= 0; b = tgt[b].ParentIndex)\n excluded.Add(b);\n\n var applied = 0;\n for (var t = 0; t < tgt.Count; t++)\n {\n if (excluded.Contains(t)\n || !srcByName.TryGetValue(Key(tgt[t].Name), out var s) || s < 0)\n continue;\n // Same joint check: rest translations must agree (same rig, same space).\n var restDelta = (src[s].RestLocal.Pos - tgt[t].RestLocal.Pos).Length();\n if (restDelta > MathF.Max(1f, 0.1f * tgt[t].RestLocal.Pos.Length()))\n continue;\n for (var f = 0; f < frames.Count; f++)\n frames[f][t] = new XForm(srcClip.Frames[f][s].Pos, frames[f][t].Rot);\n applied++;\n }\n if (applied > 0)\n AddNote(report, $\"Embedded take: authored local translations preserved on {applied} bones.\");\n }\n\n /// <summary>\n /// Normalized-rest foot pitch beyond which (toe approaching/above ankle level) the rest\n /// cannot be a flat stance \u2014 measured stance rests sit at \u221211\u00b0\u2026\u221251\u00b0; the repro artifact\n /// rig measured +2.7\u00b0 on one foot.\n /// </summary>\n private const float StancePitchMaxDeg = -5f;\n\n /// <summary>Left/right normalized-rest foot pitch asymmetry beyond which the rest is not\n /// a (symmetric) stance \u2014 measured stance rests differ \u2264 2.4\u00b0; the repro artifact rig\n /// 13.8\u00b0.</summary>\n private const float StancePitchAsymmetryDeg = 6f;\n\n /// <summary>\n /// The grounded-foot stance recalibration step (<see cref=\"FootGroundAlign\"/>). Runs ONLY\n /// when the source's NORMALIZED rest \u2014 the solver's delta reference \u2014 is implausible as a\n /// flat stance (a foot's toe at/above ankle level, or the two feet resting asymmetrically;\n /// both measured on the repro rig, where T-pose normalization of a bent-leg rest swings\n /// the feet 14\u201326\u00b0 away from the rig's true stance). On plausible stance rests the solver's\n /// rest-relative foot transfer is already faithful, and planted-sole deviations are\n /// genuine articulation (boxing stances, heel rolls) that must NOT be flattened. Plant\n /// intervals are detected on the SOURCE clip (cm space, default Kovar thresholds).\n /// Best-effort \u2014 silently skipped when the source maps no complete leg chains + toes or a\n /// character frame is not computable (the foot transfer is then simply left as solved).\n /// </summary>\n private static void GroundAlignFeet(\n List<XForm[]> frames, SourceScene scene, MappingResult map, SkeletonModel targetSkeleton,\n (FootChain Left, FootChain Right) targetFeet, Vector3 targetUp, float fps, int take)\n {\n FootChain? SourceChain(BoneRole upper, BoneRole lower, BoneRole foot, BoneRole toe)\n => map.RoleToBone.TryGetValue(upper, out var hip)\n && map.RoleToBone.TryGetValue(lower, out var knee)\n && map.RoleToBone.TryGetValue(foot, out var ankle)\n ? new FootChain\n {\n Hip = hip,\n Knee = knee,\n Ankle = ankle,\n Toe = map.RoleToBone.TryGetValue(toe, out var t) ? t : null,\n }\n : null;\n\n var srcLeft = SourceChain(BoneRole.UpperLegL, BoneRole.LowerLegL, BoneRole.FootL, BoneRole.ToeL);\n var srcRight = SourceChain(BoneRole.UpperLegR, BoneRole.LowerLegR, BoneRole.FootR, BoneRole.ToeR);\n if (srcLeft?.Toe is not int srcToeL || srcRight?.Toe is not int srcToeR)\n return; // toe-less sources take the solver's virtual-foot fallback instead\n\n // ---- rest-stance plausibility (normalized rest, the solver's delta reference) ----\n float pitchL, pitchR;\n Vector3 srcUp;\n try\n {\n var (srcNorm, _) = RestNormalizer.Normalize(scene.Skeleton, map);\n var srcCanon = CanonicalFrames.Build(scene.Skeleton, map, srcNorm.WorldRest);\n srcUp = Vector3.Normalize(srcCanon.CharacterUp);\n\n float RestPitchDeg(int foot, int toe)\n {\n var dir = srcNorm.WorldRest[toe].Pos - srcNorm.WorldRest[foot].Pos;\n if (dir.LengthSquared() < 1e-8f)\n return 0f;\n var s = Vector3.Dot(Vector3.Normalize(dir), srcUp);\n return MathF.Asin(Math.Clamp(s, -1f, 1f)) * (180f / MathF.PI);\n }\n\n pitchL = RestPitchDeg(srcLeft.Ankle, srcToeL);\n pitchR = RestPitchDeg(srcRight.Ankle, srcToeR);\n }\n catch (ArgumentException)\n {\n return;\n }\n\n var restIsStance = pitchL < StancePitchMaxDeg && pitchR < StancePitchMaxDeg\n && MathF.Abs(pitchL - pitchR) <= StancePitchAsymmetryDeg;\n if (restIsStance)\n return;\n\n var srcFrames = scene.Clips[take].Frames;\n var (plantsL, plantsR) = FootPlant.DetectPlantIntervals(\n srcFrames, scene.Skeleton, srcLeft, srcRight, srcUp, fps);\n if (plantsL.Count == 0 && plantsR.Count == 0)\n return;\n\n FootGroundAlign.Apply(\n frames, targetSkeleton, targetFeet.Left, targetFeet.Right, targetUp,\n plantsL, plantsR);\n }\n\n /// <summary>\n /// Restores the clip's SOURCE-authored foot-to-ground relationship with ONE constant\n /// vertical offset per clip (southpaw final regeneration package: DEFERRED.md f2 and\n /// gate4_review.md 3.1). The solver's vertical reference is the source REST pelvis;\n /// mid-pose exports whose kept node rest erased the authored crouch leave every solved\n /// frame hovering by exactly the erased crouch depth (measured: stance +5.9 cm, steps\n /// +7.8 to +10.3, slips ~+9, while the source FBX is grounded everywhere). Fix, the\n /// generalization of the BVH ClipPlacementOffset vertical logic to every source: measure\n /// the clip's minimum foot support (ankles + toes) in the SOLVED target frames and shift\n /// the whole clip vertically so that support equals the TARGET rest support plus the\n /// source clip's own authored support delta (source clip support minus source rest\n /// support, hip-height scaled). One constant per clip preserves every within-clip lift,\n /// heel raise and step cycle exactly: authored articulation is never flattened and an\n /// authored jump stays airborne (its source support delta rides along).\n /// </summary>\n private static void AlignSupportToGround(\n List<XForm[]> frames, SourceScene scene, MappingResult map, TargetRig rig,\n (FootChain Left, FootChain Right) feet, Vector3 up, MappingReportInfo report, int take)\n {\n if (frames.Count == 0)\n return;\n var skeleton = rig.Skeleton;\n var rest = skeleton.RestWorld;\n var srcClip = scene.Clips[take];\n var src = scene.Skeleton;\n\n // The GROUND normal is the rig-space vertical axis, not the character-lean up:\n // the character frame's up tilts a few degrees with the rest posture, and a\n // tilted dot-metric slants the ground line across a traveling clip (measured:\n // 28 cm of false offset on a long corpus walk). Snap to the dominant axis.\n up = GroundUp(up);\n\n // ---- support-bone set: the roles mapped on BOTH rigs (metrics must compare\n // the same anatomical points or anthropometric offsets leak into the gap) ----\n var supportRoles = new List<BoneRole>();\n foreach (var role in new[] { BoneRole.FootL, BoneRole.FootR, BoneRole.ToeL, BoneRole.ToeR })\n {\n if (map.RoleToBone.ContainsKey(role) && rig.BoneForRole(role) is not null)\n supportRoles.Add(role);\n }\n var sourceMeasured = supportRoles.Contains(BoneRole.FootL) && supportRoles.Contains(BoneRole.FootR)\n && srcClip.Frames.Count > 0 && srcClip.Frames[0].Length == src.Count;\n if (!sourceMeasured)\n {\n // No reliable common support metric: fall back to the ankles the target\n // chains provide and a zero source gap (best guess: grounded).\n supportRoles.Clear();\n }\n\n var tgtSupportBones = new List<int>();\n foreach (var role in supportRoles)\n tgtSupportBones.Add(rig.BoneForRole(role)!.Value);\n if (tgtSupportBones.Count == 0)\n {\n tgtSupportBones.Add(feet.Left.Ankle);\n tgtSupportBones.Add(feet.Right.Ankle);\n }\n\n var tgtRestSupport = float.MaxValue;\n foreach (var b in tgtSupportBones)\n tgtRestSupport = MathF.Min(tgtRestSupport, Vector3.Dot(rest[b].Pos, up));\n\n var solvedSupport = float.MaxValue;\n foreach (var frame in frames)\n {\n var world = new Skeleton.Pose(frame).ToWorld(skeleton);\n foreach (var b in tgtSupportBones)\n solvedSupport = MathF.Min(solvedSupport, Vector3.Dot(world[b].Pos, up));\n }\n\n // ---- source-authored support gap --------------------------------------\n float sourceRel = 0f;\n if (sourceMeasured)\n {\n var srcSupportBones = new List<int>();\n foreach (var role in supportRoles)\n srcSupportBones.Add(map.RoleToBone[role]);\n\n var srcUp = scene.UpAxis switch\n {\n 0 => new Vector3(scene.UpAxisSign, 0f, 0f),\n 2 => new Vector3(0f, 0f, scene.UpAxisSign),\n _ => new Vector3(0f, scene.UpAxisSign, 0f),\n };\n var srcHips = map.RoleToBone.TryGetValue(BoneRole.Hips, out var sh) ? sh : -1;\n\n var srcRestSupport = float.MaxValue;\n var srcRestGround = float.MaxValue;\n for (var b = 0; b < src.Count; b++)\n {\n var h = Vector3.Dot(src.RestWorld[b].Pos, srcUp);\n srcRestGround = MathF.Min(srcRestGround, h);\n }\n foreach (var b in srcSupportBones)\n srcRestSupport = MathF.Min(srcRestSupport, Vector3.Dot(src.RestWorld[b].Pos, srcUp));\n\n var srcClipSupport = float.MaxValue;\n var srcMotionGround = float.MaxValue;\n foreach (var frame in srcClip.Frames)\n {\n var world = new Skeleton.Pose(frame).ToWorld(src);\n foreach (var b in srcSupportBones)\n srcClipSupport = MathF.Min(srcClipSupport, Vector3.Dot(world[b].Pos, srcUp));\n if (!scene.RestPlacementAuthored)\n {\n for (var b = 0; b < src.Count; b++)\n srcMotionGround = MathF.Min(srcMotionGround, Vector3.Dot(world[b].Pos, srcUp));\n }\n }\n\n // AUTHORED placements (FBX): the rest stands on the authored ground, so the\n // gap is simply clip support minus rest support (same bones, same space).\n // UN-PLACED sources (BVH capture volumes): absolute placement is meaningless,\n // so both terms go placement-free: the clip's support above its OWN motion\n // ground (lowest joint anywhere, incl. static origin markers) compared to the\n // rest pose's support above ITS lowest joint. A grounded walk reads ~0, a\n // jump reads its true airborne height, a crawl reads its raised feet.\n float gap;\n if (scene.RestPlacementAuthored)\n gap = srcClipSupport - srcRestSupport;\n else\n gap = (srcClipSupport - srcMotionGround) - (srcRestSupport - srcRestGround);\n\n // Hip-height ratio scales the (small) source gap into target units,\n // clamped against degenerate rest measurements.\n var ratio = 1f;\n if (srcHips >= 0 && rig.BoneForRole(BoneRole.Hips) is { } tgtHips)\n {\n var srcHipHeight = Vector3.Dot(src.RestWorld[srcHips].Pos, srcUp) - srcRestSupport;\n var tgtHipHeight = Vector3.Dot(rest[tgtHips].Pos, up) - tgtRestSupport;\n if (srcHipHeight > 1e-3f && float.IsFinite(tgtHipHeight / srcHipHeight))\n ratio = Math.Clamp(tgtHipHeight / srcHipHeight, 0.25f, 4f);\n }\n sourceRel = gap * ratio;\n }\n\n var delta = tgtRestSupport + sourceRel - solvedSupport;\n if (!float.IsFinite(delta) || MathF.Abs(delta) > 100f)\n {\n AddNote(report, $\"Support ground alignment skipped: implausible offset {delta:0.0}.\");\n return;\n }\n if (MathF.Abs(delta) < 0.01f)\n return; // already aligned\n\n var shift = up * delta;\n foreach (var frame in frames)\n {\n for (var i = 0; i < skeleton.Count; i++)\n {\n if (skeleton[i].ParentIndex < 0)\n frame[i] = new XForm(frame[i].Pos + shift, frame[i].Rot);\n }\n }\n AddNote(report, sourceMeasured\n ? $\"Support ground alignment: {delta:0.00} vertical offset applied \"\n + $\"(source-authored support delta {sourceRel:0.00}).\"\n : $\"Support ground alignment: {delta:0.00} vertical offset applied \"\n + \"(source feet unmapped; clip support aligned to target rest support).\");\n }\n\n private static Vector3 GroundUp(Vector3 up)\n {\n var absolute = Vector3.Abs(up);\n return absolute.X >= absolute.Y && absolute.X >= absolute.Z\n ? new Vector3(MathF.Sign(up.X), 0, 0)\n : absolute.Y >= absolute.Z\n ? new Vector3(0, MathF.Sign(up.Y), 0)\n : new Vector3(0, 0, MathF.Sign(up.Z));\n }\n\n private static void ApplyRootMotion(\n RootMotionMode mode, List<XForm[]> frames, TargetContext context, MappingReportInfo report)\n {\n if (mode == RootMotionMode.Off)\n return;\n\n if (context.HipsIndex is not { } hips)\n {\n AddNote(report, $\"Root motion ({mode}) skipped: target rig maps no Hips role.\");\n return;\n }\n if (context.Up is not { } up)\n {\n AddNote(report, $\"Root motion ({mode}) skipped: {context.UpOrChainProblem}\");\n return;\n }\n\n if (mode == RootMotionMode.Extract && context.DedicatedRootIndex is null)\n {\n // s&box-style target: pelvis is parentless and there is no separate translating\n // root bone (root_IK is IkBaked). Bone-level extraction is impossible \u2014 the vmdl\n // AnimFile entry carries an ExtractMotion node instead (compile-time extraction).\n AddNote(report,\n \"Root-motion extract: target has no dedicated animated root bone distinct from \"\n + \"the hips; frames left as solved and motion extraction delegated to the vmdl \"\n + \"AnimFile ExtractMotion node.\");\n return;\n }\n\n var root = context.DedicatedRootIndex ?? hips;\n var worldUp = GroundUp(up);\n // Skeleton-aware overload: hips world via real parent-chain FK, locals re-derived\n // against the actual parent (HipsParentIsRoot is ignored by this overload).\n RootMotion.Apply(frames, context.Rig.Skeleton, new RootMotionAxes\n {\n // Match the world vertical used by support-height cleanup. A slightly\n // leaning target torso must not tilt the plane used to remove travel;\n // otherwise this final pass reintroduces vertical drift into grounded feet.\n Up = worldUp,\n RootIndex = root,\n HipsIndex = hips,\n HipsParentIsRoot = context.HipsParentIsRoot,\n }, mode);\n if (mode == RootMotionMode.InPlace && frames.Count > 0)\n {\n // Animation-only files may start far from their static reference pose.\n // Removing travel alone freezes that offset into the clip, so blending\n // directions moves the mesh away from its controller. Center every clip\n // on the target bind hips, preserving vertical motion and local rotations.\n var skeleton = context.Rig.Skeleton;\n var center = Vector3.Zero;\n foreach (var frame in frames)\n center += FkUtil.BoneWorld(frame, skeleton, hips).Pos;\n var offset = skeleton.RestWorld[hips].Pos - center / frames.Count;\n offset -= Vector3.Dot(offset, worldUp) * worldUp;\n foreach (var frame in frames)\n for (var b = 0; b < skeleton.Count; b++)\n if (skeleton[b].ParentIndex < 0)\n frame[b] = new XForm(frame[b].Pos + offset, frame[b].Rot);\n }\n AddNote(report, mode == RootMotionMode.Extract\n ? $\"Root motion extracted onto dedicated root bone (index {root}).\"\n : \"Root motion removed (in-place clip).\");\n }\n\n private static void AddNote(MappingReportInfo report, string note)\n {\n if (!report.Notes.Contains(note))\n report.Notes.Add(note);\n }\n\n /// <summary>\n /// Mid-pose FBX sources: adopts the importer's bind-rest alternate skeleton when it\n /// makes the character stand STRAIGHTER along the file's own up axis. Solving on a\n /// posed rest breaks the solver's anatomical reasoning on exactly the posed limbs\n /// (measured 48\u00b0 mean world-rot off ground truth on a real rig, left foot 124\u00b0); but\n /// a bind rest can also tilt the character-up estimate (shoulders behind hips on one\n /// real rig tilted it 7\u00b0, starving plant/stance detection), so the straighter rest\n /// wins. Clips are authored playback either way \u2014 same bones, same order.\n /// </summary>\n private static SourceScene MaybeAdoptBindRest(\n SourceScene scene, MappingResult map, MappingReportInfo report)\n {\n if (scene.MidPoseBindSkeleton is not { } bindSkeleton)\n return scene;\n\n var fileUp = scene.UpAxis switch\n {\n 0 => new Vector3(scene.UpAxisSign, 0, 0),\n 2 => new Vector3(0, 0, scene.UpAxisSign),\n _ => new Vector3(0, scene.UpAxisSign, 0),\n };\n\n float UpTiltDeg(SkeletonModel skeleton)\n {\n var (norm, _) = RestNormalizer.Normalize(skeleton, map);\n var up = Vector3.Normalize(\n CanonicalFrames.Build(skeleton, map, norm.WorldRest).CharacterUp);\n return MathF.Acos(Math.Clamp(Vector3.Dot(up, fileUp), -1f, 1f)) * 180f / MathF.PI;\n }\n\n try\n {\n float tiltStatics = UpTiltDeg(scene.Skeleton);\n float tiltBind = UpTiltDeg(bindSkeleton);\n if (tiltBind > tiltStatics + 1f)\n {\n AddNote(report,\n $\"mid-pose export: kept the exported pose as rest - the file's bind \"\n + $\"data stands {tiltBind:0.#} deg off the file up axis \"\n + $\"(vs {tiltStatics:0.#} deg)\");\n return scene;\n }\n\n AddNote(report,\n \"mid-pose export: rest pose restored from the file's own bind data \"\n + \"(clips keep their authored playback)\");\n return new SourceScene(\n bindSkeleton, scene.Clips, scene.UnitScaleCm,\n scene.UpAxis, scene.UpAxisSign,\n scene.FrontAxis, scene.FrontAxisSign,\n scene.CoordAxis, scene.CoordAxisSign,\n scene.OriginalUpAxis, scene.Notes)\n {\n AuthoredMapping = scene.AuthoredMapping,\n RestPlacementAuthored = scene.RestPlacementAuthored,\n };\n }\n catch (Exception)\n {\n // Anatomy not measurable (mapping lacks the frame's roles, degenerate rest):\n // keep today's behavior.\n return scene;\n }\n }\n\n // ================================================================ import + mapping\n\n /// <summary>\n /// Imports source-file bytes exactly like conversion does: the extension picks the\n /// importer; unknown extensions fall back to content sniffing (FBX binary magic / ASCII\n /// header token / GLB 'glTF' magic / glTF JSON). Public so UI listings\n /// inspect files through the SAME import path the pipeline uses (no duplicate sniffing\n /// logic caller-side).\n /// </summary>\n /// <param name=\"data\">Raw file bytes.</param>\n /// <param name=\"fileName\">File name; only the extension is consulted.</param>\n /// <param name=\"sampleFps\">Resample rate for the imported clips; null = importer default\n /// (30 fps).</param>\n /// <exception cref=\"FormatException\">Thrown when the bytes are not a readable\n /// motion, FBX or glTF animation.</exception>\n public static SourceScene ImportSource(\n byte[] data, string fileName, float? sampleFps = null,\n Func<string, byte[]>? externalBufferResolver = null)\n {\n ArgumentNullException.ThrowIfNull(data);\n ArgumentNullException.ThrowIfNull(fileName);\n\n var fbxOptions = sampleFps is { } fbxFps ? new FbxImportOptions { SampleFps = fbxFps } : null;\n var gltfOptions = sampleFps is not null || externalBufferResolver is not null\n ? new GltfImportOptions\n {\n SampleFps = sampleFps ?? 30f,\n ExternalBufferResolver = externalBufferResolver,\n }\n : null;\n var ext = ExtensionOf(fileName);\n return ext switch\n {\n \"hmotion\" => Motion.MotionDocument.Parse(data).ToSourceScene(sampleFps),\n \"fbx\" => FbxImporter.Import(data, fbxOptions),\n // .vrm = glTF 2.0 GLB container + VRM extension (the importer reads the authored\n // humanoid bone map into SourceScene.AuthoredMapping); unknown-extension VRM\n // bytes also land here via the GLB magic sniff below.\n \"glb\" or \"gltf\" or \"vrm\" => GltfImporter.Import(data, gltfOptions),\n _ => SniffFormat(data) switch\n {\n \"fbx\" => FbxImporter.Import(data, fbxOptions),\n \"gltf\" => GltfImporter.Import(data, gltfOptions),\n _ => throw new FormatException(\n $\"Unrecognized source format for '{fileName}' (expected .hmotion, .fbx, .glb, .gltf or .vrm).\"),\n },\n };\n }\n\n private static string? SniffFormat(byte[] data)\n {\n // Binary FBX magic: \"Kaydara FBX Binary \\0\".\n const string fbxMagic = \"Kaydara FBX Binary\";\n if (StartsWithAscii(data, fbxMagic))\n return \"fbx\";\n\n // GLB magic: 'glTF' (0x46546C67 little-endian).\n if (StartsWithAscii(data, \"glTF\"))\n return \"gltf\";\n\n var head = Encoding.UTF8.GetString(data, 0, Math.Min(data.Length, 4096));\n if (head.Contains(\"FBXHeaderExtension\", StringComparison.Ordinal))\n return \"fbx\"; // ASCII FBX\n var trimmed = head.TrimStart();\n if (trimmed.StartsWith(\"{\", StringComparison.Ordinal)\n && head.Contains(\"\\\"asset\\\"\", StringComparison.Ordinal))\n return \"gltf\"; // plain-JSON glTF\n return null;\n }\n\n private static bool StartsWithAscii(byte[] data, string prefix)\n {\n if (data.Length < prefix.Length)\n return false;\n for (var i = 0; i < prefix.Length; i++)\n {\n if (data[i] != (byte)prefix[i])\n return false;\n }\n return true;\n }\n\n /// <summary>\n /// THE mapping cascade, shared by conversion, UI file listings, and custom-target\n /// detection: explicit override \u2192 authored mapping (from the file itself, e.g. a VRM's\n /// humanoid bone map \u2014 authoritative ground truth at confidence 1.0) \u2192 user preset (via\n /// <paramref name=\"userPresetLookup\"/>, keyed by <see cref=\"Mapping.SkeletonSignature\"/>)\n /// \u2192 shipped preset detection \u2192 best-effort auto-map. The report's\n /// <see cref=\"MappingReportInfo.NeedsUserDecision\"/> is true only on the auto path below\n /// the preset detection threshold \u2014 conversion still proceeds with that map; callers\n /// decide whether to ask/reject.\n /// </summary>\n /// <param name=\"skeleton\">Source (or candidate target) skeleton.</param>\n /// <param name=\"mappingOverride\">Explicit mapping (manual table / already-resolved user\n /// preset); wins outright when non-null \u2014 a deliberate user decision beats even the\n /// file's own bone map.</param>\n /// <param name=\"userPresetLookup\">Editor-side user-preset hook: receives the skeleton's\n /// signature, returns the stored mapping or null. The facade itself can do no file IO.</param>\n /// <param name=\"authoredMapping\">A mapping authored INSIDE the source file\n /// (<see cref=\"SourceScene.AuthoredMapping\"/>, <see cref=\"MappingSource.Authored\"/>);\n /// consulted before user presets because the file itself is authoritative.</param>\n public static (MappingResult Map, MappingReportInfo Report) ResolveMapping(\n SkeletonModel skeleton, MappingResult? mappingOverride = null,\n Func<string, MappingResult?>? userPresetLookup = null,\n MappingResult? authoredMapping = null)\n {\n ArgumentNullException.ThrowIfNull(skeleton);\n\n if (mappingOverride is not null)\n return (mappingOverride, BuildReport(mappingOverride, needsUserDecision: false, skeleton));\n\n if (authoredMapping is not null)\n return (authoredMapping, BuildReport(authoredMapping, needsUserDecision: false, skeleton));\n\n if (userPresetLookup is not null\n && userPresetLookup(Mapping.SkeletonSignature.Compute(skeleton)) is { } userPreset)\n return (userPreset, BuildReport(userPreset, needsUserDecision: false, skeleton));\n\n if (ProfileDetector.Detect(skeleton) is { } detected)\n {\n AutoMapper.CompleteSingleJointThumbs(skeleton, detected.Result);\n AutoMapper.CompleteFingersByTopology(skeleton, detected.Result);\n AutoMapper.PruneNonArticulatedFingerStubs(skeleton, detected.Result);\n VetoImpossibleHead(skeleton, detected.Result);\n return (detected.Result, BuildReport(detected.Result, needsUserDecision: false, skeleton));\n }\n\n var auto = AutoMapper.Map(skeleton);\n VetoImpossibleHead(skeleton, auto);\n var needsUserDecision = auto.Confidence < ProfileDetector.DetectionThreshold;\n return (auto, BuildReport(auto, needsUserDecision, skeleton));\n }\n\n /// <summary>\n /// Drops a detected/auto-mapped Head whose bone cannot anatomically drive a skull: its\n /// rest sits clearly BELOW the neck (or the topmost mapped spine joint). Real case: an\n /// Auto-Rig Pro export parks <c>head.x</c> at chest height ~90cm under <c>neck.x</c>\n /// with the skin bind compensating \u2014 the mesh is correct at rest, but every skull\n /// rotation replayed onto that bone sweeps the head geometry on the lever arm (\"the\n /// head is stretched and it stretched more when I played the animation\"). With the\n /// role unmapped the neck \u2014 whose pivot IS at the skull base on such rigs \u2014 carries\n /// head motion, and the bone holds its rest like any other unmapped helper.\n /// Neutral-rest corpus rigs measure the neck\u2192head segment \u22128\u00b0\u202633\u00b0 from character up\n /// (dot \u2265 0.55 even on the posed Defenses bind at 40.7\u00b0); the veto fires only past\n /// 101\u00b0 (dot < \u22120.2), an impossible carriage on any working rig. Explicit,\n /// authored and user-preset mappings are authoritative and never second-guessed.\n /// </summary>\n private static void VetoImpossibleHead(SkeletonModel skeleton, MappingResult map)\n {\n if (!map.RoleToBone.TryGetValue(BoneRole.Head, out var head)\n || !map.RoleToBone.TryGetValue(BoneRole.Hips, out var hips))\n return;\n\n var reference = map.RoleToBone.TryGetValue(BoneRole.Neck, out var neck)\n ? neck\n : SpineTop(map);\n if (reference is not { } refBone || refBone == hips)\n return;\n\n var rest = skeleton.RestWorld;\n // Axis-free \"up\": the body chain ascends hips \u2192 spine \u2192 neck by construction, so\n // hips\u2192reference is the rig's own vertical regardless of world axis convention.\n var up = rest[refBone].Pos - rest[hips].Pos;\n var seg = rest[head].Pos - rest[refBone].Pos;\n if (up.LengthSquared() < 1e-6f || seg.Length() < 0.05f * up.Length())\n return; // degenerate spine / head stacked on the neck: nothing to judge\n\n if (Vector3.Dot(Vector3.Normalize(seg), Vector3.Normalize(up)) >= -0.2f)\n return;\n\n map.RoleToBone.Remove(BoneRole.Head);\n // The neck of a broken skull region rests with the head. On the real case the\n // neck bone skins the skull AND a torso column (its weights are where the head's\n // should be), so any solved neck bend shears half the body: the source spreads\n // its idle hunch through its spine while this target's spine stays put, and the\n // faithful neck world replay turned into a ~19\u00b0 LOCAL kink rendered as a hump\n // grafted onto the character's back. Skull attitude rides the spine instead.\n var neckNote = \"\";\n if (map.RoleToBone.Remove(BoneRole.Neck))\n neckNote = $\" Its neck '{skeleton[neck].Name}' rests too (on such exports it \"\n + \"carries the skull/torso skin the head joint should).\";\n map.Notes.Add(\n $\"Head '{skeleton[head].Name}' unmapped: its rest sits below \"\n + $\"'{skeleton[refBone].Name}' - not a skull joint the solver can drive \"\n + \"(rotations would sweep the head geometry on the offset lever arm).\"\n + neckNote + \" The spine carries the upper body; map manually to override.\");\n }\n\n private static int? SpineTop(MappingResult map)\n {\n foreach (var role in new[]\n {\n BoneRole.Spine4, BoneRole.Spine3, BoneRole.Spine2, BoneRole.Spine1, BoneRole.Spine0,\n })\n {\n if (map.RoleToBone.TryGetValue(role, out var bone))\n return bone;\n }\n return null;\n }\n\n private static MappingReportInfo BuildReport(\n MappingResult map, bool needsUserDecision, SkeletonModel skeleton)\n {\n var report = new MappingReportInfo\n {\n ProfileName = map.ProfileName,\n Source = map.Source,\n Confidence = map.Confidence,\n NeedsUserDecision = needsUserDecision,\n MappedRoleCount = map.RoleToBone.Count,\n SkeletonSignature = Mapping.SkeletonSignature.Compute(skeleton),\n };\n report.Notes.AddRange(map.Notes);\n if (needsUserDecision)\n report.Notes.Add(\n $\"No preset profile matched and auto-map confidence {map.Confidence:0.00} is below \"\n + $\"{ProfileDetector.DetectionThreshold:0.00}; proceeding with the best-effort auto map.\");\n return report;\n }\n\n // ================================================================ naming\n\n private static string RequestedClipName(RetargetRequest request, SourceScene scene, int take)\n {\n var multipleTakes = scene.Clips.Count > 1;\n if (!string.IsNullOrWhiteSpace(request.ClipNameOverride))\n return multipleTakes ? $\"{request.ClipNameOverride}_{take + 1}\" : request.ClipNameOverride!;\n\n var takeName = scene.Clips[take].Name;\n if (!string.IsNullOrWhiteSpace(takeName) && takeName != \"motion\"\n && !takeName.Equals(\"mixamo.com\", StringComparison.OrdinalIgnoreCase)\n && !IsExporterPlaceholderTake(takeName))\n return takeName;\n\n // BVH files (clip always named \"motion\"), Mixamo takes (always named\n // \"mixamo.com\"), Unreal exports (every stack named \"Unreal Take\") and\n // unnamed takes use the file stem.\n var stem = FileStem(request.SourceFileName);\n return multipleTakes ? $\"{stem}_{take + 1}\" : stem;\n }\n\n /// <summary>\n /// True for take names that identify the EXPORTER rather than the animation. Unreal's\n /// FBX exporter stamps <c>Unreal Take</c> into every AnimStack it writes (numbered \u2014\n /// <c>Unreal Take 001</c> \u2014 when a sequence exports several), so a folder of UE clips\n /// arrives with one identical take name on every file. Treated as authored, that name\n /// beats the file stem in <see cref=\"RequestedClipName\"/> and the whole batch collapses\n /// onto one clip name, which <see cref=\"UniqueClipName\"/> then has to pull apart into\n /// <c>Unreal_Take</c>, <c>Unreal_Take_2</c>, <c>Unreal_Take_3</c>\u2026 \u2014 every file's real\n /// name lost, and the resulting sequence names carry no meaning in the vmdl. Falling\n /// through to the file stem restores the same per-file naming BVH and Mixamo sources\n /// already get. Matched as a PREFIX, and against the underscored spelling too, so the\n /// numbered variants and any already-sanitized re-import are covered.\n /// </summary>\n private static bool IsExporterPlaceholderTake(string takeName)\n {\n var trimmed = takeName.Trim();\n return trimmed.StartsWith(\"unreal take\", StringComparison.OrdinalIgnoreCase)\n || trimmed.StartsWith(\"unreal_take\", StringComparison.OrdinalIgnoreCase);\n }\n\n /// <summary>Collision auto-suffixing across the batch: <c>name</c>, <c>name_2</c>, \u2026\n /// (case-insensitive \u2014 the names also become DMX file names).</summary>\n private static string UniqueClipName(string name, HashSet<string> usedNames, bool autoSuffix)\n {\n if (usedNames.Add(name) || !autoSuffix)\n return name;\n for (var i = 2; ; i++)\n {\n var candidate = $\"{name}_{i}\";\n if (usedNames.Add(candidate))\n return candidate;\n }\n }\n\n /// <summary>\n /// Clip names become ModelDoc AnimFile node names and Source 2 sequence names, which\n /// must stay within <c>[A-Za-z0-9_]</c> \u2014 a take name like <c>mixamo.com</c> otherwise\n /// fails the vmdl compile with \"Node 'mixamo.com' resolve failure\". Runs of invalid\n /// characters collapse to a single underscore; case is preserved. Public so UIs that\n /// dry-run name-based detection (e.g. the locomotion-set scan over raw take names) see\n /// the SAME spelling conversion will produce \u2014 a take named <c>Walk N</c> becomes the\n /// clip <c>Walk_N</c>.\n /// </summary>\n public static string SanitizeClipName(string name)\n {\n var builder = new StringBuilder(name.Length);\n var lastWasUnderscore = false;\n foreach (var c in name)\n {\n if (c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or (>= '0' and <= '9') or '_')\n {\n builder.Append(c);\n lastWasUnderscore = c == '_';\n }\n else if (!lastWasUnderscore)\n {\n builder.Append('_');\n lastWasUnderscore = true;\n }\n }\n var sanitized = builder.ToString().Trim('_');\n return sanitized.Length > 0 ? sanitized : \"clip\";\n }\n\n private static string FileStem(string fileName)\n {\n var start = Math.Max(fileName.LastIndexOf('/'), fileName.LastIndexOf('\\\\')) + 1;\n var dot = fileName.LastIndexOf('.');\n var end = dot > start ? dot : fileName.Length;\n var stem = fileName.Substring(start, end - start);\n return stem.Length > 0 ? stem : \"clip\";\n }\n\n private static string ExtensionOf(string fileName)\n {\n var dot = fileName.LastIndexOf('.');\n if (dot < 0 || dot == fileName.Length - 1)\n return \"\";\n var ext = fileName.Substring(dot + 1);\n return ext.IndexOfAny(new[] { '/', '\\\\' }) >= 0 ? \"\" : ext.ToLowerInvariant();\n }\n\n private static string SanitizeFileName(string name)\n {\n var builder = new StringBuilder(name.Length);\n foreach (var c in name)\n {\n builder.Append(\n c is (>= 'a' and <= 'z') or (>= '0' and <= '9') or '_' or '-' ? c\n : c is >= 'A' and <= 'Z' ? char.ToLowerInvariant(c)\n : '_');\n }\n var sanitized = builder.ToString().Trim('_');\n return sanitized.Length > 0 ? sanitized : \"clip\";\n }\n\n private static string JoinAssetPath(string folder, string file)\n {\n var f = folder.Replace('\\\\', '/').TrimEnd('/');\n return f.Length == 0 ? file : f + \"/\" + file;\n }\n\n // ================================================================ target context\n\n /// <summary>Everything derived once per batch from the target rig: character up axis,\n /// foot chains, hips/root indices, IK-bone presence.</summary>\n internal sealed class TargetContext\n {\n public TargetRig Rig { get; }\n\n /// <summary>Target character up direction (midShoulders \u2212 midHips on the rest pose,\n /// via <see cref=\"CharacterFrame\"/>); null when the rig lacks the bones for it.</summary>\n public Vector3? Up { get; }\n\n /// <summary>Target character frame (for the arm-IK basis change); null when not computable.</summary>\n public CharacterFrame? Frame { get; }\n\n /// <summary>Left/right leg chains from the rig's UpperLeg/LowerLeg/Foot/Toe roles;\n /// null when either leg is incompletely mapped.</summary>\n public (FootChain Left, FootChain Right)? FootChains { get; }\n\n /// <summary>Why <see cref=\"Up\"/> / <see cref=\"FootChains\"/> are unavailable (report note text).</summary>\n public string UpOrChainProblem { get; } = \"\";\n\n /// <summary>Target bone carrying the Hips role.</summary>\n public int? HipsIndex { get; }\n\n /// <summary>Dedicated translating root: a parentless Animated bone that is an\n /// ancestor of (and distinct from) the hips. Null on the s&box rig (pelvis is\n /// itself parentless; root_IK is IkBaked).</summary>\n public int? DedicatedRootIndex { get; }\n\n /// <summary>True when the hips bone's direct parent is the dedicated root.</summary>\n public bool HipsParentIsRoot { get; }\n\n public bool HasIkBakedBones { get; }\n\n /// <summary>ConstraintDriven bone indices (twist/helper) \u2014 excluded from DMX\n /// channels per design \u00a73; null when the rig has none. Face bones\n /// (<see cref=\"SboxBoneClassifier.IsFaceBone\"/>) are NOT in this set even though\n /// their class is ConstraintDriven: they keep rest-local channels, because no\n /// constraint re-drives them in a compiled sequence (channel-less face joints\n /// bake statically \u2014 eyes detach from the moving head in ModelDoc).</summary>\n public IReadOnlySet<int>? ConstraintDrivenBones { get; }\n\n public TargetContext(TargetRig rig)\n {\n Rig = rig;\n HipsIndex = rig.BoneForRole(BoneRole.Hips);\n DedicatedRootIndex = FindDedicatedRoot(rig, HipsIndex);\n HipsParentIsRoot = HipsIndex is { } hips && DedicatedRootIndex is { } root\n && rig.Skeleton[hips].ParentIndex == root;\n foreach (var _ in rig.BonesOfClass(BoneClass.IkBaked))\n {\n HasIkBakedBones = true;\n break;\n }\n\n var constraintDriven = new HashSet<int>(rig.BonesOfClass(BoneClass.ConstraintDriven));\n // Face bones (eye_/ear_/face_) are ConstraintDriven by class but, unlike the\n // twist/helper bones, nothing re-drives them when a compiled sequence plays:\n // the model's AnimConstraintList never references them and the engine's eye\n // look-at / blinking only runs in game. They must KEEP their rest-local DMX\n // channels (the shipped fbx2dmx clips carry them too) or ModelDoc bakes the\n // channel-less joints statically and the eyes detach from the moving head\n // (see SboxBoneClassifier.IsFaceBone).\n constraintDriven.RemoveWhere(i => SboxBoneClassifier.IsFaceBone(rig.Skeleton[i].Name));\n ConstraintDrivenBones = constraintDriven.Count > 0 ? constraintDriven : null;\n\n try\n {\n Frame = CharacterFrame.Compute(rig.Skeleton, rig.ToMappingResult(), rig.Skeleton.RestWorld);\n Up = Frame.Up;\n }\n catch (ArgumentException e)\n {\n UpOrChainProblem = $\"target character frame not computable ({e.Message})\";\n return;\n }\n\n var left = LegChain(rig, BoneRole.UpperLegL, BoneRole.LowerLegL, BoneRole.FootL, BoneRole.ToeL);\n var right = LegChain(rig, BoneRole.UpperLegR, BoneRole.LowerLegR, BoneRole.FootR, BoneRole.ToeR);\n if (left is not null && right is not null)\n FootChains = (left, right);\n else\n UpOrChainProblem = \"target rig does not map a complete UpperLeg/LowerLeg/Foot chain on both sides\";\n }\n\n private static FootChain? LegChain(TargetRig rig, BoneRole upper, BoneRole lower, BoneRole foot, BoneRole toe)\n => rig.BoneForRole(upper) is { } hip\n && rig.BoneForRole(lower) is { } knee\n && rig.BoneForRole(foot) is { } ankle\n ? new FootChain { Hip = hip, Knee = knee, Ankle = ankle, Toe = rig.BoneForRole(toe) }\n : null;\n\n private static int? FindDedicatedRoot(TargetRig rig, int? hipsIndex)\n {\n if (hipsIndex is not { } hips)\n return null;\n // Walk up from the hips; the topmost ancestor qualifies when it is a parentless\n // Animated bone distinct from the hips itself.\n var top = hips;\n while (rig.Skeleton[top].ParentIndex >= 0)\n top = rig.Skeleton[top].ParentIndex;\n return top != hips && rig.ClassOf(top) == BoneClass.Animated ? top : null;\n }\n }\n\n // ================================================================ arm effector IK\n\n /// <summary>\n /// Optional wrist-reach cleanup: per frame, the source hand offset from its shoulder is\n /// limb-length-normalized, re-expressed in the target's character basis, and used as a\n /// two-bone IK goal for the target arm (<see cref=\"EffectorIk.ApplyGoals\"/>).\n /// </summary>\n private static class ArmIkCleanup\n {\n /// <summary>Runs both arms; returns a problem note (and does nothing) when the\n /// prerequisites are missing, null on success.</summary>\n public static string? Apply(\n List<XForm[]> frames, SourceScene scene, MappingResult map,\n TargetRig rig, TargetContext context, int take)\n {\n if (context.Frame is not { } targetFrame)\n return context.UpOrChainProblem;\n\n CharacterFrame sourceFrame;\n try\n {\n sourceFrame = CharacterFrame.Compute(scene.Skeleton, map, scene.Skeleton.RestWorld);\n }\n catch (ArgumentException e)\n {\n return $\"source character frame not computable ({e.Message})\";\n }\n\n // Change of basis source space \u2192 target space (this library's convention:\n // a * b applies b first).\n var sourceToTarget = MathQ.Normalize(\n BasisRotation(targetFrame) * Quaternion.Conjugate(BasisRotation(sourceFrame)));\n\n var appliedAny = false;\n appliedAny |= ApplyArm(frames, scene, map, rig, sourceToTarget, take,\n BoneRole.UpperArmL, BoneRole.LowerArmL, BoneRole.HandL);\n appliedAny |= ApplyArm(frames, scene, map, rig, sourceToTarget, take,\n BoneRole.UpperArmR, BoneRole.LowerArmR, BoneRole.HandR);\n return appliedAny ? null : \"no complete UpperArm/LowerArm/Hand chain mapped on source and target\";\n }\n\n private static bool ApplyArm(\n List<XForm[]> frames, SourceScene scene, MappingResult map, TargetRig rig,\n Quaternion sourceToTarget, int take, BoneRole upperRole, BoneRole lowerRole, BoneRole handRole)\n {\n if (!map.RoleToBone.TryGetValue(upperRole, out var srcUpper)\n || !map.RoleToBone.TryGetValue(lowerRole, out var srcLower)\n || !map.RoleToBone.TryGetValue(handRole, out var srcHand))\n return false;\n if (rig.BoneForRole(upperRole) is not { } tgtUpper\n || rig.BoneForRole(lowerRole) is not { } tgtLower\n || rig.BoneForRole(handRole) is not { } tgtHand)\n return false;\n\n var src = scene.Skeleton;\n var tgt = rig.Skeleton;\n var srcLen = Vector3.Distance(src.RestWorld[srcUpper].Pos, src.RestWorld[srcLower].Pos)\n + Vector3.Distance(src.RestWorld[srcLower].Pos, src.RestWorld[srcHand].Pos);\n var tgtLen = Vector3.Distance(tgt.RestWorld[tgtUpper].Pos, tgt.RestWorld[tgtLower].Pos)\n + Vector3.Distance(tgt.RestWorld[tgtLower].Pos, tgt.RestWorld[tgtHand].Pos);\n if (srcLen < 1e-4f || tgtLen < 1e-4f)\n return false;\n var scale = tgtLen / srcLen;\n\n var sourceFrames = scene.Clips[take].Frames;\n var count = Math.Min(frames.Count, sourceFrames.Count);\n var goals = new Vector3[frames.Count];\n for (var f = 0; f < frames.Count; f++)\n {\n var srcWorld = new Pose(sourceFrames[Math.Min(f, count - 1)]).ToWorld(src);\n var tgtWorld = new Pose(frames[f]).ToWorld(tgt);\n var reach = srcWorld[srcHand].Pos - srcWorld[srcUpper].Pos;\n goals[f] = tgtWorld[tgtUpper].Pos + Vector3.Transform(reach, sourceToTarget) * scale;\n }\n\n EffectorIk.ApplyGoals(\n frames, tgt,\n new LimbChain { Upper = tgtUpper, Lower = tgtLower, End = tgtHand },\n goals, RestBendAxis(tgt, tgtUpper, tgtLower, tgtHand));\n return true;\n }\n\n /// <summary>Hinge-axis fallback from the rest bend plane (elbow), like the foot pass.</summary>\n internal static Vector3 RestBendAxis(SkeletonModel skeleton, int upper, int lower, int end)\n {\n var a = skeleton.RestWorld[upper].Pos;\n var b = skeleton.RestWorld[lower].Pos;\n var c = skeleton.RestWorld[end].Pos;\n var axis = Vector3.Cross(c - a, b - a);\n if (axis.LengthSquared() > 1e-6f)\n return Vector3.Normalize(axis);\n var limb = c - a;\n axis = Vector3.Cross(limb, Vector3.UnitY);\n if (axis.LengthSquared() < 1e-6f)\n axis = Vector3.Cross(limb, Vector3.UnitZ);\n return axis.LengthSquared() > 1e-12f ? Vector3.Normalize(axis) : Vector3.UnitX;\n }\n\n /// <summary>Rotation taking canonical character axes (X=lateral, Y=up, Z=forward)\n /// to the rig's world axes.</summary>\n private static Quaternion BasisRotation(CharacterFrame frame)\n {\n var m = new Matrix4x4(\n frame.Lateral.X, frame.Lateral.Y, frame.Lateral.Z, 0f,\n frame.Up.X, frame.Up.Y, frame.Up.Z, 0f,\n frame.Forward.X, frame.Forward.Y, frame.Forward.Z, 0f,\n 0f, 0f, 0f, 1f);\n return MathQ.Normalize(Quaternion.CreateFromRotationMatrix(m));\n }\n }\n}\n"
},
{
"Ident": "notpointless.chomnr_humanoid_mocap_x",
"Path": "HumanoidMocap/Core/Skeleton/Skeleton.cs",
"FileName": "Skeleton.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 439808,
"IsPrivate": false,
"Code": "#nullable enable annotations\n\nusing System;\nusing System.Collections.Generic;\nusing HumanoidMocapX.Core.Maths;\n\nnamespace HumanoidMocapX.Core.Skeleton;\n\n/// <summary>\n/// Immutable bone hierarchy with rest pose. Bones are stored topologically sorted\n/// (every bone's parent precedes it), which lets forward kinematics run as a single\n/// in-order pass. Supports multiple roots (e.g. the s&box rig has both\n/// <c>root_IK</c> and <c>pelvis</c> as parentless bones).\n/// </summary>\npublic sealed class Skeleton\n{\n private readonly Bone[] _bones;\n private readonly XForm[] _restWorld;\n private readonly Dictionary<string, int> _indexByName;\n\n private Skeleton(Bone[] bones, XForm[] restWorld, Dictionary<string, int> indexByName)\n {\n _bones = bones;\n _restWorld = restWorld;\n _indexByName = indexByName;\n }\n\n /// <summary>Number of bones.</summary>\n public int Count => _bones.Length;\n\n /// <summary>All bones, topologically sorted (parents before children).</summary>\n public IReadOnlyList<Bone> Bones => _bones;\n\n /// <summary>Rest (bind) world transforms, indexed like <see cref=\"Bones\"/>.</summary>\n public IReadOnlyList<XForm> RestWorld => _restWorld;\n\n /// <summary>Bone name to index lookup.</summary>\n public IReadOnlyDictionary<string, int> IndexByName => _indexByName;\n\n /// <summary>The bone at <paramref name=\"index\"/>.</summary>\n public Bone this[int index] => _bones[index];\n\n /// <summary>Returns the index of the named bone, or -1 when absent.</summary>\n public int IndexOf(string name) => _indexByName.TryGetValue(name, out var index) ? index : -1;\n\n /// <summary>\n /// Builds a skeleton from bone definitions in any order: validates names and parent links,\n /// topologically sorts (stable \u2014 input order is preserved among bones whose parents are\n /// already placed), and computes rest world transforms.\n /// </summary>\n /// <exception cref=\"ArgumentException\">\n /// Thrown on duplicate bone names, unknown parent names, or parent cycles.\n /// </exception>\n public static Skeleton Create(IReadOnlyList<BoneDefinition> definitions)\n {\n ArgumentNullException.ThrowIfNull(definitions);\n\n var count = definitions.Count;\n var originalIndexByName = new Dictionary<string, int>(count, StringComparer.Ordinal);\n for (var i = 0; i < count; i++)\n {\n var name = definitions[i].Name;\n if (string.IsNullOrEmpty(name))\n throw new ArgumentException($\"Bone at position {i} has an empty name.\", nameof(definitions));\n if (!originalIndexByName.TryAdd(name, i))\n throw new ArgumentException($\"Duplicate bone name '{name}'.\", nameof(definitions));\n }\n\n foreach (var def in definitions)\n {\n if (def.ParentName is not null && !originalIndexByName.ContainsKey(def.ParentName))\n throw new ArgumentException(\n $\"Bone '{def.Name}' references unknown parent '{def.ParentName}'.\", nameof(definitions));\n if (def.ParentName == def.Name)\n throw new ArgumentException($\"Bone '{def.Name}' is its own parent.\", nameof(definitions));\n }\n\n // Stable topological sort: repeatedly place, in input order, every bone whose parent\n // is already placed. O(n^2) worst case \u2014 fine for skeleton-sized inputs (~100 bones).\n var newIndexByOriginal = new int[count];\n Array.Fill(newIndexByOriginal, -1);\n var order = new List<int>(count);\n while (order.Count < count)\n {\n var progressed = false;\n for (var i = 0; i < count; i++)\n {\n if (newIndexByOriginal[i] >= 0)\n continue;\n var parentName = definitions[i].ParentName;\n if (parentName is not null && newIndexByOriginal[originalIndexByName[parentName]] < 0)\n continue;\n newIndexByOriginal[i] = order.Count;\n order.Add(i);\n progressed = true;\n }\n if (!progressed)\n throw new ArgumentException(\"Bone hierarchy contains a parent cycle.\", nameof(definitions));\n }\n\n var bones = new Bone[count];\n var restWorld = new XForm[count];\n var indexByName = new Dictionary<string, int>(count, StringComparer.Ordinal);\n for (var newIndex = 0; newIndex < count; newIndex++)\n {\n var def = definitions[order[newIndex]];\n var parentIndex = def.ParentName is null ? -1 : newIndexByOriginal[originalIndexByName[def.ParentName]];\n bones[newIndex] = new Bone(newIndex, def.Name, parentIndex, def.RestLocal);\n restWorld[newIndex] = parentIndex < 0\n ? def.RestLocal\n : XForm.Compose(restWorld[parentIndex], def.RestLocal);\n indexByName.Add(def.Name, newIndex);\n }\n\n return new Skeleton(bones, restWorld, indexByName);\n }\n}\n"
},
{
"Ident": "notpointless.chomnr_humanoid_mocap_x",
"Path": "HumanoidMocap/Core/Target/KvBool.cs",
"FileName": "KvBool.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 439808,
"IsPrivate": false,
"Code": "#nullable enable annotations\n\nusing System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.Text;\n\nnamespace HumanoidMocapX.Core.Target;\n\n/// <summary>A KV3 boolean.</summary>\npublic sealed class KvBool : KvValue\n{\n /// <summary>The boolean value.</summary>\n public bool Value { get; }\n\n /// <summary>Creates a boolean value.</summary>\n public KvBool(bool value) => Value = value;\n}\n"
},
{
"Ident": "notpointless.chomnr_humanoid_mocap_x",
"Path": "dev/clip_checks.cs",
"FileName": "clip_checks.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 439808,
"IsPrivate": false,
"Code": "using System.Diagnostics;\nusing System.Text;\nusing System.Text.Json;\nusing HumanoidMocapX.Core;\nusing HumanoidMocapX.Core.Formats.Fbx;\nusing HumanoidMocapX.Core.Motion;\nusing HumanoidMocapX.Core.Target;\nusing HumanoidMocapX.EditorTools;\nusing HumanoidMocapX.Inference;\nusing Sandbox;\nusing static Sandbox.Internal.GlobalGameNamespace;\n\nnamespace HumanoidMocapX.Checks;\n\n/// <summary>Runs the full model sequence on a 5\u201310-second predecoded video fixture.\n/// This developer check excludes production decoding and visual motion acceptance.</summary>\npublic static class ClipChecks\n{\n public sealed record Fixture(string RgbaPath, int Width, int Height, int Frames, string SourceVideo,\n string FlowPath, string SamPath, string MhrPath, string OmegaPath, string DetectorPath, string SmplPath, string RigDirectory,\n int CameraInterval = 10, FlowModel Model = FlowModel.Latest);\n\n /// <summary>Writes per-stage evidence, motion documents and Human/Citizen FBX\n /// exports. Requires installed released weights and a fresh writable result directory.</summary>\n public static async Task<object> Run(string fixturePath, string directory, CancellationToken cancellation)\n {\n var fixture = JsonSerializer.Deserialize<Fixture>(await File.ReadAllTextAsync(fixturePath, cancellation))\n ?? throw new ArgumentException(\"Missing clip fixture.\");\n if (fixture.Frames is < 150 or > 300) throw new ArgumentException(\"Clip validation requires 5\u201310 seconds at 30 fps.\");\n if (Directory.Exists(directory)) throw new ArgumentException(\"Use a fresh validation directory to avoid overwriting evidence.\");\n Directory.CreateDirectory(directory);\n await File.WriteAllTextAsync(Path.Combine(directory, \"fixture.json\"), JsonSerializer.Serialize(fixture), cancellation);\n var processor = Processor(fixture); using var frames = new RawFrames(fixture);\n await Task.Run(() => FlowCheckpoint.LoadDecoder(fixture.FlowPath, fixture.SmplPath, cancellation), cancellation);\n var timer = Stopwatch.StartNew(); var times = new Dictionary<string, double>();\n var person = await processor.DetectAsync(frames, cancellation); times[\"Detection\"] = timer.Elapsed.TotalSeconds;\n if (person.First != 0 || person.Count != frames.Count) throw new InvalidDataException(\"The validation fixture must show the person in every frame.\");\n var boxes = person.Boxes; await Save(\"boxes.json\", boxes); await Save(\"timings.json\", times); timer.Restart();\n var camera = await processor.CameraAsync(frames, cancellation); times[\"Camera\"] = timer.Elapsed.TotalSeconds;\n await Save(\"rotations.json\", camera.Rotations); await Save(\"intrinsics.json\", camera.Intrinsics); await Save(\"travel.json\", camera.Travel); await Save(\"timings.json\", times); timer.Restart();\n var (features, observations) = await processor.FeaturesAsync(frames, boxes, camera.Intrinsics, cancellation); times[\"Features\"] = timer.Elapsed.TotalSeconds;\n await Save(\"features.json\", features); await Save(\"observations.json\", observations); await Save(\"timings.json\", times); timer.Restart();\n var motion = await processor.GenerateAsync(features, camera.Rotations, frames.Count, cancellation); times[\"Generation\"] = timer.Elapsed.TotalSeconds;\n var exports = await Export(fixture, motion, new ObservedDetail(observations, camera.Rotations, camera.Travel), directory, cancellation); await Save(\"timings.json\", times);\n var result = new { Passed = true, PredecodedFixture = true, Frames = frames.Count, Seconds = frames.Count / 30.0, StageSeconds = times, Exports = exports };\n await Save(\"result.json\", result);\n return result;\n\n Task Save<T>(string name, T value) => File.WriteAllTextAsync(Path.Combine(directory, name), JsonSerializer.Serialize(value), cancellation);\n }\n\n /// <summary>Repeats generation/export from this check's saved features. For\n /// developer diagnosis only: the caller must preserve the same fixture and weights.</summary>\n public static async Task<object> Generate(string directory, CancellationToken cancellation)\n {\n var fixture = JsonSerializer.Deserialize<Fixture>(await File.ReadAllTextAsync(Path.Combine(directory, \"fixture.json\"), cancellation));\n var features = JsonSerializer.Deserialize<float[]>(await File.ReadAllTextAsync(Path.Combine(directory, \"features.json\"), cancellation));\n var rotations = JsonSerializer.Deserialize<float[]>(await File.ReadAllTextAsync(Path.Combine(directory, \"rotations.json\"), cancellation));\n var motion = await Processor(fixture).GenerateAsync(features, rotations, fixture.Frames, cancellation);\n var observed = Path.Combine(directory, \"observations.json\");\n var traveled = Path.Combine(directory, \"travel.json\");\n var travel = File.Exists(traveled) ? JsonSerializer.Deserialize<float>(await File.ReadAllTextAsync(traveled, cancellation)) : float.NaN;\n var detail = File.Exists(observed) ? new ObservedDetail(JsonSerializer.Deserialize<float[]>(await File.ReadAllTextAsync(observed, cancellation)), rotations, travel) : null;\n return await Export(fixture, motion, detail, directory, cancellation);\n }\n\n static FlowVideoProcessor Processor(Fixture fixture) => new(new(fixture.FlowPath, fixture.SamPath, fixture.MhrPath, fixture.OmegaPath, fixture.DetectorPath, fixture.SmplPath),\n new() { CameraInterval = fixture.CameraInterval, Model = fixture.Model }, message => Log.Info(message));\n\n static Task<object> Export(Fixture fixture, FlowMotion motion, ObservedDetail detail, string directory, CancellationToken cancellation) => Task.Run<object>(async () =>\n {\n motion.Video = fixture.SourceVideo; motion.Name = Path.GetFileNameWithoutExtension(fixture.SourceVideo); var document = motion.ToDocument(); detail?.Apply(document);\n var bytes = Encoding.UTF8.GetBytes(document.ToJson()); await File.WriteAllBytesAsync(Path.Combine(directory, \"capture.hmotion\"), bytes, cancellation);\n await File.WriteAllTextAsync(Path.Combine(directory, \"flow-motion.json\"), JsonSerializer.Serialize(motion), cancellation);\n var results = new List<object>();\n foreach (var citizen in new[] { false, true })\n {\n cancellation.ThrowIfCancellationRequested(); var name = citizen ? \"citizen\" : \"human\";\n var rig = await File.ReadAllTextAsync(Path.Combine(fixture.RigDirectory, citizen ? \"target_rig_sbox_citizen.json\" : \"target_rig_sbox.json\"), cancellation);\n var target = citizen ? RetargetTargetSpec.SboxCitizen(rig) : RetargetTargetSpec.SboxDefault(rig);\n var result = Retargeter.Convert(new RetargetRequest\n {\n SourceData = bytes,\n SourceFileName = \"capture.hmotion\",\n FootPlantCleanup = true,\n MocapCorrections = new TargetCorrectionSettings { FirstPerson = false, StabilizeFeet = true }\n }, target);\n if (!result.Success) throw new Exception(\"Clip retarget failed: \" + string.Join(\"; \", result.Errors.Concat(result.Clips.Select(c => c.Error))));\n var clip = result.Clips.Single();\n foreach (var poses in clip.SolvedFrames) foreach (var pose in poses)\n if (!float.IsFinite(pose.Pos.Length()) || !float.IsFinite(pose.Rot.Length())) throw new Exception(\"Non-finite target animation.\");\n var times = Enumerable.Range(0, clip.SolvedFrames.Count).Select(i => i / (double)clip.Fps).ToArray();\n var exported = ModelDocFbxExport.Write(target.Rig.Skeleton, motion.Name, clip.SolvedFrames, times, clip.Fps, target.UpAxis, \"WorldRelative\");\n var roundTrip = FbxImporter.Import(exported);\n if (roundTrip.Clips.Count != 1 || roundTrip.Clips[0].Frames.Count != clip.SolvedFrames.Count)\n throw new Exception(\"Target FBX lost animation frames.\");\n var path = Path.Combine(directory, name + \".fbx\"); await File.WriteAllBytesAsync(path, exported, cancellation);\n results.Add(new { Target = name, Frames = clip.SolvedFrames.Count, Bones = target.Rig.Skeleton.Count, FbxBytes = exported.Length, Path = path });\n }\n return results;\n }, cancellation);\n\n sealed class RawFrames : ICaptureFrames, IDisposable\n {\n readonly FileStream stream;\n readonly int bytes;\n public int Width { get; }\n public int Height { get; }\n public int Count { get; }\n\n internal RawFrames(Fixture fixture)\n {\n Width = fixture.Width; Height = fixture.Height; Count = fixture.Frames;\n if (Width < 1 || Height < 1 || (long)Width * Height > 33_177_600) throw new ArgumentException(\"Invalid RGBA fixture dimensions.\");\n bytes = checked(Width * Height * 4);\n stream = new(fixture.RgbaPath, FileMode.Open, FileAccess.Read, FileShare.Read, 1024 * 1024, FileOptions.Asynchronous | FileOptions.RandomAccess);\n if (stream.Length != (long)bytes * Count) { stream.Dispose(); throw new ArgumentException(\"RGBA fixture length does not match its declared frame count.\"); }\n }\n\n public async ValueTask ReadAsync(int frame, Memory<byte> rgba, CancellationToken cancellation)\n {\n if ((uint)frame >= (uint)Count || rgba.Length != bytes) throw new ArgumentException(\"Invalid fixture frame or destination size.\");\n cancellation.ThrowIfCancellationRequested(); stream.Position = (long)frame * bytes;\n await stream.ReadExactlyAsync(rgba, cancellation);\n }\n\n public void Dispose() => stream.Dispose();\n }\n}\n"
},
{
"Ident": "notpointless.chomnr_humanoid_mocap_x",
"Path": "dev/sam_frame_checks.cs",
"FileName": "sam_frame_checks.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 439808,
"IsPrivate": false,
"Code": "using System.Diagnostics;\nusing System.Text.Json;\nusing HumanoidMocapX.EditorTools;\nusing HumanoidMocapX.Inference;\nusing Sandbox;\n\nnamespace HumanoidMocapX.Checks;\n\n/// <summary>Runs released YOLOX, SAM DINOv3, body decoder and MHR on a real frame.\n/// This is an isolated body-pass diagnostic, not full video or hand refinement.</summary>\npublic static class SamFrameChecks\n{\n /// <summary>Writes crop, keypoint and feature evidence to a JSON file. Requires\n /// a visible person in an RGBA8 frame and installed, verified model files.</summary>\n public static async Task<object> Run(string samPath, string mhrPath, string detectorPath, string rgbaPath,\n int width, int height, string resultPath, CancellationToken cancellation)\n {\n var pixels = await File.ReadAllBytesAsync(rgbaPath, cancellation);\n var box = await Detect(pixels, width, height, detectorPath, cancellation);\n var shape = DinoShape.Sam(); var crop = new SamCrop(box.Left, box.Top, box.Right, box.Bottom, shape);\n var focal = (float)Math.Sqrt((double)width * width + (double)height * height);\n var intrinsics = new float[] { focal, 0, width / 2f, 0, focal, height / 2f, 0, 0, 1 }; var affine = new float[6]; crop.CopyAffine(affine);\n var rgb = new float[shape.ImageElements]; var rayEncoder = new SamRays(shape); var rays = new float[rayEncoder.Elements];\n await Task.Run(() => { crop.Prepare(pixels, width, height, rgb, cancellation); rayEncoder.Prepare(intrinsics, affine, rays, cancellation); }, cancellation);\n var context = new float[shape.Patches * shape.Channels]; var positions = new float[context.Length];\n var imageTimes = await Encode(samPath, shape, rgb, rays, context, positions, cancellation);\n var model = await Task.Run(() => MhrCheckpoint.LoadBody(mhrPath, cancellation), cancellation);\n var pose = await Task.Run(() => SamCheckpoint.LoadPoseHead(samPath, model, false, cancellation), cancellation);\n var decoderShape = new SamDecoderShape(SamInitialTokens.Count, shape.Patches);\n var camera = await Task.Run(() => new SamPredictionHead(VisionCheckpoint.LoadSamHead(samPath, false, cancellation), false, true), cancellation);\n var keypoints = await Task.Run(() => new SamKeypointFeedback(VisionCheckpoint.LoadSamFeedback(samPath, false, cancellation), decoderShape, 32, 32), cancellation);\n var projection = new SamProjection(intrinsics, affine, crop.CenterX, crop.CenterY, crop.Size, shape);\n var feedback = new SamDecoderFeedback(decoderShape, pose, camera, keypoints); feedback.Prepare(projection, context);\n var tokens = new float[decoderShape.TokenElements]; var tokenPositions = new float[tokens.Length]; var decoded = new float[tokens.Length];\n await Task.Run(() => new SamInitialTokens(VisionCheckpoint.LoadSamInitialTokens(samPath, false, cancellation))\n .Run(projection, tokens, tokenPositions, cancellation), cancellation);\n var decoderWeights = await Task.Run(() => VisionCheckpoint.LoadSamDecoder(samPath, false, cancellation), cancellation);\n await GameTask.MainThread(); var decoder = new SamGpuDecoder(decoderWeights, decoderShape); var timer = Stopwatch.StartNew();\n try\n {\n await decoder.RunAsync(tokens, context, tokenPositions, positions, decoded, feedback.Update, cancellation);\n await Task.Run(() => feedback.Finish(decoded, cancellation), cancellation);\n }\n finally { await GameTask.MainThread(); decoder.Dispose(); }\n var decodeMilliseconds = timer.Elapsed.TotalMilliseconds;\n var cropped = new float[140]; var depths = new float[70]; var translation = new float[3]; var imagePoints = new float[140];\n projection.Run(pose.Keypoints, feedback.CameraParameters, cropped, depths, translation);\n for (var i = 0; i < 70; i++)\n {\n imagePoints[i * 2] = ((cropped[i * 2] + .5f) * shape.Width - affine[2]) / affine[0];\n imagePoints[i * 2 + 1] = ((cropped[i * 2 + 1] + .5f) * shape.Height - affine[5]) / affine[4];\n }\n var result = new\n {\n BodyPassOnly = true,\n Width = width,\n Height = height,\n Box = box,\n Crop = new { crop.CenterX, crop.CenterY, crop.Size },\n ImageMilliseconds = imageTimes,\n DecodeMilliseconds = decodeMilliseconds,\n ImageKeypoints = imagePoints,\n KeypointsMetres = pose.Keypoints.ToArray(),\n Depths = depths,\n Translation = translation,\n BodyFeature = decoded.AsSpan(0, decoderShape.Width).ToArray(),\n ModelParameters = pose.Model.ToArray()\n };\n await File.WriteAllTextAsync(resultPath, JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }), cancellation);\n return new\n {\n Passed = true,\n BodyPassOnly = true,\n Box = box,\n Keypoints = 70,\n BodyFeatureChannels = decoderShape.Width,\n ImageMilliseconds = imageTimes,\n DecodeMilliseconds = decodeMilliseconds,\n ResultPath = resultPath\n };\n }\n\n static async Task<YoloBox> Detect(byte[] pixels, int width, int height, string path, CancellationToken cancellation)\n {\n var network = new YoloNetwork(); var weights = await Task.Run(() => YoloCheckpoint.Load(path, network, cancellation), cancellation);\n await GameTask.MainThread(); var detector = new YoloGpuDetector(network, weights);\n try\n {\n var boxes = new YoloBox[YoloNetwork.Predictions]; var count = await detector.DetectAsync(pixels, width, height, boxes, cancellation);\n if (count < 1) throw new Exception(\"SAM frame fixture contains no detected person.\");\n return boxes[0];\n }\n finally { await GameTask.MainThread(); detector.Dispose(); }\n }\n\n static async Task<double[]> Encode(string path, DinoShape shape, float[] rgb, float[] rays, float[] context, float[] positions, CancellationToken cancellation)\n {\n var backbone = await Task.Run(() => VisionCheckpoint.LoadSamBackbone(path, cancellation), cancellation);\n var body = await Task.Run(() => VisionCheckpoint.LoadSamImageConditioning(path, false, cancellation), cancellation);\n var hand = await Task.Run(() => VisionCheckpoint.LoadSamImageConditioning(path, true, cancellation), cancellation);\n await GameTask.MainThread(); var encoder = new SamGpuImageEncoder(backbone, body, hand, shape);\n try\n {\n var timer = Stopwatch.StartNew(); await encoder.RunAsync(rgb, rays, context, false, cancellation);\n var first = timer.Elapsed.TotalMilliseconds; var previous = context.ToArray();\n await GameTask.MainThread(); timer.Restart(); await encoder.RunAsync(rgb, rays, context, false, cancellation);\n var repeated = timer.Elapsed.TotalMilliseconds;\n if (!previous.SequenceEqual(context)) throw new Exception(\"SAM image encoding did not repeat exactly.\");\n encoder.CopyPositions(positions); return new[] { first, repeated };\n }\n finally { await GameTask.MainThread(); encoder.Dispose(); }\n }\n}\n"
},
{
"Ident": "notpointless.chomnr_humanoid_mocap_x",
"Path": "Editor/AccountBar.cs",
"FileName": "AccountBar.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 439808,
"IsPrivate": false,
"Code": "using System;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Editor;\nusing Sandbox;\n\nnamespace HumanoidMocapX.EditorTools;\n\n/// <summary>The strip across the top of the X window. With a provider: avatar, name, status pills,\n/// account ID and limits, then icon buttons for the queue, adding videos and sign-out. On a processing\n/// machine: its SSH address. On this computer: the GPU. Before setup: a pointer to settings. Missing\n/// names and pictures show as not provided; account details and setup live in Mocap adjustments \u203a Processing.</summary>\nsealed class AccountBar : Widget\n{\n readonly HostingSettings settings;\n readonly QueueTracker queue;\n readonly AccountAvatar avatar;\n readonly Label name, details, problem;\n readonly Layout pills;\n readonly Button signIn;\n readonly BadgeButton queueButton;\n readonly IconButton add, signOut;\n CancellationTokenSource pending;\n\n /// <summary>Provider mode when <paramref name=\"queue\"/> is set, otherwise the processing machine\n /// or this computer, or a pointer to settings while processing is not <paramref name=\"configured\"/>.</summary>\n internal AccountBar(Widget parent, HostingSettings settings, SshSettings machine, QueueTracker queue, bool configured, Action openSetup) : base(parent)\n {\n this.settings = settings; this.queue = queue;\n Layout = Layout.Row(); Layout.Margin = new Sandbox.UI.Margin(12, 10, 12, 10); Layout.Spacing = 10;\n avatar = Layout.Add(new AccountAvatar(this, 34));\n var text = Layout.AddColumn(1); text.Spacing = 2;\n var top = text.AddRow(); top.Spacing = 8;\n name = top.Add(Ui.Bold(new Label(\"\", this), 14));\n pills = top.AddRow(); pills.Spacing = 6;\n top.AddStretchCell();\n details = text.Add(Ui.Muted(new Label(\"\", this), small: true));\n problem = Layout.Add(Ui.Error(this));\n if (!configured)\n {\n avatar.ShowIcon(\"settings\");\n name.Text = \"Not set up\";\n details.Text = \"Choose where videos are processed: settings (top right) \u203a Processing \u203a Setup.\";\n return;\n }\n if (queue is null && machine.Enabled)\n {\n avatar.ShowIcon(\"dns\");\n name.Text = \"Processing machine\";\n details.Text = $\"{machine.Address} over SSH \u00b7 videos are copied there for processing\";\n return;\n }\n if (queue is null)\n {\n avatar.ShowIcon(\"memory\");\n name.Text = \"This computer\";\n var gpu = Sandbox.Engine.SystemInfo.Gpu is { Length: > 0 } gpuName && gpuName != \"unset\" ? gpuName : \"This GPU\";\n details.Text = $\"{gpu} \u00b7 up to {FlowVideoProcessor.LaneCapacity()} frames at once \u00b7 videos never leave this computer\";\n return;\n }\n signIn = Layout.Add(Ui.Primary(\"Sign in\", \"login\", () => _ = SignInAsync(openSetup)));\n queueButton = Layout.Add(new BadgeButton(this, \"format_list_bulleted\", OpenQueue, \"Queue: your jobs on the provider\"));\n add = Layout.Add(Ui.Icon(this, \"playlist_add\", () => QueuePopup.AddVideos(queue), \"Add videos to the queue\"));\n signOut = Layout.Add(Ui.Icon(this, \"logout\", () => _ = SignOutAsync(), \"Sign out\"));\n settings.Changed += Refresh; queue.Changed += Refresh;\n Refresh();\n }\n\n void OpenQueue()\n {\n var popup = new QueuePopup(this, queue);\n popup.OpenAt(queueButton.ScreenRect.BottomLeft + new Vector2(queueButton.Width - popup.FixedWidth, 6));\n }\n\n void Refresh()\n {\n if (!this.IsValid()) return;\n var profile = settings.SignedIn ? settings.Profile : null;\n var label = EditorCookie.Get(ProcessingSetup.ProviderLabelCookie, \"\");\n signIn.Visible = profile is null; signIn.Text = label.Length > 0 ? \"Sign in with \" + label : \"Sign in\";\n queueButton.Visible = add.Visible = signOut.Visible = profile is not null;\n queueButton.Count = queue.Active;\n pills.Clear(true);\n if (profile is null)\n {\n name.Text = \"Signed out\"; details.Text = Host(); avatar.ShowIcon(\"person\");\n return;\n }\n var user = profile.User;\n name.Text = Clip(user.Name ?? user.Username, 32) ?? \"Name not provided\";\n var (status, color) = profile switch\n {\n { Admin: true } => (\"Admin\", Ui.Accent),\n { CanProcess: true } => (\"Whitelisted\", Ui.Accent),\n { InGuild: false } => (\"Join the Discord server\", Theme.Yellow),\n _ => (\"Not whitelisted\", Theme.Red)\n };\n pills.Add(new Pill(this, status, color));\n if (profile.Limits is { Priority: true }) pills.Add(new Pill(this, \"Priority\", Theme.Yellow));\n details.Text = $\"{Clip(user.Id, 40)} \u00b7 {Host()}\" + (profile.Limits is { } limit ? \" \u00b7 \" + Describe(limit) : \"\");\n details.ToolTip = user.Id + \": your account ID on this provider. Give it to the provider to be whitelisted. Click to copy.\";\n avatar.ShowAccount(user.Avatar, user.Name ?? user.Username);\n }\n\n protected override void OnMouseClick(MouseEvent e)\n {\n base.OnMouseClick(e);\n // The account ID line copies the ID, so users can send it to the provider.\n if (e.LeftMouseButton && details.IsUnderMouse && settings.Profile?.User.Id is { } id) EditorUtility.Clipboard.Copy(id);\n }\n\n static string Describe(HostLimits limit)\n => (limit.MaximumClipSeconds is { } clip ? $\"clips up to {HostedCapture.Duration((int)clip)}\" : \"clips up to the provider's limit\")\n + $\" \u00b7 uploads up to {limit.MaximumUploadBytes / (1024 * 1024)} MiB\";\n\n string Host()\n {\n try { return settings.Endpoint().Host; } catch (ArgumentException) { return \"No provider address\"; }\n }\n\n static string Clip(string text, int length) => text is { } value && value.Length > length ? value[..(length - 1)] + \"\u2026\" : text;\n\n async Task SignInAsync(Action openSetup)\n {\n if (pending is not null) return;\n if (ProcessingSetup.SavedProvider() is not { } provider) { openSetup(); return; }\n using var cancellation = new CancellationTokenSource(); pending = cancellation; var token = cancellation.Token;\n Ui.Show(problem, null); signIn.Enabled = false; signIn.Text = \"Finish in your browser\u2026\";\n try { await ProcessingSetup.SignInAsync(settings, provider, token); }\n catch (Exception failure) when (ProcessingSetup.IsSignInFailure(failure))\n {\n await EditorPipeline.SwitchToMainThread();\n if (!token.IsCancellationRequested) Ui.Show(problem, failure is OperationCanceledException ? \"Sign-in timed out. Try again.\" : failure.Message);\n }\n finally\n {\n await EditorPipeline.SwitchToMainThread();\n pending = null;\n if (signIn.IsValid()) { signIn.Enabled = true; Refresh(); }\n }\n }\n\n async Task SignOutAsync()\n {\n using var client = new HostSignIn();\n await client.SignOutAsync(settings);\n }\n\n public override void OnDestroyed()\n {\n settings.Changed -= Refresh; pending?.Cancel();\n if (queue is not null) queue.Changed -= Refresh;\n base.OnDestroyed();\n }\n\n protected override void OnPaint()\n {\n Paint.SetPen(Ui.CardEdge, 1);\n Paint.DrawLine(new Vector2(0, Height - .5f), new Vector2(Width, Height - .5f));\n }\n}\n"
},
{
"Ident": "notpointless.chomnr_humanoid_mocap_x",
"Path": "tests/test_sam_backbone.cs",
"FileName": "test_sam_backbone.cs",
"PackageType": "library",
"CodeKind": "UnitTest",
"AssetVersionId": 439808,
"IsPrivate": false,
"Code": "using HumanoidMocapX.EditorTools;\nusing HumanoidMocapX.Inference;\n\nstatic class SamBackboneTests\n{\n internal static void Run()\n {\n Dimensions(); Encoder(); Checkpoint();\n Console.WriteLine(\"PASS: 32-layer SAM DINOv3 encoder, SwiGLU gates, per-axis positions, selective checkpoint loading and allocation-free reuse\");\n }\n\n static void Dimensions()\n {\n var shape = DinoShape.Sam();\n MotionTests.Equal(512, shape.Width); MotionTests.Equal(512, shape.Height);\n MotionTests.Equal(1280, shape.Channels); MotionTests.Equal(20, shape.Heads); MotionTests.Equal(32, shape.Depth);\n MotionTests.Equal(1024, shape.Patches); MotionTests.Equal(1029, shape.Tokens);\n MotionTests.Throws<ArgumentException>(() => DinoShape.Sam(511, 512));\n MotionTests.Throws<ArgumentException>(() => DinoShape.Sam(channels: 1281));\n MotionTests.Throws<ArgumentException>(() => DinoShape.Sam(heads: 3));\n var positions = OmegaPosition.Factors(2, 3, 5, new float[] { 1 }, true);\n // Row zero's Y coordinate is -1/2; column zero's X coordinate is -2/3.\n MotionTests.Near(-1, positions[5 * 4], 1e-6);\n MotionTests.Near(Math.Cos(-4 * Math.PI / 3), positions[5 * 4 + 2], 1e-6);\n }\n\n static void Encoder()\n {\n var shape = DinoShape.Sam(32, 48, 8, 2); var tensors = OmegaFixture.Sam(shape.Channels, shape.Heads);\n var encoder = new DinoImageEncoder(new(tensors), shape); var output = new float[shape.Patches * shape.Channels];\n var image = OmegaVisionTests.Image(shape.ImageElements, 0);\n for (var pass = 0; pass < 2; pass++)\n {\n if (pass > 0) Array.Reverse(image);\n encoder.Run(image, output);\n var expected = OmegaVisionReference.Image(tensors, image, shape.Height, shape.Width, shape.Channels, shape.Heads, true);\n for (var i = 0; i < output.Length; i++) MotionTests.Near(expected[i], output[i], 2e-4);\n }\n for (var warm = 0; warm < 3; warm++) encoder.Run(image, output);\n var before = GC.GetAllocatedBytesForCurrentThread(); encoder.Run(image, output);\n MotionTests.Equal(0L, GC.GetAllocatedBytesForCurrentThread() - before);\n using var cancelled = new CancellationTokenSource(); cancelled.Cancel();\n MotionTests.Throws<OperationCanceledException>(() => encoder.Run(image, output, cancelled.Token));\n MotionTests.Throws<ArgumentException>(() => encoder.Run(image, image.AsSpan(0, output.Length)));\n image[0] = 1.1f; MotionTests.Throws<ArgumentException>(() => encoder.Run(image, output));\n image[0] = float.NaN; MotionTests.Throws<ArgumentException>(() => encoder.Run(image, output));\n var block = new OmegaBlock(new(tensors), \"backbone.encoder.blocks.0\", 8, 2, false, true);\n var values = new float[shape.Tokens * shape.Channels];\n MotionTests.Throws<ArgumentException>(() => block.Run(values, new(shape.Tokens, 8), new(shape.Tokens, 8, 2, false), default));\n }\n\n static void Checkpoint()\n {\n var path = Path.Combine(Path.GetTempPath(), \"sam-backbone-\" + Guid.NewGuid().ToString(\"N\") + \".pt\");\n try\n {\n var tensors = OmegaFixture.Sam(8, 2);\n tensors.Add(\"decoder.unused\", new(\"decoder.unused\", new[] { float.NaN }, new[] { 1, 1, 1, 1, 1 }));\n foreach (var component in new string[] { null, \"state_dict\" })\n {\n CheckpointFixture.Write(path, tensors, component, \"\"); var loaded = VisionCheckpoint.LoadSamBackbone(path);\n foreach (var pair in tensors.Where(pair => pair.Key.StartsWith(\"backbone.encoder.\")))\n MotionTests.Check(loaded.Get(pair.Key, pair.Value.Shape).Data.AsSpan().SequenceEqual(pair.Value.Data), \"SAM backbone checkpoint changed tensor layout\");\n _ = new DinoImageEncoder(loaded, DinoShape.Sam(16, 32, 8, 2));\n using var cancelled = new CancellationTokenSource(); cancelled.Cancel();\n MotionTests.Throws<OperationCanceledException>(() => VisionCheckpoint.LoadSamBackbone(path, cancelled.Token));\n File.Delete(path);\n }\n tensors[\"backbone.encoder.blocks.31.mlp.w3.bias\"].Data[0] = float.NaN;\n CheckpointFixture.Write(path, tensors, \"state_dict\", \"\");\n MotionTests.Throws<InvalidDataException>(() => VisionCheckpoint.LoadSamBackbone(path));\n }\n finally { File.Delete(path); }\n }\n}\n"
},
{
"Ident": "notpointless.chomnr_humanoid_mocap_x",
"Path": "tests/test_capture_inputs.cs",
"FileName": "test_capture_inputs.cs",
"PackageType": "library",
"CodeKind": "UnitTest",
"AssetVersionId": 439808,
"IsPrivate": false,
"Code": "using HumanoidMocapX.Inference;\n\nstatic class CaptureInputTests\n{\n internal static void Run()\n {\n var person = new YoloBox(10, 20, 110, 220, .8f); var other = new YoloBox(250, 10, 290, 100, .95f);\n var track = new PersonTrack(5); track.Add([other, person]); track.Add([person with { Left = 12, Right = 112 }, other]);\n track.Add([]); track.Add([person with { Left = 16, Right = 116 }]); track.Add([person with { Left = 18, Right = 118 }]);\n var span = track.Complete(); var boxes = span.Boxes;\n MotionTests.Equal(0, span.First); MotionTests.Equal(5, span.Count); MotionTests.Check(span.Note(2, 5) is null, \"A fully visible person needs no note\");\n for (var i = 0; i < 5; i++) { MotionTests.Equal(10f + i * 2, boxes[i * 4]); MotionTests.Equal(110f + i * 2, boxes[i * 4 + 2]); }\n MotionTests.Throws<InvalidOperationException>(() => track.Add([person]));\n var nobody = new PersonTrack(2); nobody.Add([]); nobody.Add([]); MotionTests.Throws<InvalidOperationException>(() => nobody.Complete());\n var unfinished = new PersonTrack(2); unfinished.Add([person]); MotionTests.Throws<InvalidOperationException>(() => unfinished.Complete());\n // The capture starts where the person first appears and ends at their last detection.\n var late = new PersonTrack(6); late.Add([]); late.Add([]); late.Add([person]); late.Add([]); late.Add([person with { Left = 14, Right = 114 }]); late.Add([]);\n var visible = late.Complete();\n MotionTests.Equal(2, visible.First); MotionTests.Equal(3, visible.Count); MotionTests.Equal(12, visible.Boxes.Length);\n MotionTests.Equal(12f, visible.Boxes[4]); MotionTests.Equal(14f, visible.Boxes[8]); MotionTests.Near(2 / 30d, visible.StartSeconds);\n MotionTests.Equal(\"The person is in view from 1.07 s to 1.17 s of the video, so only that part was captured.\", visible.Note(1, 6));\n var uncertain = new PersonTrack(2); uncertain.Add([person]);\n MotionTests.Throws<InvalidOperationException>(() => uncertain.Add([person, person with { Left = 11, Right = 111 }]));\n // A person lost for more than half a second ends the capture; a later return is not joined on.\n var lost = new PersonTrack(18); lost.Add([person]); for (var i = 0; i < 15; i++) lost.Add([]);\n lost.Add([other]); lost.Add([person]);\n var kept = lost.Complete(); MotionTests.Equal(0, kept.First); MotionTests.Equal(1, kept.Count);\n MotionTests.Throws<ArgumentException>(() => new PersonTrack(1).Add([person with { Left = float.NaN }]));\n var reuse = new PersonTrack(20); var input = new[] { person }; for (var i = 0; i < 4; i++) reuse.Add(input);\n var before = GC.GetAllocatedBytesForCurrentThread(); reuse.Add(input); MotionTests.Equal(0L, GC.GetAllocatedBytesForCurrentThread() - before);\n var noise = new float[491 * 501]; var repeated = new float[noise.Length]; FlowNoise.Fill(noise, 0); FlowNoise.Fill(repeated, 0);\n MotionTests.Check(noise.SequenceEqual(repeated), \"Initial noise must repeat for the same seed\");\n var mean = noise.Average(v => (double)v); var variance = noise.Average(v => (double)v * v) - mean * mean;\n MotionTests.Near(0, mean, .01); MotionTests.Near(1, variance, .02);\n MotionTests.Check(noise.Min() < -3 && noise.Max() > 3 && noise.All(float.IsFinite), \"Normal noise must cover both tails and remain finite\");\n FlowNoise.Fill(repeated, 1); MotionTests.Check(!noise.SequenceEqual(repeated), \"Different seeds must vary initial noise\");\n using var stop = new CancellationTokenSource(); stop.Cancel();\n MotionTests.Throws<OperationCanceledException>(() => FlowNoise.Fill(noise, 0, stop.Token));\n MotionTests.Throws<ArgumentException>(() => FlowNoise.Fill(new float[490], 0));\n Console.WriteLine(\"PASS: person identity continuity, short-gap interpolation, ambiguous/lost-person rejection, allocation-free tracking and seeded standard-normal latents\");\n }\n}\n"
},
{
"Ident": "notpointless.chomnr_humanoid_mocap_x",
"Path": "tests/test_ccd_ik.cs",
"FileName": "test_ccd_ik.cs",
"PackageType": "library",
"CodeKind": "UnitTest",
"AssetVersionId": 439808,
"IsPrivate": false,
"Code": "using System.Numerics;\nusing HumanoidMocapX.Core.Cleanup;\nusing HumanoidMocapX.Core.Maths;\nusing HumanoidMocapX.Core.Skeleton;\n\nstatic class CcdIkTests\n{\n internal static void Run()\n {\n var skeleton = Skeleton.Create(new[]\n {\n new BoneDefinition(\"root\", null, XForm.Identity),\n new BoneDefinition(\"elbow\", \"root\", new XForm(Vector3.UnitX, Quaternion.Identity)),\n new BoneDefinition(\"hand\", \"elbow\", new XForm(Vector3.UnitY, Quaternion.Identity)),\n new BoneDefinition(\"finger\", \"hand\", new XForm(Vector3.UnitY * .2f, Quaternion.CreateFromAxisAngle(Vector3.UnitX, .4f)))\n });\n var rest = skeleton.Bones.Select(b => b.RestLocal).ToArray();\n var solver = new CcdIk(skeleton, new[] { 0, 1 }, 2);\n var pose = (XForm[])rest.Clone();\n var residual = solver.Solve(pose, new Vector3(.7f, 1.3f, .4f), iterations: 128, tolerance: 1e-5f);\n MotionTests.Check(residual < 1e-4, $\"CCD must reach a reachable target; residual {residual}\");\n Invariants(rest, pose);\n pose = (XForm[])rest.Clone();\n residual = solver.Solve(pose, new Vector3(20, 0, 0), iterations: 128);\n MotionTests.Near(18, residual, .001);\n Invariants(rest, pose);\n\n var locked = (XForm[])rest.Clone();\n var limits = new LockedJoint(rest);\n solver.Solve(locked, new Vector3(-1, 1, 0), constrain: limits.Apply);\n MotionTests.Near(0, MathQ.AngleBetween(rest[0].Rot, locked[0].Rot), 1e-6);\n MotionTests.Check(limits.Calls > 0, \"CCD must enforce the supplied joint constraint\");\n\n var offsetSolver = new CcdIk(skeleton, new[] { 0 }, 2);\n var offset = new Vector3(.1f, .3f, 0);\n var offsetGoal = Vector3.Transform(new Vector3(1, 1, 0) + offset,\n Quaternion.CreateFromAxisAngle(Vector3.UnitZ, .4f));\n pose = (XForm[])rest.Clone();\n MotionTests.Check(offsetSolver.Solve(pose, offsetGoal, tipOffset: offset) < .001f, \"CCD tip offset must reach the target\");\n\n Reject(() => new CcdIk(skeleton, new[] { 1, 0 }, 2));\n Reject(() => new CcdIk(skeleton, new[] { -1 }, 2));\n Reject(() => solver.Solve(rest, new Vector3(float.NaN, 0, 0)));\n Reject(() => solver.Solve(rest, Vector3.Zero, iterations: 0));\n Reject(() => solver.Solve(rest, Vector3.Zero, gain: float.NaN));\n var invalid = (XForm[])rest.Clone(); invalid[1].Rot = default;\n Reject(() => solver.Solve(invalid, Vector3.Zero));\n\n pose = (XForm[])rest.Clone();\n for (var i = 0; i < 10; i++) solver.Solve(pose, new Vector3(.7f, 1.3f, .4f));\n var before = GC.GetAllocatedBytesForCurrentThread();\n for (var i = 0; i < 100; i++) solver.Solve(pose, new Vector3(.7f, 1.3f, .4f));\n MotionTests.Check(GC.GetAllocatedBytesForCurrentThread() == before, \"CCD solve must not allocate\");\n Console.WriteLine(\"PASS: DigitalRune CCD adaptation, reach residuals, constraints, offsets, validation and zero-allocation solve\");\n }\n\n static void Invariants(XForm[] before, XForm[] after)\n {\n for (var i = 0; i < before.Length; i++) MotionTests.Near(0, Vector3.Distance(before[i].Pos, after[i].Pos));\n for (var i = 2; i < before.Length; i++) MotionTests.Near(0, MathQ.AngleBetween(before[i].Rot, after[i].Rot));\n }\n\n static void Reject(Action action)\n {\n try { action(); } catch (ArgumentException) { return; }\n throw new Exception(\"CCD accepted an invalid chain or input\");\n }\n\n sealed class LockedJoint(XForm[] reference)\n {\n internal int Calls;\n internal void Apply(XForm[] pose, int bone) { Calls++; if (bone == 0) pose[bone].Rot = reference[bone].Rot; }\n }\n}\n"
},
{
"Ident": "notpointless.chomnr_humanoid_mocap_x",
"Path": "Editor/Server/EditorHost.cs",
"FileName": "EditorHost.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 439808,
"IsPrivate": false,
"Code": "using System;\nusing System.IO;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Editor;\nusing HumanoidMocapX.EditorTools.Video;\nusing HumanoidMocapX;\nusing HumanoidMocapX.EditorTools;\nusing Sandbox;\n\nnamespace HumanoidMocapHost;\n\n/// <summary>Opt-in processing host inside the s&box editor. Operator configuration\n/// comes from MOCAP_* environment variables; see docs/editor-host.md.</summary>\npublic static class EditorHost\n{\n // The engine exposes static console/event entry points; session state stays here.\n static readonly SemaphoreSlim Changes = new(1, 1);\n static EditorHostLifetime active;\n static CancellationTokenSource starting;\n\n /// <summary>Verifies installed Base/Latest weights, then listens on loopback. Requires\n /// Windows Media Foundation and a GPU budget. Startup failures propagate to the console.</summary>\n [ConCmd(\"mocap_x_host_start\", ConVarFlags.Protected, Help = \"Start the configured in-editor Humanoid Mocap X processing host\")]\n public static async Task StartAsync()\n {\n await Changes.WaitAsync();\n using var cancellation = new CancellationTokenSource();\n starting = cancellation;\n try\n {\n if (active is { IsRunning: true }) { Log.Info(\"Humanoid Mocap X host is already running.\"); return; }\n if (active is not null) { await active.StopAsync(); active = null; }\n if (LocalCapture.Availability is { } reason) throw new PlatformNotSupportedException(reason);\n var config = HostConfiguration.Read(Environment.GetEnvironmentVariable,\n Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), \"HumanoidMocapX\"));\n await ModelStore.InstalledAsync(config.Models, FlowModel.Base, message => Log.Info(message), cancellation.Token);\n await ModelStore.InstalledAsync(config.Models, FlowModel.Latest, message => Log.Info(message), cancellation.Token);\n await GameTask.MainThread(); cancellation.Token.ThrowIfCancellationRequested();\n if (!Graphics.IsAvailable || Graphics.VideoMemoryBudget == 0 && Sandbox.Engine.SystemInfo.GpuMemory == 0)\n throw new InvalidOperationException(\"The host needs an initialized GPU with a reported video-memory budget.\");\n active = Start(config);\n Log.Info($\"Humanoid Mocap X host listening on http://127.0.0.1:{config.Port}/. Stop with mocap_x_host_stop.\");\n }\n finally { starting = null; Changes.Release(); }\n }\n\n static EditorHostLifetime Start(HostConfiguration config)\n {\n var lanes = config.Lanes == CaptureLanes.Auto ? CaptureLanes.Maximum : config.Lanes;\n var admission = new GpuAdmission(lanes);\n admission.Update(Graphics.VideoMemoryBudget, Graphics.VideoMemoryUsed, Sandbox.Engine.SystemInfo.GpuMemory);\n var storage = new FileJobStorage(config.Data);\n try\n {\n var processor = new CaptureProcessor(storage.CaptureDirectory, config.UploadBytes,\n (selected, workspace, progress) => new LocalCapture(new CaptureOptions\n { Model = selected.Model, CameraInterval = selected.CameraInterval, Lanes = lanes }, config.Models, workspace, progress),\n ReadDurationAsync);\n var queue = new CaptureQueue(storage, processor, TimeProvider.System, config.ActivePerOwner,\n maximumUploadBytes: config.UploadBytes, maximumClipSeconds: config.ClipSeconds,\n concurrentJobs: config.ConcurrentJobs, canStartJob: admission.CanStart);\n var session = new HostSession($\"http://127.0.0.1:{config.Port}/\", config.Keys, storage, queue, ReportError);\n return new EditorHostLifetime(session, admission);\n }\n catch { storage.Dispose(); throw; }\n }\n\n static Task<double> ReadDurationAsync(string video, CancellationToken token) => Task.Run(() =>\n {\n token.ThrowIfCancellationRequested();\n using var decoder = IVideoDecoder.Open(video);\n return decoder.Duration;\n }, token);\n\n /// <summary>Cancels startup, uploads and active captures, then waits for GPU cleanup.\n /// Repeated calls are safe; completed job IDs are invalidated by the next start.</summary>\n [ConCmd(\"mocap_x_host_stop\", ConVarFlags.Protected, Help = \"Stop and drain the in-editor Humanoid Mocap X processing host\")]\n public static async Task StopAsync()\n {\n try { starting?.Cancel(); } catch (ObjectDisposedException) { } // Startup just finished.\n await Changes.WaitAsync();\n try\n {\n await GameTask.MainThread();\n if (active is not null) { await active.StopAsync(); active = null; }\n }\n finally { Changes.Release(); }\n }\n\n [EditorEvent.Hotload]\n static void Hotloaded() => _ = StopAfterHotloadAsync();\n\n static async Task StopAfterHotloadAsync()\n {\n try { await StopAsync(); }\n catch (Exception error) { ReportError(error); }\n }\n\n static void ReportError(Exception error) => Log.Error($\"Humanoid Mocap X host: {error}\");\n}\n"
},
{
"Ident": "notpointless.chomnr_humanoid_mocap_x",
"Path": "Editor/Server/HostSession.cs",
"FileName": "HostSession.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 439808,
"IsPrivate": false,
"Code": "#nullable enable annotations\n\nusing System;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace HumanoidMocapHost;\n\n/// <summary>Owns one HTTP listener, queue loop and file-store lifetime. Shutdown drains\n/// uploads and GPU captures before disposing their resources.</summary>\ninternal sealed class HostSession : IAsyncDisposable\n{\n readonly object gate = new();\n readonly CancellationTokenSource stopping = new();\n readonly FileJobStorage storage;\n readonly CaptureQueue queue;\n readonly HttpHost http;\n readonly Task worker;\n readonly Action<Exception> reportError;\n Task? shutdown;\n internal bool IsStopping { get { lock (gate) return shutdown is not null; } }\n\n internal HostSession(string prefix, ApiKeys keys, FileJobStorage storage, CaptureQueue queue, Action<Exception> reportError)\n {\n this.storage = storage; this.queue = queue; this.reportError = reportError;\n try { http = new HttpHost(prefix, keys, queue, reportError); }\n catch { stopping.Dispose(); queue.Dispose(); storage.Dispose(); throw; }\n worker = Task.Run(() => QueueLoop.RunAsync(queue, (id, error) => reportError(new InvalidOperationException($\"Capture {id} failed.\", error)), stopping.Token));\n _ = ObserveAsync();\n }\n\n async Task ObserveAsync()\n {\n try { await worker; }\n catch (Exception error) { reportError(error); }\n finally\n {\n try { await StopAsync(); }\n catch (Exception error) { reportError(error); }\n }\n }\n\n /// <summary>Idempotently stops this instance. The returned task completes only after\n /// cancellation and resource cleanup; callers must await it before starting a replacement.</summary>\n internal Task StopAsync()\n {\n lock (gate) return shutdown ??= StopCoreAsync();\n }\n\n async Task StopCoreAsync()\n {\n await stopping.CancelAsync();\n try { await http.DisposeAsync(); }\n finally\n {\n try { await worker; }\n finally\n {\n try { queue.Dispose(); }\n finally\n {\n try { storage.DeleteRun(); }\n finally { storage.Dispose(); stopping.Dispose(); }\n }\n }\n }\n }\n\n public ValueTask DisposeAsync() => new(StopAsync());\n}\n"
}
]
}