Reader for Source engine VTX files used by the editor conversion pipeline. It parses VTX version 7 binary data into a hierarchy of VtxFile -> BodyParts -> Models -> Lods -> Meshes, and fills mesh index lists by reading strip-group index buffers and mapping them to model vertex indices.
using System;
using System.Collections.Generic;
using System.IO;
namespace GmodAssetImporter;
public sealed class VtxFile
{
public List<VtxBodyPart> BodyParts { get; } = new();
}
public sealed class VtxBodyPart
{
public List<VtxModel> Models { get; } = new();
}
public sealed class VtxModel
{
public List<VtxLod> Lods { get; } = new();
}
public sealed class VtxLod
{
public List<VtxMesh> Meshes { get; } = new();
}
public sealed class VtxMesh
{
public List<int> Indices { get; } = new(); // triangle list indices into model vertex range
}
/// <summary>
/// VTX reader — uses the strip-group index buffer as a triangle list (same as the first
/// working import). Fancy strip expansion proved less reliable on INS2 weapon VTXs.
/// </summary>
public static class VtxReader
{
public static string FindVtxPath( string mdlPath )
{
var dir = Path.GetDirectoryName( mdlPath )!;
var name = Path.GetFileNameWithoutExtension( mdlPath );
var candidates = new[]
{
Path.Combine( dir, name + ".dx90.vtx" ),
Path.Combine( dir, name + ".dx80.vtx" ),
Path.Combine( dir, name + ".sw.vtx" ),
Path.Combine( dir, name + ".vtx" )
};
return PathResolve.FindExistingFile( candidates );
}
public static VtxFile Load( string path ) => Load( File.ReadAllBytes( path ) );
public static VtxFile Load( byte[] data )
{
using var ms = new MemoryStream( data );
using var br = new BinaryReader( ms );
var version = br.ReadInt32();
if ( version != 7 )
throw new InvalidDataException( $"Unsupported VTX version {version}" );
br.ReadInt32(); // vertCacheSize
br.ReadUInt16(); // maxBonesPerStrip
br.ReadUInt16(); // maxBonesPerTri
br.ReadInt32(); // maxBonesPerVert
br.ReadInt32(); // checkSum
br.ReadInt32(); // numLODs
var materialReplacementListOffset = br.ReadInt32();
var numBodyParts = br.ReadInt32();
var bodyPartOffset = br.ReadInt32();
_ = materialReplacementListOffset;
var file = new VtxFile();
for ( var bp = 0; bp < numBodyParts; bp++ )
{
var bpPos = bodyPartOffset + bp * 8;
br.BaseStream.Position = bpPos;
var numModels = br.ReadInt32();
var modelOffset = br.ReadInt32();
var body = new VtxBodyPart();
for ( var m = 0; m < numModels; m++ )
{
var modelPos = bpPos + modelOffset + m * 8;
br.BaseStream.Position = modelPos;
var numLods = br.ReadInt32();
var lodOffset = br.ReadInt32();
var model = new VtxModel();
if ( numLods > 0 )
{
var lodPos = modelPos + lodOffset;
br.BaseStream.Position = lodPos;
var numMeshes = br.ReadInt32();
var meshOffset = br.ReadInt32();
br.ReadSingle(); // switchPoint
var lod = new VtxLod();
for ( var me = 0; me < numMeshes; me++ )
{
var meshPos = lodPos + meshOffset + me * 9;
br.BaseStream.Position = meshPos;
var numStripGroups = br.ReadInt32();
var stripGroupOffset = br.ReadInt32();
br.ReadByte();
var mesh = new VtxMesh();
for ( var sg = 0; sg < numStripGroups; sg++ )
{
var sgPos = meshPos + stripGroupOffset + sg * 25;
br.BaseStream.Position = sgPos;
var numVerts = br.ReadInt32();
var vertOffset = br.ReadInt32();
var numIndices = br.ReadInt32();
var indexOffset = br.ReadInt32();
br.ReadInt32(); // numStrips
br.ReadInt32(); // stripOffset
br.ReadByte(); // flags
var vertMap = new int[numVerts];
br.BaseStream.Position = sgPos + vertOffset;
for ( var v = 0; v < numVerts; v++ )
{
br.ReadByte(); br.ReadByte(); br.ReadByte();
br.ReadByte();
vertMap[v] = br.ReadUInt16();
br.ReadByte(); br.ReadByte(); br.ReadByte();
}
br.BaseStream.Position = sgPos + indexOffset;
for ( var i = 0; i < numIndices; i++ )
{
var idx = br.ReadUInt16();
if ( idx < vertMap.Length )
mesh.Indices.Add( vertMap[idx] );
}
}
lod.Meshes.Add( mesh );
}
model.Lods.Add( lod );
}
body.Models.Add( model );
}
file.BodyParts.Add( body );
}
return file;
}
}