Represents per-node FBX local transform data and computes local matrices. It stores static Lcl translation/rotation/scale, pivots/offsets, pre/post rotations and conversion rules, and provides methods to build the local Matrix4x4 for given TRS or the stored defaults and to convert FBX Euler degrees to a Quaternion respecting FBX rotation order.
using System;
using System.Numerics;
namespace AutoRig.Formats.Fbx;
using Vector3 = System.Numerics.Vector3;
/// <summary>
/// Per-node FBX local-transform evaluation: the static pivot/pre-rotation data of one Model
/// plus the full FBX transform formula. Ported from humanoid-retargeter's FbxTransform
/// (same author) minus the rigid-decomposition helpers — mesh baking wants full scaled
/// world matrices.
/// </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; <c>World = Local · ParentWorld</c>.
/// </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.</summary>
public int RotationOrder { get; init; }
/// <summary>InheritType 0=RrSs, 1=RSrs, 2=Rrs.</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);
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.
/// </summary>
public Matrix4x4 LocalMatrix(Vector3 translation, Vector3 rotationDeg, Vector3 scaling)
{
var rotation = Quaternion.Normalize(
PreRotation *
EulerDegreesToQuaternion(rotationDeg, RotationOrder) *
Quaternion.Conjugate(PostRotation));
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}."),
};
}
}