Editor helper that locates the Humanoid Retargeter assembly by reflection and exposes a small, stable surface for creating skeletons, mappings and a runtime pose retargeter. It constructs types and invokes methods via reflection, wraps errors, and provides a PoseRetargeter that copies between engine Vector3/Quaternion arrays and the library's XForm instances.
#nullable enable annotations
using System;
using System.Collections.Generic;
using System.Linq;
using System.Collections;
using System.Numerics;
using System.Reflection;
using HumanoidMocap.Core.Mapping;
namespace HumanoidMocap.EditorTools.MotionBricks;
using Vector3 = System.Numerics.Vector3;
/// <summary>
/// The installed Humanoid Retargeter library, reached by reflection so Humanoid Mocap compiles and
/// explains itself when the library is missing or too old (the same pattern the Weapon Importer
/// uses). Only a small, stable surface is used: <c>Skeleton.Create</c>,
/// <c>Retargeter.ResolveMapping</c>, <c>TargetRig.FromSkeleton</c> and
/// <c>RuntimePoseRetargeter</c>. Assemblies are scanned newest first so hot-reloaded copies win.
/// </summary>
public sealed class RetargeterLibrary
{
public const string InstallUrl = "https://github.com/zeljkovranjes/humanoid-retargeter";
private readonly Type _xform;
private readonly Type _boneDefinition;
private readonly Type _skeleton;
private readonly Type _retargeter;
private readonly Type _mappingResult;
private readonly Type _mappingSource;
private readonly Type _boneRole;
private readonly Type _targetRig;
private readonly Type _runtime;
private readonly FieldInfo _pos;
private readonly FieldInfo _rot;
public Assembly Assembly { get; }
public string Version => Assembly.GetName().Version?.ToString() ?? "unknown";
private RetargeterLibrary( Assembly assembly, Type xform, Type boneDefinition, Type skeleton, Type retargeter, Type mappingResult,
Type mappingSource, Type boneRole, Type targetRig, Type runtime )
{
Assembly = assembly;
_xform = xform;
_boneDefinition = boneDefinition;
_skeleton = skeleton;
_retargeter = retargeter;
_mappingResult = mappingResult;
_mappingSource = mappingSource;
_boneRole = boneRole;
_targetRig = targetRig;
_runtime = runtime;
_pos = xform.GetField( "Pos" ) ?? throw new MissingFieldException( "XForm.Pos" );
_rot = xform.GetField( "Rot" ) ?? throw new MissingFieldException( "XForm.Rot" );
}
/// <summary>
/// Finds the library among the loaded assemblies. Returns null with a user-facing
/// <paramref name="problem"/> when it is not installed or lacks the runtime retargeting API.
/// </summary>
public static RetargeterLibrary? Find( out string problem )
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
var sawRetargeter = false;
for ( var i = assemblies.Length - 1; i >= 0; i-- )
{
var assembly = assemblies[i];
if ( assembly.IsDynamic )
continue;
Type? Get( string name )
{
try
{
return assembly.GetType( name, throwOnError: false );
}
catch ( Exception )
{
return null;
}
}
var retargeter = Get( "HumanoidRetargeter.Core.Retargeter" );
if ( retargeter is null )
continue;
sawRetargeter = true;
var runtime = Get( "HumanoidRetargeter.Core.Solve.RuntimePoseRetargeter" );
var xform = Get( "HumanoidRetargeter.Core.Maths.XForm" );
var definition = Get( "HumanoidRetargeter.Core.Skeleton.BoneDefinition" );
var skeleton = Get( "HumanoidRetargeter.Core.Skeleton.Skeleton" );
var mapping = Get( "HumanoidRetargeter.Core.Mapping.MappingResult" );
var source = Get( "HumanoidRetargeter.Core.Mapping.MappingSource" );
var role = Get( "HumanoidRetargeter.Core.Mapping.BoneRole" );
var rig = Get( "HumanoidRetargeter.Core.Target.TargetRig" );
if ( runtime is null || xform is null || definition is null || skeleton is null || mapping is null || source is null || role is null || rig is null )
continue;
try
{
problem = "";
return new RetargeterLibrary( assembly, xform, definition, skeleton, retargeter, mapping, source, role, rig, runtime );
}
catch ( Exception e ) when ( e is MissingMemberException )
{
continue;
}
}
problem = sawRetargeter
? "The installed Humanoid Retargeter is too old for MotionBricks. Update it to the latest version."
: "MotionBricks needs the Humanoid Retargeter library. Add it to your project's libraries: " + InstallUrl;
return null;
}
// ------------------------------------------------------------------ values
internal object XForm( Vector3 position, Quaternion rotation ) => Activator.CreateInstance( _xform, position, rotation )!;
internal (Vector3 Position, Quaternion Rotation) Read( object xform ) => ((Vector3)_pos.GetValue( xform )!, (Quaternion)_rot.GetValue( xform )!);
internal Array NewXForms( int count ) => Array.CreateInstance( _xform, count );
internal void Write( Array array, int index, Vector3 position, Quaternion rotation ) => array.SetValue( XForm( position, rotation ), index );
// ------------------------------------------------------------------ skeletons and mappings
public RetargetSkeleton CreateSkeleton( IReadOnlyList<RetargetBone> bones )
{
var definitions = Array.CreateInstance( _boneDefinition, bones.Count );
for ( var i = 0; i < bones.Count; i++ )
{
var b = bones[i];
definitions.SetValue( Activator.CreateInstance( _boneDefinition, b.Name, b.Parent, XForm( b.Position, b.Rotation ) ), i );
}
var create = _skeleton.GetMethod( "Create", BindingFlags.Public | BindingFlags.Static ) ?? throw new MissingMethodException( "Skeleton.Create" );
var handle = Unwrap( () => create.Invoke( null, new object[] { definitions } ) )!;
var count = (int)_skeleton.GetProperty( "Count" )!.GetValue( handle )!;
if ( count != bones.Count )
throw new InvalidOperationException( "The retargeter reordered the skeleton unexpectedly." );
// Bones must keep the caller's order: Skeleton.Create only reorders when a child comes first.
var names = (IEnumerable)_skeleton.GetProperty( "Bones" )!.GetValue( handle )!;
var index = 0;
foreach ( var bone in names )
{
var name = (string)bone.GetType().GetProperty( "Name" )!.GetValue( bone )!;
if ( name != bones[index++].Name )
throw new InvalidOperationException( "Bones must be listed parents first." );
}
return new RetargetSkeleton( handle, count );
}
/// <summary>Runs the retargeter's full detection cascade (shipped presets, then automatic mapping).</summary>
public RetargetMapping ResolveMapping( RetargetSkeleton skeleton )
{
var resolve = _retargeter.GetMethods( BindingFlags.Public | BindingFlags.Static ).First( m => m.Name == "ResolveMapping" && m.GetParameters().Length >= 1 );
var parameters = resolve.GetParameters();
var args = new object?[parameters.Length];
args[0] = skeleton.Handle;
for ( var i = 1; i < args.Length; i++ )
args[i] = parameters[i].HasDefaultValue ? parameters[i].DefaultValue : null;
var tuple = Unwrap( () => resolve.Invoke( null, args ) )!;
var map = tuple.GetType().GetField( "Item1" )!.GetValue( tuple )!;
var report = tuple.GetType().GetField( "Item2" )!.GetValue( tuple );
var roles = new Dictionary<BoneRole, int>();
foreach ( DictionaryEntry entry in (IDictionary)_mappingResult.GetProperty( "RoleToBone" )!.GetValue( map )! )
if ( Enum.TryParse<BoneRole>( entry.Key.ToString(), out var role ) )
roles[role] = (int)entry.Value!;
var confidence = (float)_mappingResult.GetProperty( "Confidence" )!.GetValue( map )!;
var profile = (string)_mappingResult.GetProperty( "ProfileName" )!.GetValue( map )!;
var review = report?.GetType().GetProperty( "NeedsUserDecision" )?.GetValue( report ) is true;
return new RetargetMapping( roles, confidence, profile, review ) { Handle = map };
}
/// <summary>A hand-made mapping (used for the fixed G1 skeleton).</summary>
public RetargetMapping CreateMapping( string profile, IReadOnlyDictionary<BoneRole, int> roles )
{
var manual = Enum.Parse( _mappingSource, "Manual" );
var map = Activator.CreateInstance( _mappingResult, profile, manual )!;
var dictionary = (IDictionary)_mappingResult.GetProperty( "RoleToBone" )!.GetValue( map )!;
// Roles the installed retargeter does not know (a newer or older enum) are left out.
foreach ( var (role, bone) in roles )
if ( Enum.TryParse( _boneRole, role.ToString(), out var parsed ) )
dictionary[parsed!] = bone;
_mappingResult.GetProperty( "Confidence" )!.SetValue( map, 1f );
return new RetargetMapping( roles, 1f, profile, false ) { Handle = map };
}
/// <summary>Compiles a cached pose retargeter from <paramref name="source"/> to <paramref name="target"/>.</summary>
public PoseRetargeter CreatePoseRetargeter( RetargetSkeleton source, RetargetMapping sourceMap, RetargetSkeleton target, RetargetMapping targetMap )
{
var rig = CreateTargetRig( target, targetMap );
var constructor = _runtime.GetConstructors().OrderByDescending( c => c.GetParameters().Length ).First();
var cp = constructor.GetParameters();
var args = new object?[cp.Length];
args[0] = source.Handle;
args[1] = sourceMap.Handle;
args[2] = rig;
for ( var i = 3; i < args.Length; i++ )
args[i] = cp[i].HasDefaultValue ? cp[i].DefaultValue : null;
var runtime = Unwrap( () => constructor.Invoke( args ) )!;
var retarget = _runtime.GetMethod( "Retarget" ) ?? throw new MissingMethodException( "RuntimePoseRetargeter.Retarget" );
var delegateType = typeof( RetargetInto<> ).MakeGenericType( _xform );
var call = Delegate.CreateDelegate( delegateType, runtime, retarget );
var invoke = typeof( RetargeterLibrary ).GetMethod( nameof( InvokeRetarget ), BindingFlags.NonPublic | BindingFlags.Static )!.MakeGenericMethod( _xform );
return new PoseRetargeter( this, call, invoke, source.Count, target.Count );
}
private object CreateTargetRig( RetargetSkeleton target, RetargetMapping targetMap )
{
var fromSkeleton = _targetRig.GetMethod( "FromSkeleton", BindingFlags.Public | BindingFlags.Static ) ?? throw new MissingMethodException( "TargetRig.FromSkeleton" );
var args = fromSkeleton.GetParameters().Select( p => p.HasDefaultValue ? p.DefaultValue : null ).ToArray();
args[0] = target.Handle;
args[1] = targetMap.Handle;
return Unwrap( () => fromSkeleton.Invoke( null, args ) )!;
}
private Type Find( string name ) => Assembly.GetType( name, throwOnError: false ) ?? throw new MissingMemberException( $"The Humanoid Retargeter has no {name}; update it." );
private delegate void RetargetInto<T>( ReadOnlySpan<T> source, Span<T> destination );
private static void InvokeRetarget<T>( Delegate call, T[] source, T[] destination ) => ((RetargetInto<T>)call)( source, destination );
private static object? Unwrap( Func<object?> call )
{
try
{
return call();
}
catch ( TargetInvocationException e ) when ( e.InnerException is not null )
{
throw new InvalidOperationException( e.InnerException.Message, e.InnerException );
}
}
}
/// <summary>A compiled source-to-target pose retargeter living in the Humanoid Retargeter.</summary>
public sealed class PoseRetargeter
{
private readonly RetargeterLibrary _library;
private readonly Delegate _call;
private readonly MethodInfo _invoke;
private readonly Array _source;
private readonly Array _target;
public int SourceCount { get; }
public int TargetCount { get; }
internal PoseRetargeter( RetargeterLibrary library, Delegate call, MethodInfo invoke, int sourceCount, int targetCount )
{
_library = library;
_call = call;
_invoke = invoke;
SourceCount = sourceCount;
TargetCount = targetCount;
_source = library.NewXForms( sourceCount );
_target = library.NewXForms( targetCount );
}
/// <summary>Retargets one parent-local pose (retargeter conventions: cm, Y up).</summary>
public void Retarget( ReadOnlySpan<Vector3> sourcePositions, ReadOnlySpan<Quaternion> sourceRotations, Span<Vector3> targetPositions, Span<Quaternion> targetRotations )
{
for ( var i = 0; i < SourceCount; i++ )
_library.Write( _source, i, sourcePositions[i], sourceRotations[i] );
try
{
_invoke.Invoke( null, new object[] { _call, _source, _target } );
}
catch ( TargetInvocationException e ) when ( e.InnerException is not null )
{
throw new InvalidOperationException( $"Retargeting failed: {e.InnerException.Message}", e.InnerException );
}
for ( var i = 0; i < TargetCount; i++ )
{
var (p, r) = _library.Read( _target.GetValue( i )! );
targetPositions[i] = p;
targetRotations[i] = r;
}
}
}