Utility that generates a minimal KV3 modeldoc (.vmdl) text referencing an exported mesh file. It builds the file header and a fixed KV3 structure including a RenderMeshFile entry and an optional MaterialGroup remap block when material paths are provided.
using System.Text;
namespace AutoRig.Export;
/// <summary>
/// Generates a minimal ModelDoc vmdl (KV3 text) referencing an exported mesh file.
/// Header and layout follow the shipped modeldoc conventions (same header line the
/// humanoid-retargeter's VmdlWriter proved compiles).
/// </summary>
public static class VmdlGenerator
{
/// <summary>The KV3 header line used by shipped modeldoc vmdl files.</summary>
public const string Kv3Header =
"<!-- kv3 encoding:text:version{e21c7f3c-8a33-41c5-9977-a76d3a32aa0d} format:modeldoc30:version{8c2d7a91-9c42-4bf0-883a-5a3b1762d4f1} -->";
/// <param name="materialRemapFrom">Material name (with .vmat) as imported from the
/// mesh file, e.g. "duck_mat.vmat"; null = no material group.</param>
/// <param name="materialRemapTo">Project-relative vmat to bind it to.</param>
public static string Generate( string fbxRelativePath, string modelName,
string materialRemapFrom = null, string materialRemapTo = null )
{
ArgumentNullException.ThrowIfNull( fbxRelativePath );
ArgumentNullException.ThrowIfNull( modelName );
var path = fbxRelativePath.Replace( '\\', '/' );
var sb = new StringBuilder();
sb.Append( Kv3Header ).Append( '\n' );
sb.Append( "{\n" );
sb.Append( "\trootNode = \n" );
sb.Append( "\t{\n" );
sb.Append( "\t\t_class = \"RootNode\"\n" );
sb.Append( "\t\tchildren = \n" );
sb.Append( "\t\t[\n" );
sb.Append( "\t\t\t{\n" );
sb.Append( "\t\t\t\t_class = \"RenderMeshList\"\n" );
sb.Append( "\t\t\t\tchildren = \n" );
sb.Append( "\t\t\t\t[\n" );
sb.Append( "\t\t\t\t\t{\n" );
sb.Append( "\t\t\t\t\t\t_class = \"RenderMeshFile\"\n" );
sb.Append( $"\t\t\t\t\t\tfilename = \"{path}\"\n" );
sb.Append( "\t\t\t\t\t},\n" );
sb.Append( "\t\t\t\t]\n" );
sb.Append( "\t\t\t},\n" );
if ( materialRemapFrom is not null && materialRemapTo is not null )
{
sb.Append( "\t\t\t{\n" );
sb.Append( "\t\t\t\t_class = \"MaterialGroupList\"\n" );
sb.Append( "\t\t\t\tchildren = \n" );
sb.Append( "\t\t\t\t[\n" );
sb.Append( "\t\t\t\t\t{\n" );
sb.Append( "\t\t\t\t\t\t_class = \"DefaultMaterialGroup\"\n" );
sb.Append( "\t\t\t\t\t\tremaps = \n" );
sb.Append( "\t\t\t\t\t\t[\n" );
sb.Append( "\t\t\t\t\t\t\t{\n" );
sb.Append( $"\t\t\t\t\t\t\t\tfrom = \"{materialRemapFrom.Replace( '\\', '/' )}\"\n" );
sb.Append( $"\t\t\t\t\t\t\t\tto = \"{materialRemapTo.Replace( '\\', '/' )}\"\n" );
sb.Append( "\t\t\t\t\t\t\t},\n" );
sb.Append( "\t\t\t\t\t\t]\n" );
sb.Append( "\t\t\t\t\t\tuse_global_default = false\n" );
sb.Append( "\t\t\t\t\t\tglobal_default_material = \"\"\n" );
sb.Append( "\t\t\t\t\t},\n" );
sb.Append( "\t\t\t\t]\n" );
sb.Append( "\t\t\t},\n" );
}
sb.Append( "\t\t]\n" );
sb.Append( "\t\tmodel_archetype = \"\"\n" );
sb.Append( "\t\tprimary_associated_entity = \"\"\n" );
sb.Append( "\t\tanim_graph_name = \"\"\n" );
sb.Append( "\t}\n" );
sb.Append( "}\n" );
return sb.ToString();
}
}