Code/HumanoidMocap/Core/Formats/Fbx/ModelDocFbxExport.cs
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 );
}
}