Editor/WeaponImporter/Tool/Engine/VmdlReader.cs
using Sandbox;
using WeaponImporter.Core.Geometry;
using WeaponImporter.Core.Maths;
using WeaponImporter.Core.Rig;
using WeaponImporter.Core.Weapon;
using N = System.Numerics;
namespace WeaponImporter.Tool;
/// <summary>
/// Reads an existing s&box model as a weapon source: bind skeleton, render triangles (each
/// vertex assigned to its nearest bone, since the engine exposes no skin weights) and every
/// sequence sampled at 30 fps.
/// </summary>
public static class VmdlReader
{
public static WeaponAsset Read( string path, string name )
{
var model = Model.Load( path );
if ( model is null || model.IsError )
throw new InvalidOperationException( $"'{path}' is not a loadable model." );
var skeleton = model.BoneCount > 0 ? CharacterLibrary.SkeletonOf( model ) : WeaponAsset.SingleBone();
var verts = model.GetVertices() ?? Array.Empty<Vertex>();
var raw = model.GetIndices() ?? Array.Empty<uint>();
if ( verts.Length == 0 || raw.Length < 3 )
throw new InvalidOperationException( $"'{path}' has no readable render mesh." );
var positions = verts.Select( v => v.Position.ToCore() ).ToArray();
var indices = raw.Take( raw.Length - raw.Length % 3 ).Select( i => (int)i ).Where( i => i < positions.Length ).ToArray();
if ( indices.Length % 3 != 0 )
indices = indices.Take( indices.Length - indices.Length % 3 ).ToArray();
// Nearest bone by distance to the bone segment (bone origin to its first child).
var boneSegments = Enumerable.Range( 0, skeleton.Count ).Select( b =>
{
var head = skeleton.RestWorld[b].Pos;
var child = Enumerable.Range( 0, skeleton.Count ).FirstOrDefault( c => skeleton[c].ParentIndex == b, -1 );
var tail = child >= 0 ? skeleton.RestWorld[child].Pos : head;
return (head, tail);
} ).ToArray();
var vertexBone = positions.Select( p =>
{
var best = 0;
var bestD = float.MaxValue;
for ( var b = 0; b < boneSegments.Length; b++ )
{
var (h, t) = boneSegments[b];
var ab = t - h;
var u = ab.LengthSquared() > 1e-8f ? Math.Clamp( N.Vector3.Dot( p - h, ab ) / ab.LengthSquared(), 0f, 1f ) : 0f;
var d = N.Vector3.DistanceSquared( p, h + ab * u );
if ( d < bestD ) { bestD = d; best = b; }
}
return best;
} ).ToArray();
var clips = new List<Clip>();
var world = new SceneWorld();
try
{
var sm = new SceneModel( world, model, Transform.Zero ) { UseAnimGraph = false };
foreach ( var seq in model.AnimationNames )
{
sm.CurrentSequence.Name = seq;
sm.Update( 0 );
var duration = sm.CurrentSequence.Duration;
var frames = Math.Clamp( (int)MathF.Round( duration * 30f ) + 1, 2, 3000 );
var clip = new Clip( seq, 30f, false );
for ( var f = 0; f < frames; f++ )
{
sm.CurrentSequence.Time = duration * f / (frames - 1);
sm.Update( 0 );
var locals = new XForm[skeleton.Count];
for ( var b = 0; b < skeleton.Count; b++ )
locals[b] = sm.GetParentSpaceBone( b ).ToCore();
clip.Frames.Add( locals );
}
clips.Add( clip );
}
sm.Delete();
}
finally
{
world.Delete();
}
var attachments = model.Attachments?.All?.Select( a => new SourceAttachment( a.Name, a.Bone?.Name ?? skeleton[0].Name, a.LocalTransform.ToCore() ) ).ToList() ?? new List<SourceAttachment>();
return new WeaponAsset
{
Name = name,
Kind = SourceKind.Vmdl,
SourcePath = path,
Skeleton = skeleton,
Mesh = new TriMesh( positions, indices, vertexBone ),
Clips = clips,
Attachments = attachments,
};
}
}