FBX rig writer for AutoRig, constructs an in-memory FBX scene from a RigMesh and RigResult. It emits FBX nodes for header, settings, geometry, materials, optional skeleton with limb nodes, skin clusters, and bind pose, then serializes via FbxWriter.
using AutoRig.Formats.Fbx;
using AutoRig.Mesh;
using AutoRig.Rig;
namespace AutoRig.Export;
// s&box compat: the engine defines Vector2/Vector3 in the GLOBAL namespace, which
// shadows using-directive imports - alias explicitly to System.Numerics.
using Vector3 = System.Numerics.Vector3;
using Matrix4x4 = System.Numerics.Matrix4x4;
/// <summary>
/// Assembles a rigged binary FBX scene: mesh geometry, a LimbNode joint tree with
/// translation-only bind transforms, skin clusters carrying the weights, and a bind
/// pose — connected the way the FBX SDK expects.
/// </summary>
public static class FbxRigWriter
{
public static byte[] Write( RigMesh mesh, RigResult rig, string modelName,
bool includeSkeleton = true )
{
ArgumentNullException.ThrowIfNull( mesh );
ArgumentNullException.ThrowIfNull( rig );
mesh.Validate();
rig.Skeleton.Validate();
var root = new FbxNode( "(root)" );
// ---- header + settings ----
var header = new FbxNode( "FBXHeaderExtension" );
header.Children.Add( Scalar( "FBXHeaderVersion", 1003 ) );
header.Children.Add( Scalar( "FBXVersion", 7400 ) );
// The FileId blob, the CreationTime string and the binary footer id form a
// cryptographically CONSISTENT triplet that FBX-SDK importers validate.
// Blender pins CreationTime to the epoch below because the fixed FileId and
// footer bytes are only valid for that exact timestamp - any other time
// string makes the SDK silently reject the whole file. Do the same.
var stamp = new FbxNode( "CreationTimeStamp" );
stamp.Children.Add( Scalar( "Version", 1000 ) );
foreach ( var (name, value) in new (string, int)[]
{
("Year", 1970), ("Month", 1), ("Day", 1),
("Hour", 10), ("Minute", 0), ("Second", 0), ("Millisecond", 0),
} )
stamp.Children.Add( Scalar( name, value ) );
header.Children.Add( stamp );
header.Children.Add( ScalarString( "Creator", "auto_rig" ) );
root.Children.Add( header );
var fileId = new FbxNode( "FileId" );
fileId.Properties.Add( new byte[]
{
0x28, 0xB3, 0x2A, 0xEB, 0xB6, 0x24, 0xCC, 0xC2,
0xBF, 0xC8, 0xB0, 0x2A, 0xA9, 0x2B, 0xFC, 0xF1,
} );
root.Children.Add( fileId );
root.Children.Add( ScalarString( "CreationTime", "1970-01-01 10:00:00:000" ) );
root.Children.Add( ScalarString( "Creator", "auto_rig" ) );
var settings = new FbxNode( "GlobalSettings" );
settings.Children.Add( Scalar( "Version", 1000 ) );
var settingsProps = new FbxNode( "Properties70" );
settingsProps.Children.Add( P( "UpAxis", "int", "Integer", "", 1 ) );
settingsProps.Children.Add( P( "UpAxisSign", "int", "Integer", "", 1 ) );
settingsProps.Children.Add( P( "FrontAxis", "int", "Integer", "", 2 ) );
settingsProps.Children.Add( P( "FrontAxisSign", "int", "Integer", "", 1 ) );
settingsProps.Children.Add( P( "CoordAxis", "int", "Integer", "", 0 ) );
settingsProps.Children.Add( P( "CoordAxisSign", "int", "Integer", "", 1 ) );
settingsProps.Children.Add( P( "OriginalUpAxis", "int", "Integer", "", -1 ) );
settingsProps.Children.Add( P( "OriginalUpAxisSign", "int", "Integer", "", 1 ) );
settingsProps.Children.Add( P( "UnitScaleFactor", "double", "Number", "", 1.0 ) );
settingsProps.Children.Add( P( "OriginalUnitScaleFactor", "double", "Number", "", 1.0 ) );
settingsProps.Children.Add( P( "AmbientColor", "ColorRGB", "Color", "", 0.0, 0.0, 0.0 ) );
settingsProps.Children.Add( P( "DefaultCamera", "KString", "", "", "Producer Perspective" ) );
settingsProps.Children.Add( P( "TimeMode", "enum", "", "", 11 ) );
settingsProps.Children.Add( P( "TimeSpanStart", "KTime", "Time", "", 0L ) );
settingsProps.Children.Add( P( "TimeSpanStop", "KTime", "Time", "", 46186158000L ) );
settingsProps.Children.Add( P( "CustomFrameRate", "double", "Number", "", 24.0 ) );
settings.Children.Add( settingsProps );
root.Children.Add( settings );
// ---- documents + references + definitions (standard boilerplate many
// importers require before they will read Objects at all) ----
var documents = new FbxNode( "Documents" );
documents.Children.Add( Scalar( "Count", 1 ) );
var document = new FbxNode( "Document" );
document.Properties.Add( 999999L );
document.Properties.Add( "Scene" );
document.Properties.Add( "Scene" );
var documentProps = new FbxNode( "Properties70" );
documentProps.Children.Add( P( "SourceObject", "object", "", "" ) );
documentProps.Children.Add( P( "ActiveAnimStackName", "KString", "", "", "" ) );
document.Children.Add( documentProps );
var rootNodeId = new FbxNode( "RootNode" );
rootNodeId.Properties.Add( 0L );
document.Children.Add( rootNodeId );
documents.Children.Add( document );
root.Children.Add( documents );
root.Children.Add( new FbxNode( "References" ) );
// Definitions are filled at the END from the objects actually emitted:
// declaring phantom types (or omitting real ones - the Material bug) breaks
// Definitions-driven importers.
var definitions = new FbxNode( "Definitions" );
root.Children.Add( definitions );
// ---- objects ----
var objects = new FbxNode( "Objects" );
root.Children.Add( objects );
var connections = new FbxNode( "Connections" );
long nextId = 100000;
long NewId() => nextId++;
void Connect( long child, long parent )
{
var c = new FbxNode( "C" );
c.Properties.Add( "OO" );
c.Properties.Add( child );
c.Properties.Add( parent );
connections.Children.Add( c );
}
// Geometry.
var geometryId = NewId();
objects.Children.Add( BuildGeometry( geometryId, mesh, modelName ) );
// Mesh model at the scene root. Connection ORDER matters to at least one
// importer: parent linkage (model → root) must precede attachments
// (geometry → model), the order real exporters emit.
var meshModelId = NewId();
objects.Children.Add( BuildModel( meshModelId, modelName, "Mesh", Vector3.Zero ) );
Connect( meshModelId, 0 );
Connect( geometryId, meshModelId );
// A minimal material: import pipelines commonly drop meshes with none.
var materialId = NewId();
var material = new FbxNode( "Material" );
material.Properties.Add( materialId );
material.Properties.Add( $"{modelName}_mat\0\x01Material" );
material.Properties.Add( "" );
material.Children.Add( Scalar( "Version", 102 ) );
material.Children.Add( ScalarString( "ShadingModel", "Phong" ) );
material.Children.Add( Scalar( "MultiLayer", 0 ) );
var materialProperties = new FbxNode( "Properties70" );
materialProperties.Children.Add( P( "DiffuseColor", "Color", "", "A", 0.8, 0.8, 0.8 ) );
material.Children.Add( materialProperties );
objects.Children.Add( material );
Connect( materialId, meshModelId );
if ( !includeSkeleton )
{
root.Children.Add( connections );
var takesOnly = new FbxNode( "Takes" );
takesOnly.Children.Add( ScalarString( "Current", "" ) );
root.Children.Add( takesOnly );
FillDefinitions( definitions, objects );
return FbxWriter.Write( root );
}
// Joint LimbNode models (local translations; parents precede children), each
// with a Skeleton NodeAttribute - that attribute is how importers decide a
// node is a BONE rather than an empty transform.
var joints = rig.Skeleton.Joints;
var jointModelIds = new long[joints.Count];
// Bone rest frames: each bone's local axis points at its child so the
// skeleton imports oriented along the limbs (not spiky). The world bind
// matrices below are derived from the SAME frames, so the rest pose is
// deformation-identity - the mesh looks exactly the same, only the bones
// are drawn nicely. Falls back to translation-only if disabled.
var (boneWorld, boneLocal) = BoneFrames( joints );
for ( var j = 0; j < joints.Count; j++ )
{
var joint = joints[j];
jointModelIds[j] = NewId();
objects.Children.Add( OrientBones
? BuildModelLocal( jointModelIds[j], joint.Name, "LimbNode", boneLocal[j] )
: BuildModel( jointModelIds[j], joint.Name, "LimbNode",
joint.Parent >= 0 ? joint.Position - joints[joint.Parent].Position : joint.Position ) );
Connect( jointModelIds[j], joint.Parent >= 0 ? jointModelIds[joint.Parent] : 0 );
var attributeId = NewId();
var attribute = new FbxNode( "NodeAttribute" );
attribute.Properties.Add( attributeId );
attribute.Properties.Add( $"{joint.Name}_attr\0\x01NodeAttribute" );
attribute.Properties.Add( "LimbNode" );
attribute.Children.Add( ScalarString( "TypeFlags", "Skeleton" ) );
objects.Children.Add( attribute );
Connect( attributeId, jointModelIds[j] );
}
// Skin deformer + one cluster per bone that influences at least one vertex.
var skinId = NewId();
var skin = new FbxNode( "Deformer" );
skin.Properties.Add( skinId );
skin.Properties.Add( $"{modelName}_skin\0\x01Deformer" );
skin.Properties.Add( "Skin" );
skin.Children.Add( Scalar( "Version", 101 ) );
var accuracy = new FbxNode( "Link_DeformAcuracy" );
accuracy.Properties.Add( 50.0 );
skin.Children.Add( accuracy );
objects.Children.Add( skin );
Connect( skinId, geometryId );
var vertexCount = mesh.Positions.Length;
for ( var bone = 0; bone < joints.Count; bone++ )
{
var indexes = new List<int>();
var weights = new List<double>();
for ( var v = 0; v < vertexCount; v++ )
for ( var k = 0; k < 4; k++ )
{
if ( rig.Weights.BoneIndices[v * 4 + k] != bone )
continue;
var w = rig.Weights.Weights[v * 4 + k];
if ( w <= 0f )
continue;
indexes.Add( v );
weights.Add( w );
}
if ( indexes.Count == 0 )
continue;
var clusterId = NewId();
var cluster = new FbxNode( "Deformer" );
cluster.Properties.Add( clusterId );
cluster.Properties.Add( $"{joints[bone].Name}_cluster\0\x01SubDeformer" );
cluster.Properties.Add( "Cluster" );
cluster.Children.Add( Scalar( "Version", 100 ) );
cluster.Children.Add( ScalarString( "Mode", "Total1" ) );
var indexesNode = new FbxNode( "Indexes" );
indexesNode.Properties.Add( indexes.ToArray() );
cluster.Children.Add( indexesNode );
var weightsNode = new FbxNode( "Weights" );
weightsNode.Properties.Add( weights.ToArray() );
cluster.Children.Add( weightsNode );
// TransformLink = bone world bind matrix; Transform = its inverse.
// Oriented mode uses the full rest frame (rotation + translation);
// both are the SAME matrix inverted, so the bind stays exact.
if ( OrientBones )
{
Matrix4x4.Invert( boneWorld[bone], out var inv );
cluster.Children.Add( MatrixM( "TransformLink", boneWorld[bone] ) );
cluster.Children.Add( MatrixM( "Transform", inv ) );
}
else
{
var p = joints[bone].Position;
cluster.Children.Add( Matrix( "TransformLink", p ) );
cluster.Children.Add( Matrix( "Transform", -p ) );
}
objects.Children.Add( cluster );
Connect( clusterId, skinId );
Connect( jointModelIds[bone], clusterId );
}
// Bind pose.
var pose = new FbxNode( "Pose" );
pose.Properties.Add( NewId() );
pose.Properties.Add( $"{modelName}_bindpose\0\x01Pose" );
pose.Properties.Add( "BindPose" );
pose.Children.Add( Scalar( "NbPoseNodes", joints.Count ) );
for ( var j = 0; j < joints.Count; j++ )
{
var poseNode = new FbxNode( "PoseNode" );
var nodeId = new FbxNode( "Node" );
nodeId.Properties.Add( jointModelIds[j] );
poseNode.Children.Add( nodeId );
poseNode.Children.Add( OrientBones
? MatrixM( "Matrix", boneWorld[j] )
: Matrix( "Matrix", joints[j].Position ) );
pose.Children.Add( poseNode );
}
objects.Children.Add( pose );
root.Children.Add( connections );
var takes = new FbxNode( "Takes" );
takes.Children.Add( ScalarString( "Current", "" ) );
root.Children.Add( takes );
FillDefinitions( definitions, objects );
return FbxWriter.Write( root );
}
/// <summary>Definitions computed from the objects actually present (plus GlobalSettings).</summary>
static void FillDefinitions( FbxNode definitions, FbxNode objects )
{
var counts = new Dictionary<string, int>( StringComparer.Ordinal ) { ["GlobalSettings"] = 1 };
foreach ( var child in objects.Children )
counts[child.Name] = counts.GetValueOrDefault( child.Name ) + 1;
definitions.Children.Add( Scalar( "Version", 100 ) );
definitions.Children.Add( Scalar( "Count", counts.Values.Sum() ) );
foreach ( var (typeName, count) in counts )
{
var objectType = new FbxNode( "ObjectType" );
objectType.Properties.Add( typeName );
objectType.Children.Add( Scalar( "Count", count ) );
definitions.Children.Add( objectType );
}
}
static FbxNode BuildGeometry( long id, RigMesh mesh, string modelName )
{
var geometry = new FbxNode( "Geometry" );
geometry.Properties.Add( id );
geometry.Properties.Add( $"{modelName}_geo\0\x01Geometry" );
geometry.Properties.Add( "Mesh" );
geometry.Children.Add( new FbxNode( "Properties70" ) );
geometry.Children.Add( Scalar( "GeometryVersion", 124 ) );
var vertices = new double[mesh.Positions.Length * 3];
for ( var i = 0; i < mesh.Positions.Length; i++ )
{
vertices[i * 3] = mesh.Positions[i].X;
vertices[i * 3 + 1] = mesh.Positions[i].Y;
vertices[i * 3 + 2] = mesh.Positions[i].Z;
}
var verticesNode = new FbxNode( "Vertices" );
verticesNode.Properties.Add( vertices );
geometry.Children.Add( verticesNode );
var polygons = new int[mesh.Triangles.Length];
for ( var t = 0; t < mesh.TriangleCount; t++ )
{
polygons[t * 3] = mesh.Triangles[t * 3];
polygons[t * 3 + 1] = mesh.Triangles[t * 3 + 1];
polygons[t * 3 + 2] = ~mesh.Triangles[t * 3 + 2];
}
var polygonsNode = new FbxNode( "PolygonVertexIndex" );
polygonsNode.Properties.Add( polygons );
geometry.Children.Add( polygonsNode );
// Normals ByPolygonVertex/Direct (per corner) - what every real exporter
// writes; ByVertice made at least one engine importer drop the geometry.
var normals = mesh.Normals.Length == mesh.Positions.Length
? mesh.Normals
: ComputeVertexNormals( mesh );
var normalData = new double[mesh.Triangles.Length * 3];
for ( var corner = 0; corner < mesh.Triangles.Length; corner++ )
{
var n = normals[mesh.Triangles[corner]];
normalData[corner * 3] = n.X;
normalData[corner * 3 + 1] = n.Y;
normalData[corner * 3 + 2] = n.Z;
}
var layer = new FbxNode( "LayerElementNormal" );
layer.Properties.Add( 0 );
layer.Children.Add( Scalar( "Version", 101 ) );
layer.Children.Add( ScalarString( "Name", "" ) );
layer.Children.Add( ScalarString( "MappingInformationType", "ByPolygonVertex" ) );
layer.Children.Add( ScalarString( "ReferenceInformationType", "Direct" ) );
var normalsNode = new FbxNode( "Normals" );
normalsNode.Properties.Add( normalData );
layer.Children.Add( normalsNode );
geometry.Children.Add( layer );
var hasUvs = mesh.Uvs.Length == mesh.Positions.Length;
if ( hasUvs )
{
// Per-corner Direct, mirroring the normal layer (the safest widely-
// accepted layout).
var uvData = new double[mesh.Triangles.Length * 2];
for ( var corner = 0; corner < mesh.Triangles.Length; corner++ )
{
var uv = mesh.Uvs[mesh.Triangles[corner]];
uvData[corner * 2] = uv.X;
uvData[corner * 2 + 1] = uv.Y;
}
var uvLayer = new FbxNode( "LayerElementUV" );
uvLayer.Properties.Add( 0 );
uvLayer.Children.Add( Scalar( "Version", 101 ) );
uvLayer.Children.Add( ScalarString( "Name", "UVMap" ) );
uvLayer.Children.Add( ScalarString( "MappingInformationType", "ByPolygonVertex" ) );
uvLayer.Children.Add( ScalarString( "ReferenceInformationType", "Direct" ) );
var uvNode = new FbxNode( "UV" );
uvNode.Properties.Add( uvData );
uvLayer.Children.Add( uvNode );
geometry.Children.Add( uvLayer );
}
// All triangles use material slot 0.
var materialLayer = new FbxNode( "LayerElementMaterial" );
materialLayer.Properties.Add( 0 );
materialLayer.Children.Add( Scalar( "Version", 101 ) );
materialLayer.Children.Add( ScalarString( "Name", "" ) );
materialLayer.Children.Add( ScalarString( "MappingInformationType", "AllSame" ) );
materialLayer.Children.Add( ScalarString( "ReferenceInformationType", "IndexToDirect" ) );
var materialsNode = new FbxNode( "Materials" );
materialsNode.Properties.Add( new[] { 0 } );
materialLayer.Children.Add( materialsNode );
geometry.Children.Add( materialLayer );
// The Layer node binds the layer elements; importers ignore elements it
// does not list.
var layerNode = new FbxNode( "Layer" );
layerNode.Properties.Add( 0 );
layerNode.Children.Add( Scalar( "Version", 100 ) );
void AddLayerElement( string type )
{
var element = new FbxNode( "LayerElement" );
element.Children.Add( ScalarString( "Type", type ) );
element.Children.Add( Scalar( "TypedIndex", 0 ) );
layerNode.Children.Add( element );
}
AddLayerElement( "LayerElementNormal" );
AddLayerElement( "LayerElementMaterial" );
if ( hasUvs )
AddLayerElement( "LayerElementUV" );
geometry.Children.Add( layerNode );
return geometry;
}
static Vector3[] ComputeVertexNormals( RigMesh mesh )
{
var normals = new Vector3[mesh.Positions.Length];
for ( var t = 0; t < mesh.Triangles.Length; t += 3 )
{
var a = mesh.Positions[mesh.Triangles[t]];
var b = mesh.Positions[mesh.Triangles[t + 1]];
var c = mesh.Positions[mesh.Triangles[t + 2]];
var n = Vector3.Cross( b - a, c - a );
normals[mesh.Triangles[t]] += n;
normals[mesh.Triangles[t + 1]] += n;
normals[mesh.Triangles[t + 2]] += n;
}
for ( var i = 0; i < normals.Length; i++ )
{
var length = normals[i].Length();
normals[i] = length > 1e-12f ? normals[i] / length : Vector3.UnitY;
}
return normals;
}
static FbxNode BuildModel( long id, string name, string subClass, Vector3 translation )
{
var model = new FbxNode( "Model" );
model.Properties.Add( id );
model.Properties.Add( $"{name}\0\x01Model" );
model.Properties.Add( subClass );
model.Children.Add( Scalar( "Version", 232 ) );
var properties = new FbxNode( "Properties70" );
properties.Children.Add( P( "Lcl Translation", "Lcl Translation", "", "A",
(double)translation.X, (double)translation.Y, (double)translation.Z ) );
properties.Children.Add( P( "Lcl Rotation", "Lcl Rotation", "", "A", 0.0, 0.0, 0.0 ) );
properties.Children.Add( P( "Lcl Scaling", "Lcl Scaling", "", "A", 1.0, 1.0, 1.0 ) );
properties.Children.Add( P( "DefaultAttributeIndex", "int", "Integer", "", 0 ) );
properties.Children.Add( P( "InheritType", "enum", "", "", 1 ) );
model.Children.Add( properties );
model.Children.Add( Scalar( "MultiLayer", 0 ) );
model.Children.Add( Scalar( "MultiTake", 0 ) );
var shading = new FbxNode( "Shading" );
shading.Properties.Add( true ); // bool, like real exporters - a string here
model.Children.Add( shading ); // can abort strict SDK parses
model.Children.Add( ScalarString( "Culling", "CullingOff" ) );
return model;
}
/// <summary>Orient bone rest frames toward their children so the skeleton
/// imports along the limbs. The bind matrices are derived from the same
/// frames, so the rest deformation is identity - the mesh is unchanged.</summary>
const bool OrientBones = true;
/// <summary>World + local rest frames per bone. Each bone's local +Y axis is
/// aimed at (the mean of) its children; leaves inherit their parent's aim.
/// Row-vector matrices: world = rotate-then-translate; local = world *
/// inverse(parent world).</summary>
static (Matrix4x4[] World, Matrix4x4[] Local) BoneFrames( IReadOnlyList<RigJoint> joints )
{
var n = joints.Count;
var kids = new List<int>[n];
for ( var i = 0; i < n; i++ )
kids[i] = new List<int>();
for ( var i = 0; i < n; i++ )
if ( joints[i].Parent >= 0 )
kids[joints[i].Parent].Add( i );
var dir = new Vector3[n];
for ( var i = 0; i < n; i++ )
{
if ( kids[i].Count == 0 )
continue;
var c = Vector3.Zero;
foreach ( var k in kids[i] )
c += joints[k].Position;
c /= kids[i].Count;
var d = c - joints[i].Position;
dir[i] = d.LengthSquared() > 1e-12f ? Vector3.Normalize( d ) : Vector3.Zero;
}
for ( var i = 0; i < n; i++ ) // parents precede children → parent dir is ready
if ( dir[i] == Vector3.Zero )
dir[i] = joints[i].Parent >= 0 ? dir[joints[i].Parent] : Vector3.UnitY;
var world = new Matrix4x4[n];
var local = new Matrix4x4[n];
for ( var i = 0; i < n; i++ )
{
var rot = RotationFromTo( Vector3.UnitY, dir[i] );
world[i] = rot * Matrix4x4.CreateTranslation( joints[i].Position );
if ( joints[i].Parent >= 0 && Matrix4x4.Invert( world[joints[i].Parent], out var invP ) )
local[i] = world[i] * invP;
else
local[i] = world[i];
}
return (world, local);
}
/// <summary>Row-vector rotation mapping unit vector <paramref name="a"/> onto
/// <paramref name="b"/> (both assumed unit length).</summary>
static Matrix4x4 RotationFromTo( Vector3 a, Vector3 b )
{
var d = Math.Clamp( Vector3.Dot( a, b ), -1f, 1f );
if ( d > 0.99999f )
return Matrix4x4.Identity;
if ( d < -0.99999f )
{
var perp = Vector3.Cross( a, Vector3.UnitX );
if ( perp.LengthSquared() < 1e-6f )
perp = Vector3.Cross( a, Vector3.UnitZ );
return Matrix4x4.CreateFromAxisAngle( Vector3.Normalize( perp ), MathF.PI );
}
return Matrix4x4.CreateFromAxisAngle(
Vector3.Normalize( Vector3.Cross( a, b ) ), MathF.Acos( d ) );
}
/// <summary>Euler angles (degrees, FBX XYZ order) from a row-vector rotation
/// matrix's upper-left 3x3.</summary>
static Vector3 EulerXYZ( Matrix4x4 m )
{
var sy = MathF.Sqrt( m.M11 * m.M11 + m.M12 * m.M12 );
float x, y, z;
if ( sy > 1e-6f )
{
x = MathF.Atan2( m.M23, m.M33 );
y = MathF.Atan2( -m.M13, sy );
z = MathF.Atan2( m.M12, m.M11 );
}
else
{
x = MathF.Atan2( -m.M32, m.M22 );
y = MathF.Atan2( -m.M13, sy );
z = 0f;
}
const float toDeg = 180f / MathF.PI;
return new Vector3( x * toDeg, y * toDeg, z * toDeg );
}
/// <summary>A LimbNode model whose local transform carries translation AND the
/// child-pointing rotation (as Euler, FBX's node representation).</summary>
static FbxNode BuildModelLocal( long id, string name, string subClass, Matrix4x4 local )
{
var euler = EulerXYZ( local );
var model = new FbxNode( "Model" );
model.Properties.Add( id );
model.Properties.Add( $"{name}\0\x01Model" );
model.Properties.Add( subClass );
model.Children.Add( Scalar( "Version", 232 ) );
var properties = new FbxNode( "Properties70" );
properties.Children.Add( P( "Lcl Translation", "Lcl Translation", "", "A",
(double)local.M41, (double)local.M42, (double)local.M43 ) );
properties.Children.Add( P( "Lcl Rotation", "Lcl Rotation", "", "A",
(double)euler.X, (double)euler.Y, (double)euler.Z ) );
properties.Children.Add( P( "Lcl Scaling", "Lcl Scaling", "", "A", 1.0, 1.0, 1.0 ) );
properties.Children.Add( P( "DefaultAttributeIndex", "int", "Integer", "", 0 ) );
properties.Children.Add( P( "InheritType", "enum", "", "", 1 ) );
model.Children.Add( properties );
model.Children.Add( Scalar( "MultiLayer", 0 ) );
model.Children.Add( Scalar( "MultiTake", 0 ) );
var shading = new FbxNode( "Shading" );
shading.Properties.Add( true );
model.Children.Add( shading );
model.Children.Add( ScalarString( "Culling", "CullingOff" ) );
return model;
}
/// <summary>A full 4x4 matrix as 16 doubles (row-vector layout).</summary>
static FbxNode MatrixM( string name, Matrix4x4 m )
{
var node = new FbxNode( name );
node.Properties.Add( new[]
{
(double)m.M11, m.M12, m.M13, m.M14,
m.M21, m.M22, m.M23, m.M24,
m.M31, m.M32, m.M33, m.M34,
m.M41, m.M42, m.M43, m.M44,
} );
return node;
}
/// <summary>A 4x4 translation matrix as 16 doubles (row-vector layout).</summary>
static FbxNode Matrix( string name, Vector3 translation )
{
var node = new FbxNode( name );
node.Properties.Add( new[]
{
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
(double)translation.X, translation.Y, translation.Z, 1.0,
} );
return node;
}
static FbxNode Scalar( string name, int value )
{
var node = new FbxNode( name );
node.Properties.Add( value );
return node;
}
static FbxNode ScalarString( string name, string value )
{
var node = new FbxNode( name );
node.Properties.Add( value );
return node;
}
static FbxNode P( string name, string type, string label, string flags, params object[] values )
{
var p = new FbxNode( "P" );
p.Properties.Add( name );
p.Properties.Add( type );
p.Properties.Add( label );
p.Properties.Add( flags );
foreach ( var v in values )
p.Properties.Add( v );
return p;
}
}