Exporter utility that converts a rigged mesh and rig result into an export bundle containing FBX bytes, a vmdl text file, filenames, and any extra companion files like textures and a generated .vmat. It recenters the mesh to sit on the origin floor, optionally lifts joints, extracts a source material texture and tint, and constructs vmat and vmdl content.
using AutoRig.Mesh;
using AutoRig.Rig;
namespace AutoRig.Export;
/// <summary>Everything a caller needs to write a rigged model to disk.</summary>
public sealed class ExportBundle
{
public required byte[] Fbx { get; init; }
public required string Vmdl { get; init; }
public required string FbxFileName { get; init; }
public required string VmdlFileName { get; init; }
/// <summary>Companion files (texture image + generated .vmat), written into the
/// same folder as the fbx/vmdl. Empty when the source had no textures.</summary>
public IReadOnlyList<(string FileName, byte[] Bytes)> ExtraFiles { get; init; }
= Array.Empty<(string, byte[])>();
}
/// <summary>
/// Export façade: RigResult → binary FBX bytes + vmdl text. No file IO here (Code/
/// discipline) — the editor layer writes the files where the user chose.
/// </summary>
public static class RigExporter
{
/// <param name="assetFolder">Project-relative folder the caller will write the
/// bundle into (used for texture/vmat references inside the generated files).</param>
public static ExportBundle Export(
RigMesh mesh, RigResult rig, string modelName, string assetFolder = "models/autorig" )
{
ArgumentNullException.ThrowIfNull( mesh );
ArgumentNullException.ThrowIfNull( rig );
ArgumentNullException.ThrowIfNull( modelName );
var name = NameUtil.Sanitize( modelName );
var fbxFileName = $"{name}.fbx";
var folder = assetFolder.Replace( '\\', '/' ).Trim( '/' );
// Place the model where ModelDoc expects it: centered on the origin's
// ground plane. Off-origin sources otherwise appear shoved to one side
// (X/Z offset) or sunk through the floor (Y offset). Center the footprint
// (X, Z at the bbox center → 0) and floor-snap so the lowest point sits at
// y = 0. Mesh AND joints move together so the rig stays aligned.
var bounds = mesh.ComputeBounds();
var center = bounds.Center;
var lift = new System.Numerics.Vector3( -center.X, -bounds.Min.Y, -center.Z );
if ( lift.Length() > 1e-4f )
{
var lifted = new Mesh.RigMesh
{
SourceName = mesh.SourceName,
Positions = mesh.Positions.Select( p => p + lift ).ToArray(),
Normals = mesh.Normals,
Uvs = mesh.Uvs,
Triangles = mesh.Triangles,
TriangleTags = mesh.TriangleTags,
Tags = mesh.Tags,
Materials = mesh.Materials,
TriangleMaterials = mesh.TriangleMaterials,
};
var liftedSkeleton = new Rig.RigSkeleton();
foreach ( var j in rig.Skeleton.Joints )
liftedSkeleton.Joints.Add( new Rig.RigJoint
{
Name = j.Name,
Parent = j.Parent,
Position = j.Position + lift,
HingeAxis = j.HingeAxis,
} );
mesh = lifted;
rig = new Rig.RigResult
{
Skeleton = liftedSkeleton,
Weights = rig.Weights,
SolverName = rig.SolverName,
Degraded = rig.Degraded,
Explanation = rig.Explanation,
};
}
// Texture passthrough (v1: whole model bound to the first textured source
// material — the FBX carries a single material named "{name}_mat").
var extras = new List<(string, byte[])>();
string remapFrom = null, remapTo = null;
var source = mesh.Materials.FirstOrDefault( m => m.BaseColorImage is not null )
?? mesh.Materials.FirstOrDefault();
if ( source is not null )
{
var vmat = $"// generated by auto_rig from '{mesh.SourceName}'\nLayer0\n{{\n"
+ "\tshader \"shaders/complex.shader\"\n";
if ( source.BaseColorImage is { } image )
{
var extension = image.Length > 2 && image[0] == 0xFF && image[1] == 0xD8
? "jpg" : "png";
var imageFileName = $"{name}_color.{extension}";
extras.Add( (imageFileName, image) );
vmat += $"\tTextureColor \"{folder}/{imageFileName}\"\n";
}
if ( source.Tint != new System.Numerics.Vector3( 1f, 1f, 1f ) )
vmat += $"\tg_vColorTint \"[{source.Tint.X:0.###} {source.Tint.Y:0.###} {source.Tint.Z:0.###}]\"\n";
vmat += "}\n";
var vmatFileName = $"{name}_mat.vmat";
extras.Add( (vmatFileName, System.Text.Encoding.UTF8.GetBytes( vmat )) );
remapFrom = $"{name}_mat.vmat"; // FBX material name
remapTo = $"{folder}/{vmatFileName}";
}
return new ExportBundle
{
Fbx = FbxRigWriter.Write( mesh, rig, name ),
Vmdl = VmdlGenerator.Generate( fbxFileName, name, remapFrom, remapTo ),
FbxFileName = fbxFileName,
VmdlFileName = $"{name}.vmdl",
ExtraFiles = extras,
};
}
}