A reader for Source engine VVD vertex files. It parses VVD headers, handles fixup tables to reconstruct LOD0 vertex order, reads vertex attributes (position, normal, UV) and bone weights, and returns a VvdFile containing a vertex list.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace GmodAssetImporter;
public sealed class VvdFile
{
public int Checksum;
public int LodCount;
public List<MeshVertex> Vertices { get; } = new();
}
public static class VvdReader
{
const int IdStudioVertex = 0x56534449; // "IDSV"
public static VvdFile Load( string path ) => Load( File.ReadAllBytes( path ) );
public static VvdFile Load( byte[] data )
{
using var ms = new MemoryStream( data );
using var br = new BinaryReader( ms );
var id = br.ReadInt32();
if ( id != IdStudioVertex )
throw new InvalidDataException( $"Invalid VVD id 0x{id:X8}" );
var version = br.ReadInt32();
if ( version != 4 )
throw new InvalidDataException( $"Unsupported VVD version {version}" );
var checksum = br.ReadInt32();
var numLods = br.ReadInt32();
var numLodVertexes = new int[8];
for ( var i = 0; i < 8; i++ )
numLodVertexes[i] = br.ReadInt32();
var numFixups = br.ReadInt32();
var fixupTableStart = br.ReadInt32();
var vertexDataStart = br.ReadInt32();
br.ReadInt32(); // tangentDataStart
var vertexCount = numLodVertexes[0];
var file = new VvdFile { Checksum = checksum, LodCount = numLods };
if ( numFixups == 0 )
{
br.BaseStream.Position = vertexDataStart;
for ( var i = 0; i < vertexCount; i++ )
file.Vertices.Add( ReadVertex( br ) );
}
else
{
// Apply fixup table to reconstruct LOD0 order
var raw = new List<MeshVertex>( vertexCount );
br.BaseStream.Position = vertexDataStart;
// After fixups, vertex buffer still holds all vertices; fixups remap
var totalVerts = 0;
for ( var i = 0; i < numLods; i++ )
totalVerts = Math.Max( totalVerts, numLodVertexes[i] );
// Read full vertex buffer — size is typically numLodVertexes[0] before fixup apply,
// but with fixups the on-disk count is the sum of fixup ranges.
var maxVert = 0;
br.BaseStream.Position = fixupTableStart;
var fixups = new List<(int lod, int source, int count)>();
for ( var i = 0; i < numFixups; i++ )
{
var lod = br.ReadInt32();
var source = br.ReadInt32();
var count = br.ReadInt32();
fixups.Add( (lod, source, count) );
maxVert = Math.Max( maxVert, source + count );
}
br.BaseStream.Position = vertexDataStart;
var buffer = new MeshVertex[maxVert];
for ( var i = 0; i < maxVert; i++ )
buffer[i] = ReadVertex( br );
// A fixup tagged lod N is used by every LOD <= N, so LOD0 takes all of them.
foreach ( var fix in fixups )
{
if ( fix.lod < 0 ) continue;
for ( var i = 0; i < fix.count; i++ )
file.Vertices.Add( buffer[fix.source + i] );
}
if ( file.Vertices.Count == 0 )
{
// Fallback: use buffer as-is
file.Vertices.AddRange( buffer );
}
}
return file;
}
static MeshVertex ReadVertex( BinaryReader br )
{
// mstudiovertex_t / mstudioboneweight_t
var w0 = br.ReadSingle();
var w1 = br.ReadSingle();
var w2 = br.ReadSingle();
var b0 = br.ReadByte();
var b1 = br.ReadByte();
var b2 = br.ReadByte();
var numBones = br.ReadByte();
var pos = BinaryUtil.ReadVec3( br );
var normal = BinaryUtil.ReadVec3( br );
var u = br.ReadSingle();
var v = br.ReadSingle();
var vertex = new MeshVertex
{
Position = pos,
Normal = normal,
Uv = new Vec2( u, v )
};
AddWeight( vertex, b0, w0, numBones, 0 );
AddWeight( vertex, b1, w1, numBones, 1 );
AddWeight( vertex, b2, w2, numBones, 2 );
NormalizeWeights( vertex );
return vertex;
}
static void AddWeight( MeshVertex v, byte bone, float weight, int numBones, int index )
{
if ( index >= numBones || weight <= 0f ) return;
v.Weights.Add( new BoneWeight { Bone = bone, Weight = weight } );
}
static void NormalizeWeights( MeshVertex v )
{
if ( v.Weights.Count == 0 )
{
v.Weights.Add( new BoneWeight { Bone = 0, Weight = 1 } );
return;
}
// Cap at 4 influences
if ( v.Weights.Count > 4 )
{
v.Weights.Sort( ( a, b ) => b.Weight.CompareTo( a.Weight ) );
v.Weights.RemoveRange( 4, v.Weights.Count - 4 );
}
var sum = v.Weights.Sum( w => w.Weight );
if ( sum <= 0.0001f )
{
v.Weights.Clear();
v.Weights.Add( new BoneWeight { Bone = 0, Weight = 1 } );
return;
}
foreach ( var w in v.Weights )
w.Weight /= sum;
}
}