Editor/Conversion/Writers/SmdWriter.cs

Editor utility that writes SMD files for meshes and skeletal animations. It outputs nodes, skeleton bind pose, triangles and per-frame animation bone transforms to disk in Valve SMD text format.

File Access
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;

namespace GmodAssetImporter;

public static class SmdWriter
{
	static readonly Encoding Utf8NoBom = new UTF8Encoding( false );

	public static void WriteMesh( string path, List<BoneData> bones, MeshExport mesh )
	{
		Directory.CreateDirectory( Path.GetDirectoryName( path )! );
		using var sw = new StreamWriter( path, false, Utf8NoBom );
		sw.WriteLine( "version 1" );
		WriteNodes( sw, bones );
		WriteSkeletonBind( sw, bones );
		sw.WriteLine( "triangles" );
		foreach ( var tri in mesh.Triangles )
		{
			sw.WriteLine( tri.MaterialName );
			WriteVert( sw, mesh.Vertices[tri.A] );
			WriteVert( sw, mesh.Vertices[tri.B] );
			WriteVert( sw, mesh.Vertices[tri.C] );
		}
		sw.WriteLine( "end" );
	}

	/// <summary>
	/// Write a simple single-frame (or multi-frame) skeletal animation SMD using per-frame bone transforms.
	/// </summary>
	public static void WriteAnimation( string path, List<BoneData> bones, List<List<BoneData>> frames, float /*fps*/ _ )
	{
		Directory.CreateDirectory( Path.GetDirectoryName( path )! );
		using var sw = new StreamWriter( path, false, Utf8NoBom );
		sw.WriteLine( "version 1" );
		WriteNodes( sw, bones );
		sw.WriteLine( "skeleton" );
		for ( var f = 0; f < frames.Count; f++ )
		{
			sw.WriteLine( $"time {f}" );
			var frame = frames[f];
			for ( var i = 0; i < bones.Count; i++ )
			{
				var b = i < frame.Count ? frame[i] : bones[i];
				sw.WriteLine( string.Format( CultureInfo.InvariantCulture,
					"{0} {1:0.######} {2:0.######} {3:0.######} {4:0.######} {5:0.######} {6:0.######}",
					i, b.Position.X, b.Position.Y, b.Position.Z, b.Rotation.X, b.Rotation.Y, b.Rotation.Z ) );
			}
		}
		sw.WriteLine( "end" );
	}

	static void WriteNodes( StreamWriter sw, List<BoneData> bones )
	{
		sw.WriteLine( "nodes" );
		for ( var i = 0; i < bones.Count; i++ )
		{
			var parent = bones[i].Parent;
			sw.WriteLine( $"{i} \"{Escape( bones[i].Name )}\" {parent}" );
		}
		sw.WriteLine( "end" );
	}

	static void WriteSkeletonBind( StreamWriter sw, List<BoneData> bones )
	{
		sw.WriteLine( "skeleton" );
		sw.WriteLine( "time 0" );
		for ( var i = 0; i < bones.Count; i++ )
		{
			var b = bones[i];
			sw.WriteLine( string.Format( CultureInfo.InvariantCulture,
				"{0} {1:0.######} {2:0.######} {3:0.######} {4:0.######} {5:0.######} {6:0.######}",
				i, b.Position.X, b.Position.Y, b.Position.Z, b.Rotation.X, b.Rotation.Y, b.Rotation.Z ) );
		}
		sw.WriteLine( "end" );
	}

	static void WriteVert( StreamWriter sw, MeshVertex v )
	{
		var links = Math.Min( 4, v.Weights.Count );
		var parent = links > 0 ? v.Weights[0].Bone : 0;
		var sb = new StringBuilder();
		sb.Append( string.Format( CultureInfo.InvariantCulture,
			"{0} {1:0.######} {2:0.######} {3:0.######} {4:0.######} {5:0.######} {6:0.######} {7:0.######} {8:0.######} {9}",
			parent,
			v.Position.X, v.Position.Y, v.Position.Z,
			v.Normal.X, v.Normal.Y, v.Normal.Z,
			v.Uv.X, v.Uv.Y,
			links ) );

		for ( var i = 0; i < links; i++ )
		{
			sb.Append( ' ' );
			sb.Append( v.Weights[i].Bone );
			sb.Append( ' ' );
			sb.Append( v.Weights[i].Weight.ToString( "0.######", CultureInfo.InvariantCulture ) );
		}

		sw.WriteLine( sb.ToString() );
	}

	static string Escape( string s ) => (s ?? "").Replace( "\"", "" );
}