Editor utility that generates KV3-format .vmdl model text from a source mesh path and options. It builds string output for material group, physics shape list and render mesh list according to selected collision, scale and origin alignment options.
using System.Globalization;
using System.Text;
namespace ModelPro.Vmdl;
/// <summary>
/// Options for converting a mesh file (.fbx, .obj, .dmx) into a model (.vmdl),
/// matching the asset browser's create-model popup.
/// </summary>
public struct MeshToModelOptions
{
/// <summary>The collision shape to generate for the model.</summary>
public CollisionMode Collision { get; set; }
/// <summary>The import scale (in inches) to apply to the mesh.</summary>
public float ImportScale { get; set; }
public AlignOrigin AlignOriginX { get; set; }
public AlignOrigin AlignOriginY { get; set; }
public AlignOrigin AlignOriginZ { get; set; }
}
/// <summary>
/// Generates the KV3 text of a model (.vmdl) file from a source mesh file,
/// writing the same structure ModelDoc produces so it compiles cleanly.
/// </summary>
public static class VmdlGenerator
{
const string Header = "<!-- kv3 encoding:text:version{e21c7f3c-8a33-41c5-9977-a76d3a32aa0d} format:modeldoc30:version{8c2d7a91-9c42-4bf0-883a-5a3b1762d4f1} -->";
/// <summary>
/// Build the vmdl source text for a mesh file at the given content-relative
/// path (e.g. "models/foo.fbx").
/// </summary>
public static string Generate( string meshRelativePath, MeshToModelOptions options )
{
var sb = new StringBuilder();
sb.AppendLine( Header );
sb.AppendLine( "{" );
sb.AppendLine( "\trootNode = " );
sb.AppendLine( "\t{" );
sb.AppendLine( "\t\t_class = \"RootNode\"" );
sb.AppendLine( "\t\tchildren = " );
sb.AppendLine( "\t\t[" );
// Material group
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\tuse_global_default = true" );
sb.AppendLine( "\t\t\t\t\t\tglobal_default_material = \"materials/default.vmat\"" );
sb.AppendLine( "\t\t\t\t\t}," );
sb.AppendLine( "\t\t\t\t]" );
sb.AppendLine( "\t\t\t}," );
// Physics shape list
AppendPhysicsShapeList( sb, meshRelativePath, options );
// Render mesh list
AppendRenderMeshList( sb, meshRelativePath, options );
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();
}
private static void AppendRenderMeshList( StringBuilder sb, string meshRelativePath, MeshToModelOptions options )
{
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[" );
sb.AppendLine( "\t\t\t\t\t{" );
sb.AppendLine( "\t\t\t\t\t\t_class = \"RenderMeshFile\"" );
sb.AppendLine( $"\t\t\t\t\t\tfilename = \"{meshRelativePath}\"" );
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 = {FormatNumber( options.ImportScale )}" );
sb.AppendLine( $"\t\t\t\t\t\talign_origin_x_type = \"{options.AlignOriginX.ToKv3Value()}\"" );
sb.AppendLine( $"\t\t\t\t\t\talign_origin_y_type = \"{options.AlignOriginY.ToKv3Value()}\"" );
sb.AppendLine( $"\t\t\t\t\t\talign_origin_z_type = \"{options.AlignOriginZ.ToKv3Value()}\"" );
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}" );
sb.AppendLine( "\t\t\t\t\t}," );
sb.AppendLine( "\t\t\t\t]" );
sb.AppendLine( "\t\t\t}," );
}
private static void AppendPhysicsShapeList( StringBuilder sb, string meshRelativePath, MeshToModelOptions options )
{
if ( options.Collision == CollisionMode.None )
return;
sb.AppendLine( "\t\t\t{" );
sb.AppendLine( "\t\t\t\t_class = \"PhysicsShapeList\"" );
sb.AppendLine( "\t\t\t\tchildren = " );
sb.AppendLine( "\t\t\t\t[" );
sb.AppendLine( "\t\t\t\t\t{" );
switch ( options.Collision )
{
case CollisionMode.Mesh:
sb.AppendLine( "\t\t\t\t\t\t_class = \"PhysicsMeshFromRender\"" );
sb.AppendLine( "\t\t\t\t\t\tparent_bone = \"\"" );
sb.AppendLine( "\t\t\t\t\t\tsurface_prop = \"default\"" );
sb.AppendLine( "\t\t\t\t\t\tcollision_tags = \"solid\"" );
sb.AppendLine( $"\t\t\t\t\t\talign_origin_x_type = \"{options.AlignOriginX.ToKv3Value()}\"" );
sb.AppendLine( $"\t\t\t\t\t\talign_origin_y_type = \"{options.AlignOriginY.ToKv3Value()}\"" );
sb.AppendLine( $"\t\t\t\t\t\talign_origin_z_type = \"{options.AlignOriginZ.ToKv3Value()}\"" );
break;
case CollisionMode.File:
sb.AppendLine( "\t\t\t\t\t\t_class = \"PhysicsHullFile\"" );
sb.AppendLine( $"\t\t\t\t\t\tfilename = \"{meshRelativePath}\"" );
sb.AppendLine( $"\t\t\t\t\t\timport_scale = {FormatNumber( options.ImportScale )}" );
sb.AppendLine( "\t\t\t\t\t\tparent_bone = \"\"" );
sb.AppendLine( "\t\t\t\t\t\tsurface_prop = \"default\"" );
sb.AppendLine( "\t\t\t\t\t\tcollision_tags = \"solid\"" );
sb.AppendLine( $"\t\t\t\t\t\talign_origin_x_type = \"{options.AlignOriginX.ToKv3Value()}\"" );
sb.AppendLine( $"\t\t\t\t\t\talign_origin_y_type = \"{options.AlignOriginY.ToKv3Value()}\"" );
sb.AppendLine( $"\t\t\t\t\t\talign_origin_z_type = \"{options.AlignOriginZ.ToKv3Value()}\"" );
sb.AppendLine( "\t\t\t\t\t\tfaceMergeAngle = 20.0" );
sb.AppendLine( "\t\t\t\t\t\tmaxHullVertices = 32" );
break;
case CollisionMode.Hull:
default:
sb.AppendLine( "\t\t\t\t\t\t_class = \"PhysicsHullFromRender\"" );
sb.AppendLine( "\t\t\t\t\t\tparent_bone = \"\"" );
sb.AppendLine( "\t\t\t\t\t\tsurface_prop = \"default\"" );
sb.AppendLine( "\t\t\t\t\t\tcollision_tags = \"solid\"" );
sb.AppendLine( $"\t\t\t\t\t\talign_origin_x_type = \"{options.AlignOriginX.ToKv3Value()}\"" );
sb.AppendLine( $"\t\t\t\t\t\talign_origin_y_type = \"{options.AlignOriginY.ToKv3Value()}\"" );
sb.AppendLine( $"\t\t\t\t\t\talign_origin_z_type = \"{options.AlignOriginZ.ToKv3Value()}\"" );
sb.AppendLine( "\t\t\t\t\t\tfaceMergeAngle = 20.0" );
sb.AppendLine( "\t\t\t\t\t\tmaxHullVertices = 32" );
sb.AppendLine( "\t\t\t\t\t\thull_mode = \"HullPerElement\"" );
break;
}
sb.AppendLine( "\t\t\t\t\t}," );
sb.AppendLine( "\t\t\t\t]" );
sb.AppendLine( "\t\t\t}," );
}
private static string FormatNumber( float value )
{
return value.ToString( "0.0########", CultureInfo.InvariantCulture );
}
}