Writer for .vmdl (KV3 model) files used in the editor conversion pipeline. It builds a KV3 text representation from a ModelBuildContext and writes it to disk, emitting render meshes, body groups, material remaps/groups, animations, attachments and an optional scale modifier.
using System;
using System.Globalization;
using System.IO;
using System.Text;
namespace GmodAssetImporter;
public static class VmdlWriter
{
const string Kv3Header =
"<!-- kv3 encoding:text:version{e21c7f3c-8a33-41c5-9977-a76d3a32aa0d} format:modeldoc30:version{8c2d7a91-9c42-4bf0-883a-5a3b1762d4f1} -->";
static readonly Encoding Utf8NoBom = new UTF8Encoding( false );
public static string Build( ModelBuildContext context )
{
var sb = new StringBuilder();
sb.AppendLine( Kv3Header );
sb.AppendLine( "{" );
sb.AppendLine( "\trootNode =" );
sb.AppendLine( "\t{" );
sb.AppendLine( "\t\t_class = \"RootNode\"" );
sb.AppendLine( "\t\tchildren =" );
sb.AppendLine( "\t\t[" );
WriteRenderMeshList( sb, context );
WriteBodyGroupList( sb, context );
WriteMaterialGroupList( sb, context );
WriteAnimationList( sb, context );
WriteAttachmentList( sb, context );
WriteScaleModifier( sb, context );
sb.AppendLine( "\t\t]" );
sb.AppendLine( "\t\tmodel_archetype = \"\"" );
sb.AppendLine( "\t\tprimary_associated_entity = \"\"" );
sb.AppendLine( "\t\tanim_graph_name = \"\"" );
sb.AppendLine( "\t\tbase_model_name = \"\"" );
sb.AppendLine( "\t}" );
sb.AppendLine( "}" );
return sb.ToString();
}
public static void Write( string path, ModelBuildContext context )
{
Directory.CreateDirectory( Path.GetDirectoryName( path )! );
File.WriteAllText( path, Build( context ), Utf8NoBom );
}
static void WriteRenderMeshList( StringBuilder sb, ModelBuildContext context )
{
sb.AppendLine( "\t\t\t{" );
sb.AppendLine( "\t\t\t\t_class = \"RenderMeshList\"" );
sb.AppendLine( "\t\t\t\tchildren =" );
sb.AppendLine( "\t\t\t\t[" );
foreach ( var mesh in context.Meshes )
{
var file = AssetPath( context, mesh.FileName );
sb.AppendLine( "\t\t\t\t\t{" );
sb.AppendLine( "\t\t\t\t\t\t_class = \"RenderMeshFile\"" );
sb.AppendLine( $"\t\t\t\t\t\tname = \"{Esc( mesh.Name )}\"" );
sb.AppendLine( $"\t\t\t\t\t\tfilename = \"{Esc( file )}\"" );
sb.AppendLine( "\t\t\t\t\t\timport_translation = [ 0.0, 0.0, 0.0 ]" );
sb.AppendLine( "\t\t\t\t\t\timport_rotation = [ 0.0, 0.0, 0.0 ]" );
sb.AppendLine( "\t\t\t\t\t\timport_scale = 1.0" );
sb.AppendLine( "\t\t\t\t\t\talign_origin_x_type = \"None\"" );
sb.AppendLine( "\t\t\t\t\t\talign_origin_y_type = \"None\"" );
sb.AppendLine( "\t\t\t\t\t\talign_origin_z_type = \"None\"" );
sb.AppendLine( "\t\t\t\t\t\tparent_bone = \"\"" );
sb.AppendLine( "\t\t\t\t\t\timport_filter =" );
sb.AppendLine( "\t\t\t\t\t\t{" );
sb.AppendLine( "\t\t\t\t\t\t\texclude_by_default = false" );
sb.AppendLine( "\t\t\t\t\t\t\texception_list = [ ]" );
sb.AppendLine( "\t\t\t\t\t\t}" );
sb.AppendLine( "\t\t\t\t\t}," );
}
sb.AppendLine( "\t\t\t\t]" );
sb.AppendLine( "\t\t\t}," );
}
static void WriteBodyGroupList( StringBuilder sb, ModelBuildContext context )
{
if ( context.BodyGroups.Count == 0 ) return;
sb.AppendLine( "\t\t\t{" );
sb.AppendLine( "\t\t\t\t_class = \"BodyGroupList\"" );
sb.AppendLine( "\t\t\t\tchildren =" );
sb.AppendLine( "\t\t\t\t[" );
foreach ( var group in context.BodyGroups )
{
sb.AppendLine( "\t\t\t\t\t{" );
sb.AppendLine( "\t\t\t\t\t\t_class = \"BodyGroup\"" );
sb.AppendLine( $"\t\t\t\t\t\tname = \"{Esc( group.Name )}\"" );
sb.AppendLine( "\t\t\t\t\t\tchildren =" );
sb.AppendLine( "\t\t\t\t\t\t[" );
foreach ( var choice in group.Choices )
{
sb.AppendLine( "\t\t\t\t\t\t\t{" );
sb.AppendLine( "\t\t\t\t\t\t\t\t_class = \"BodyGroupChoice\"" );
sb.AppendLine( $"\t\t\t\t\t\t\t\tname = \"{Esc( choice.Name )}\"" );
sb.AppendLine( "\t\t\t\t\t\t\t\tmeshes =" );
sb.AppendLine( "\t\t\t\t\t\t\t\t[" );
foreach ( var meshName in choice.MeshNames )
sb.AppendLine( $"\t\t\t\t\t\t\t\t\t\"{Esc( meshName )}\"," );
sb.AppendLine( "\t\t\t\t\t\t\t\t]" );
sb.AppendLine( "\t\t\t\t\t\t\t}," );
}
sb.AppendLine( "\t\t\t\t\t\t]" );
sb.AppendLine( "\t\t\t\t\t\thidden_in_tools = false" );
sb.AppendLine( "\t\t\t\t\t}," );
}
sb.AppendLine( "\t\t\t\t]" );
sb.AppendLine( "\t\t\t}," );
}
static void WriteMaterialGroupList( StringBuilder sb, ModelBuildContext context )
{
if ( context.MaterialRemaps.Count == 0 && context.MaterialGroups.Count == 0 ) return;
sb.AppendLine( "\t\t\t{" );
sb.AppendLine( "\t\t\t\t_class = \"MaterialGroupList\"" );
sb.AppendLine( "\t\t\t\tchildren =" );
sb.AppendLine( "\t\t\t\t[" );
sb.AppendLine( "\t\t\t\t\t{" );
sb.AppendLine( "\t\t\t\t\t\t_class = \"DefaultMaterialGroup\"" );
sb.AppendLine( "\t\t\t\t\t\tremaps =" );
sb.AppendLine( "\t\t\t\t\t\t[" );
foreach ( var remap in context.MaterialRemaps )
{
sb.AppendLine( "\t\t\t\t\t\t\t{" );
sb.AppendLine( $"\t\t\t\t\t\t\t\tfrom = \"{Esc( remap.From )}\"" );
sb.AppendLine( $"\t\t\t\t\t\t\t\tto = \"{Esc( remap.To )}\"" );
sb.AppendLine( "\t\t\t\t\t\t\t}," );
}
sb.AppendLine( "\t\t\t\t\t\t]" );
sb.AppendLine( "\t\t\t\t\t\tuse_global_default = false" );
sb.AppendLine( "\t\t\t\t\t\tglobal_default_material = \"\"" );
sb.AppendLine( "\t\t\t\t\t}," );
foreach ( var group in context.MaterialGroups )
{
sb.AppendLine( "\t\t\t\t\t{" );
sb.AppendLine( "\t\t\t\t\t\t_class = \"MaterialGroup\"" );
sb.AppendLine( $"\t\t\t\t\t\tname = \"{Esc( group.Name )}\"" );
sb.AppendLine( "\t\t\t\t\t\tremaps =" );
sb.AppendLine( "\t\t\t\t\t\t[" );
foreach ( var remap in group.Remaps )
{
sb.AppendLine( "\t\t\t\t\t\t\t{" );
sb.AppendLine( $"\t\t\t\t\t\t\t\tfrom = \"{Esc( remap.From )}\"" );
sb.AppendLine( $"\t\t\t\t\t\t\t\tto = \"{Esc( remap.To )}\"" );
sb.AppendLine( "\t\t\t\t\t\t\t}," );
}
sb.AppendLine( "\t\t\t\t\t\t]" );
sb.AppendLine( "\t\t\t\t\t}," );
}
sb.AppendLine( "\t\t\t\t]" );
sb.AppendLine( "\t\t\t}," );
}
static void WriteAnimationList( StringBuilder sb, ModelBuildContext context )
{
sb.AppendLine( "\t\t\t{" );
sb.AppendLine( "\t\t\t\t_class = \"AnimationList\"" );
sb.AppendLine( "\t\t\t\tchildren =" );
sb.AppendLine( "\t\t\t\t[" );
if ( context.IncludeBindPose )
{
sb.AppendLine( "\t\t\t\t\t{" );
sb.AppendLine( "\t\t\t\t\t\t_class = \"AnimBindPose\"" );
sb.AppendLine( "\t\t\t\t\t\tname = \"bindPose\"" );
sb.AppendLine( "\t\t\t\t\t\tactivity_name = \"\"" );
sb.AppendLine( "\t\t\t\t\t\tactivity_weight = 1" );
sb.AppendLine( "\t\t\t\t\t\tweight_list_name = \"\"" );
sb.AppendLine( "\t\t\t\t\t\tfade_in_time = 0.2" );
sb.AppendLine( "\t\t\t\t\t\tfade_out_time = 0.2" );
sb.AppendLine( "\t\t\t\t\t\tlooping = false" );
sb.AppendLine( "\t\t\t\t\t\tdelta = false" );
sb.AppendLine( "\t\t\t\t\t\tworldSpace = false" );
sb.AppendLine( "\t\t\t\t\t\thidden = false" );
sb.AppendLine( "\t\t\t\t\t\tanim_markup_ordered = false" );
sb.AppendLine( "\t\t\t\t\t\tdisable_compression = false" );
sb.AppendLine( "\t\t\t\t\t\tdisable_interpolation = false" );
sb.AppendLine( "\t\t\t\t\t\tenable_scale = false" );
sb.AppendLine( "\t\t\t\t\t\tframe_count = 1" );
sb.AppendLine( "\t\t\t\t\t\tframe_rate = 30" );
sb.AppendLine( "\t\t\t\t\t}," );
}
foreach ( var anim in context.Animations )
{
var file = AssetPath( context, anim.FileName );
sb.AppendLine( "\t\t\t\t\t{" );
sb.AppendLine( "\t\t\t\t\t\t_class = \"AnimFile\"" );
sb.AppendLine( $"\t\t\t\t\t\tname = \"{Esc( anim.Name )}\"" );
sb.AppendLine( "\t\t\t\t\t\tactivity_name = \"\"" );
sb.AppendLine( "\t\t\t\t\t\tactivity_weight = 1" );
sb.AppendLine( "\t\t\t\t\t\tweight_list_name = \"\"" );
sb.AppendLine( "\t\t\t\t\t\tfade_in_time = 0.2" );
sb.AppendLine( "\t\t\t\t\t\tfade_out_time = 0.2" );
sb.AppendLine( $"\t\t\t\t\t\tlooping = {(anim.Looping ? "true" : "false")}" );
sb.AppendLine( "\t\t\t\t\t\tdelta = false" );
sb.AppendLine( "\t\t\t\t\t\tworldSpace = false" );
sb.AppendLine( "\t\t\t\t\t\thidden = false" );
sb.AppendLine( "\t\t\t\t\t\tanim_markup_ordered = false" );
sb.AppendLine( "\t\t\t\t\t\tdisable_compression = false" );
sb.AppendLine( "\t\t\t\t\t\tenable_scale = false" );
sb.AppendLine( $"\t\t\t\t\t\tsource_filename = \"{Esc( file )}\"" );
sb.AppendLine( "\t\t\t\t\t\tstart_frame = -1" );
sb.AppendLine( "\t\t\t\t\t\tend_frame = -1" );
sb.AppendLine( $"\t\t\t\t\t\tframerate = {Fmt( anim.FrameRate )}" );
sb.AppendLine( "\t\t\t\t\t\ttake = 0" );
sb.AppendLine( "\t\t\t\t\t\treverse = false" );
sb.AppendLine( "\t\t\t\t\t}," );
}
sb.AppendLine( "\t\t\t\t]" );
sb.AppendLine( "\t\t\t}," );
}
static void WriteAttachmentList( StringBuilder sb, ModelBuildContext context )
{
if ( context.Attachments.Count == 0 ) return;
sb.AppendLine( "\t\t\t{" );
sb.AppendLine( "\t\t\t\t_class = \"AttachmentList\"" );
sb.AppendLine( "\t\t\t\tchildren =" );
sb.AppendLine( "\t\t\t\t[" );
foreach ( var att in context.Attachments )
{
sb.AppendLine( "\t\t\t\t\t{" );
sb.AppendLine( "\t\t\t\t\t\t_class = \"Attachment\"" );
sb.AppendLine( $"\t\t\t\t\t\tname = \"{Esc( att.Name )}\"" );
sb.AppendLine( $"\t\t\t\t\t\tparent_bone = \"{Esc( att.ParentBone )}\"" );
sb.AppendLine( $"\t\t\t\t\t\trelative_origin = {FmtVec( att.Origin )}" );
sb.AppendLine( $"\t\t\t\t\t\trelative_angles = {FmtVec( att.Angles )}" );
sb.AppendLine( "\t\t\t\t\t\tweight = 1.0" );
sb.AppendLine( "\t\t\t\t\t\tignore_rotation = false" );
sb.AppendLine( "\t\t\t\t\t}," );
}
sb.AppendLine( "\t\t\t\t]" );
sb.AppendLine( "\t\t\t}," );
}
static void WriteScaleModifier( StringBuilder sb, ModelBuildContext context )
{
if ( MathF.Abs( context.Scale - 1.0f ) < 0.0001f ) return;
sb.AppendLine( "\t\t\t{" );
sb.AppendLine( "\t\t\t\t_class = \"ModelModifierList\"" );
sb.AppendLine( "\t\t\t\tchildren =" );
sb.AppendLine( "\t\t\t\t[" );
sb.AppendLine( "\t\t\t\t\t{" );
sb.AppendLine( "\t\t\t\t\t\t_class = \"ModelModifier_ScaleAndMirror\"" );
sb.AppendLine( $"\t\t\t\t\t\tscale = {Fmt( context.Scale )}" );
sb.AppendLine( "\t\t\t\t\t\tmirror_x = false" );
sb.AppendLine( "\t\t\t\t\t\tmirror_y = false" );
sb.AppendLine( "\t\t\t\t\t\tmirror_z = false" );
sb.AppendLine( "\t\t\t\t\t\tflip_bone_forward = false" );
sb.AppendLine( "\t\t\t\t\t\tswap_left_and_right_bones = false" );
sb.AppendLine( "\t\t\t\t\t}," );
sb.AppendLine( "\t\t\t\t]" );
sb.AppendLine( "\t\t\t}," );
}
static string AssetPath( ModelBuildContext context, string fileName )
{
if ( string.IsNullOrWhiteSpace( context.ModelAssetDirectory ) )
return PathResolve.NormalizeSlashes( fileName );
return PathResolve.CombineAsset( context.ModelAssetDirectory, fileName );
}
static string Esc( string s ) => (s ?? "").Replace( "\\", "\\\\" ).Replace( "\"", "\\\"" );
static string Fmt( float v ) => v.ToString( "0.######", CultureInfo.InvariantCulture );
static string FmtVec( Vec3 v ) => $"[ {Fmt( v.X )}, {Fmt( v.Y )}, {Fmt( v.Z )} ]";
}