diff --git a/engine/Mounting/Sandbox.Mounting.HL2/Assembly.cs b/engine/Mounting/Sandbox.Mounting.HL2/Assembly.cs
new file mode 100644
index 000000000..486c074e0
--- /dev/null
+++ b/engine/Mounting/Sandbox.Mounting.HL2/Assembly.cs
@@ -0,0 +1,5 @@
+global using Sandbox.Mounting;
+global using System.Collections.Generic;
+global using System.IO;
+global using System.Linq;
+global using System.Threading.Tasks;
diff --git a/engine/Mounting/Sandbox.Mounting.HL2/HL2Mount.cs b/engine/Mounting/Sandbox.Mounting.HL2/HL2Mount.cs
new file mode 100644
index 000000000..3600627cc
--- /dev/null
+++ b/engine/Mounting/Sandbox.Mounting.HL2/HL2Mount.cs
@@ -0,0 +1,179 @@
+using System;
+using ValvePak;
+
+///
+/// A mounting implementation for Half-Life 2 and Episodes + Lost Coast
+///
+public partial class HL2Mount : BaseGameMount
+{
+ public override string Ident => "hl2";
+ public override string Title => "Half-Life 2";
+
+ private const long HL2AppId = 220;
+
+ private readonly List _packages = [];
+ private readonly List _gameDirs = [];
+
+ protected override void Initialize( InitializeContext context )
+ {
+ if ( !context.IsAppInstalled( HL2AppId ) )
+ return;
+
+ var baseDir = context.GetAppDirectory( HL2AppId );
+ if ( string.IsNullOrEmpty( baseDir ) || !System.IO.Directory.Exists( baseDir ) )
+ return;
+
+ AddGameDir( baseDir, "hl2" );
+ AddGameDir( baseDir, "episodic" );
+ AddGameDir( baseDir, "ep2" );
+ AddGameDir( baseDir, "lostcoast" );
+ AddGameDir( baseDir, "hl2_complete" );
+
+ IsInstalled = _gameDirs.Count > 0;
+ }
+
+ private void AddGameDir( string baseDir, string subDir )
+ {
+ var path = Path.Combine( baseDir, subDir );
+ if ( System.IO.Directory.Exists( path ) )
+ _gameDirs.Add( path );
+ }
+
+ protected override Task Mount( MountContext context )
+ {
+ if ( !IsInstalled )
+ return Task.CompletedTask;
+
+ foreach ( var gameDir in _gameDirs )
+ MountGameDirectory( context, gameDir );
+
+ IsMounted = true;
+ return Task.CompletedTask;
+ }
+
+ private void MountGameDirectory( MountContext context, string gameDir )
+ {
+ foreach ( var vpkPath in System.IO.Directory.EnumerateFiles( gameDir, "*_dir.vpk" ) )
+ {
+ var package = new VpkArchive();
+ package.Read( vpkPath );
+ _packages.Add( package );
+ MountVpkContents( context, package );
+ }
+
+ MountLooseFiles( context, gameDir );
+ }
+
+ private void MountVpkContents( MountContext context, VpkArchive package )
+ {
+ foreach ( var (extension, entries) in package.Entries )
+ {
+ var ext = extension.ToLowerInvariant();
+
+ foreach ( var entry in entries )
+ {
+ var fullPath = entry.GetFullPath();
+ var path = fullPath[..^(ext.Length + 1)];
+
+ switch ( ext )
+ {
+ case "vtf":
+ context.Add( ResourceType.Texture, path, new HL2Texture( package, entry ) );
+ break;
+ case "vmt":
+ context.Add( ResourceType.Material, path, new HL2Material( package, entry ) );
+ break;
+ case "mdl":
+ context.Add( ResourceType.Model, path, new HL2Model( package, entry ) );
+ break;
+ case "wav":
+ case "mp3":
+ context.Add( ResourceType.Sound, path, new HL2Sound( package, entry ) );
+ break;
+ }
+ }
+ }
+ }
+
+ private void MountLooseFiles( MountContext context, string gameDir )
+ {
+ MountLooseFilesOfType( context, gameDir, "materials", "*.vtf", ResourceType.Texture,
+ filePath => new HL2Texture( filePath ) );
+ MountLooseFilesOfType( context, gameDir, "materials", "*.vmt", ResourceType.Material,
+ filePath => new HL2Material( filePath ) );
+ MountLooseFilesOfType( context, gameDir, "models", "*.mdl", ResourceType.Model,
+ filePath => new HL2Model( filePath ) );
+ MountLooseFilesOfType( context, gameDir, "sound", "*.wav", ResourceType.Sound,
+ filePath => new HL2Sound( filePath ) );
+ MountLooseFilesOfType( context, gameDir, "sound", "*.mp3", ResourceType.Sound,
+ filePath => new HL2Sound( filePath ) );
+ }
+
+ private void MountLooseFilesOfType( MountContext context, string gameDir, string subDir, string pattern,
+ ResourceType resourceType, Func> createLoader )
+ {
+ var dir = Path.Combine( gameDir, subDir );
+ if ( !System.IO.Directory.Exists( dir ) )
+ return;
+
+ foreach ( var filePath in System.IO.Directory.EnumerateFiles( dir, pattern, SearchOption.AllDirectories ) )
+ {
+ var relativePath = Path.GetRelativePath( gameDir, filePath ).Replace( '\\', '/' );
+ var resourcePath = relativePath[..^Path.GetExtension( relativePath ).Length];
+ context.Add( resourceType, resourcePath, createLoader( filePath ) );
+ }
+ }
+
+ internal bool FileExists( string path )
+ {
+ path = path.ToLowerInvariant().Replace( '\\', '/' );
+
+ foreach ( var package in _packages )
+ {
+ if ( package.FindEntry( path ) != null )
+ return true;
+ }
+
+ foreach ( var dir in _gameDirs )
+ {
+ var fullPath = Path.Combine( dir, path );
+ if ( File.Exists( fullPath ) )
+ return true;
+ }
+
+ return false;
+ }
+
+ internal byte[] ReadFile( string path )
+ {
+ path = path.ToLowerInvariant().Replace( '\\', '/' );
+
+ foreach ( var package in _packages )
+ {
+ var entry = package.FindEntry( path );
+ if ( entry != null )
+ {
+ package.ReadEntry( entry, out var data );
+ return data;
+ }
+ }
+
+ foreach ( var dir in _gameDirs )
+ {
+ var fullPath = Path.Combine( dir, path );
+ if ( File.Exists( fullPath ) )
+ return File.ReadAllBytes( fullPath );
+ }
+
+ return null;
+ }
+
+ protected override void Shutdown()
+ {
+ foreach ( var package in _packages )
+ package.Dispose();
+
+ _packages.Clear();
+ _gameDirs.Clear();
+ }
+}
diff --git a/engine/Mounting/Sandbox.Mounting.HL2/Resource/HL2Material.cs b/engine/Mounting/Sandbox.Mounting.HL2/Resource/HL2Material.cs
new file mode 100644
index 000000000..95e669b42
--- /dev/null
+++ b/engine/Mounting/Sandbox.Mounting.HL2/Resource/HL2Material.cs
@@ -0,0 +1,276 @@
+using Sandbox;
+using ValvePak;
+using ValveKeyValue;
+using System;
+
+internal class HL2Material : ResourceLoader
+{
+ private readonly VpkArchive _package;
+ private readonly VpkEntry _entry;
+ private readonly string _filePath;
+
+ public HL2Material( VpkArchive package, VpkEntry entry )
+ {
+ _package = package;
+ _entry = entry;
+ }
+
+ public HL2Material( string filePath )
+ {
+ _filePath = filePath;
+ }
+
+ protected override object Load()
+ {
+ byte[] data;
+ if ( _package != null )
+ {
+ _package.ReadEntry( _entry, out data );
+ }
+ else
+ {
+ data = File.ReadAllBytes( _filePath );
+ }
+ return VmtLoader.Load( data, Path );
+ }
+}
+
+internal static class VmtLoader
+{
+ public static Material Load( byte[] data, string path )
+ {
+ var vmt = KeyValues.Parse( data );
+ if ( vmt == null )
+ {
+ return null;
+ }
+
+ // Merge conditional blocks (>=dx90, etc.)
+ vmt.MergeConditionals();
+
+ var shaderName = vmt.Name?.ToLowerInvariant();
+ var material = Material.Create( path, shaderName );
+
+ SetShaderFeatures( material, shaderName, vmt );
+
+ foreach ( var prop in vmt.Children )
+ {
+ ApplyMaterialParameter( material, prop.Name, prop.Value );
+ }
+
+ // Special case: phong exponent from texture
+ if ( shaderName == "vertexlitgeneric" && vmt.GetBool( "$phong" ) )
+ {
+ if ( vmt["$phongexponenttexture"] != null && vmt["$phongexponent"] == null )
+ {
+ material.Set( "g_flPhongExponent", -1.0f );
+ }
+ }
+
+ return material;
+ }
+
+ private static void SetShaderFeatures( Material material, string shaderName, KeyValues vmt )
+ {
+ if ( vmt.GetBool( "$translucent" ) )
+ material.SetFeature( "F_TRANSLUCENT", 1 );
+
+ if ( vmt.GetBool( "$alphatest" ) )
+ material.SetFeature( "F_ALPHA_TEST", 1 );
+
+ if ( vmt.GetBool( "$additive" ) )
+ {
+ material.SetFeature( "F_TRANSLUCENT", 1 );
+ material.SetFeature( "F_ADDITIVE_BLEND", 1 );
+ }
+
+ if ( vmt.GetBool( "$nocull" ) )
+ material.Attributes.SetCombo( "D_NO_CULLING", 1 );
+
+ var bumpmap = vmt.GetString( "$bumpmap" ) ?? vmt.GetString( "$normalmap" );
+ if ( TextureExists( bumpmap ) )
+ material.SetFeature( "F_BUMPMAP", 1 );
+
+ if ( vmt.GetBool( "$selfillum" ) )
+ material.SetFeature( "F_SELFILLUM", 1 );
+
+ if ( TextureExists( vmt.GetString( "$detail" ) ) )
+ material.SetFeature( "F_DETAIL", 1 );
+
+ var envmap = vmt.GetString( "$envmap" );
+ if ( !string.IsNullOrEmpty( envmap ) && envmap != "0" && !envmap.Equals( "env_cubemap", StringComparison.OrdinalIgnoreCase ) )
+ {
+ if ( TextureExists( envmap ) )
+ material.SetFeature( "F_ENVMAP", 1 );
+ }
+
+ switch ( shaderName )
+ {
+ case "vertexlitgeneric":
+ if ( vmt.GetBool( "$phong" ) )
+ material.SetFeature( "F_PHONG", 1 );
+ if ( vmt.GetBool( "$rimlight" ) )
+ material.SetFeature( "F_RIMLIGHT", 1 );
+ if ( vmt.GetBool( "$halflambert" ) )
+ material.SetFeature( "F_HALFLAMBERT", 1 );
+ if ( TextureExists( vmt.GetString( "$lightwarptexture" ) ) )
+ material.SetFeature( "F_LIGHTWARP", 1 );
+ if ( TextureExists( vmt.GetString( "$phongwarptexture" ) ) )
+ material.SetFeature( "F_PHONGWARP", 1 );
+ break;
+
+ case "teeth":
+ break;
+
+ case "lightmappedgeneric":
+ if ( TextureExists( vmt.GetString( "$basetexture2" ) ) )
+ material.SetFeature( "F_BLEND", 1 );
+ if ( vmt.GetBool( "$seamless_base" ) || vmt["$seamless_scale"] != null )
+ material.SetFeature( "F_SEAMLESS", 1 );
+ break;
+
+ case "unlitgeneric":
+ if ( vmt.GetBool( "$vertexcolor" ) )
+ material.SetFeature( "F_VERTEX_COLOR", 1 );
+ if ( vmt.GetBool( "$vertexalpha" ) )
+ material.SetFeature( "F_VERTEX_ALPHA", 1 );
+ break;
+ }
+ }
+
+ private static bool TextureExists( string textureName )
+ {
+ if ( string.IsNullOrWhiteSpace( textureName ) )
+ return false;
+
+ textureName = textureName.Replace( '\\', '/' );
+ var path = $"mount://hl2/materials/{textureName}.vtex";
+ var texture = Texture.Load( path, false );
+ return texture.IsValid() && !texture.IsError;
+ }
+
+ private static void ApplyMaterialParameter( Material material, string key, string value )
+ {
+ if ( string.IsNullOrEmpty( key ) || string.IsNullOrEmpty( value ) )
+ return;
+
+ key = key.ToLowerInvariant();
+
+ switch ( key )
+ {
+ // Base Textures
+ case "$basetexture": LoadAndSetTexture( material, value, "g_tColor" ); break;
+ case "$basetexture2": LoadAndSetTexture( material, value, "g_tColor2" ); break;
+ case "$bumpmap":
+ case "$normalmap": LoadAndSetTexture( material, value, "g_tNormal" ); break;
+ case "$bumpmap2": LoadAndSetTexture( material, value, "g_tNormal2" ); break;
+ case "$detail": LoadAndSetTexture( material, value, "g_tDetail" ); break;
+ case "$envmapmask": LoadAndSetTexture( material, value, "g_tEnvMapMask" ); break;
+ case "$phongexponenttexture": LoadAndSetTexture( material, value, "g_tPhongExponent" ); break;
+ case "$lightwarptexture": LoadAndSetTexture( material, value, "g_tLightWarp" ); break;
+ case "$phongwarptexture": LoadAndSetTexture( material, value, "g_tPhongWarp" ); break;
+ case "$selfillummask": LoadAndSetTexture( material, value, "g_tSelfIllumMask" ); break;
+ case "$iris": LoadAndSetTexture( material, value, "g_tIris" ); break;
+
+ // Environment Map (special handling)
+ case "$envmap":
+ if ( value != "0" && !value.Equals( "env_cubemap", StringComparison.OrdinalIgnoreCase ) )
+ LoadAndSetTexture( material, value, "g_tEnvMap" );
+ break;
+
+ // Color and Alpha
+ case "$color":
+ case "$color2": SetVector( material, "g_vColorTint", value ); break;
+ case "$alpha": SetFloat( material, "g_flAlpha", value ); break;
+ case "$alphatestreference": SetFloat( material, "g_flAlphaTestReference", value ); break;
+
+ // Detail Texture
+ case "$detailscale": SetFloat( material, "g_flDetailScale", value ); break;
+ case "$detailblendfactor": SetFloat( material, "g_flDetailBlendFactor", value ); break;
+ case "$detailblendmode": SetInt( material, "g_nDetailBlendMode", value ); break;
+ case "$detailtint": SetVector( material, "g_vDetailTint", value ); break;
+
+ // Phong Specular
+ case "$phongexponent": SetFloat( material, "g_flPhongExponent", value ); break;
+ case "$phongboost": SetFloat( material, "g_flPhongBoost", value ); break;
+ case "$phongtint": SetVector( material, "g_vPhongTint", value ); break;
+ case "$phongfresnelranges": SetVector( material, "g_vPhongFresnelRanges", value ); break;
+ case "$phongalbedotint": SetFloat( material, "g_flPhongAlbedoTint", value ); break;
+ case "$basemapalphaphongmask": SetBool( material, "g_nBaseMapAlphaPhongMask", value ); break;
+ case "$invertphongmask": SetBool( material, "g_nInvertPhongMask", value ); break;
+
+ // Self Illumination
+ case "$selfillumtint": SetVector( material, "g_vSelfIllumTint", value ); break;
+
+ // Rim Lighting
+ case "$rimlightexponent": SetFloat( material, "g_flRimLightExponent", value ); break;
+ case "$rimlightboost": SetFloat( material, "g_flRimLightBoost", value ); break;
+ case "$rimmask": SetBool( material, "g_nRimMask", value ); break;
+
+ // Environment Map Parameters
+ case "$envmaptint": SetVector( material, "g_vEnvMapTint", value ); break;
+ case "$envmapcontrast": SetFloat( material, "g_flEnvMapContrast", value ); break;
+ case "$envmapsaturation": SetFloat( material, "g_flEnvMapSaturation", value ); break;
+ case "$envmapfresnel": SetFloat( material, "g_flEnvMapFresnel", value ); break;
+ case "$basealphaenvmapmask": SetBool( material, "g_nBaseAlphaEnvMapMask", value ); break;
+ case "$normalmapalphaenvmapmask": SetBool( material, "g_nNormalMapAlphaEnvMapMask", value ); break;
+ case "$fresnelreflection": SetFloat( material, "g_flFresnelReflection", value ); break;
+
+ // Seamless Mapping
+ case "$seamless_scale": SetFloat( material, "g_flSeamlessScale", value ); break;
+
+ // Teeth/Eyes
+ case "$forward": SetVector( material, "g_vForward", value ); break;
+ case "$illumfactor": SetFloat( material, "g_flIllumFactor", value ); break;
+ }
+ }
+
+ private static void LoadAndSetTexture( Material material, string textureName, string uniformName )
+ {
+ if ( string.IsNullOrWhiteSpace( textureName ) )
+ return;
+
+ textureName = textureName.Replace( '\\', '/' );
+ var path = $"mount://hl2/materials/{textureName}.vtex";
+ var texture = Texture.Load( path, false );
+ if ( texture.IsValid() && !texture.IsError )
+ material.Set( uniformName, texture );
+ }
+
+ private static void SetFloat( Material material, string name, string value )
+ {
+ if ( float.TryParse( value, out var f ) )
+ material.Set( name, f );
+ }
+
+ private static void SetInt( Material material, string name, string value )
+ {
+ if ( int.TryParse( value, out var i ) )
+ material.Set( name, i );
+ }
+
+ private static void SetBool( Material material, string name, string value )
+ {
+ if ( value == "1" )
+ material.Set( name, 1 );
+ }
+
+ private static void SetVector( Material material, string name, string value )
+ {
+ var vec = ParseVector( value );
+ if ( vec.HasValue )
+ material.Set( name, vec.Value );
+ }
+
+ private static Vector3? ParseVector( string value )
+ {
+ value = value.Trim( '[', ']', '{', '}', '"', ' ' );
+ var parts = value.Split( [' ', '\t'], StringSplitOptions.RemoveEmptyEntries );
+
+ return parts.Length >= 3 && float.TryParse( parts[0], out var x ) && float.TryParse( parts[1], out var y ) && float.TryParse( parts[2], out var z )
+ ? new Vector3( x, y, z )
+ : parts.Length == 1 && float.TryParse( parts[0], out var single )
+ ? new Vector3( single, single, single )
+ : null;
+ }
+}
diff --git a/engine/Mounting/Sandbox.Mounting.HL2/Resource/HL2Model.cs b/engine/Mounting/Sandbox.Mounting.HL2/Resource/HL2Model.cs
new file mode 100644
index 000000000..ffec4b481
--- /dev/null
+++ b/engine/Mounting/Sandbox.Mounting.HL2/Resource/HL2Model.cs
@@ -0,0 +1,2036 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Text;
+using Sandbox;
+using ValvePak;
+using ValveKeyValue;
+
+internal class HL2Model : ResourceLoader
+{
+ private readonly VpkArchive _package;
+ private readonly VpkEntry _entry;
+ private readonly string _filePath;
+
+ public HL2Model( VpkArchive package, VpkEntry entry )
+ {
+ _package = package;
+ _entry = entry;
+ }
+
+ public HL2Model( string filePath )
+ {
+ _filePath = filePath;
+ }
+
+ protected override object Load()
+ {
+ byte[] mdlData;
+ byte[] vvdData = null;
+ byte[] vtxData = null;
+ byte[] aniData = null;
+ byte[] phyData = null;
+
+ if ( _package != null )
+ {
+ _package.ReadEntry( _entry, out mdlData );
+ var basePath = _entry.GetFullPath()[..^4];
+
+ var vvdEntry = _package.FindEntry( basePath + ".vvd" );
+ if ( vvdEntry != null )
+ _package.ReadEntry( vvdEntry, out vvdData );
+
+ var vtxEntry = _package.FindEntry( basePath + ".dx90.vtx" )
+ ?? _package.FindEntry( basePath + ".dx80.vtx" )
+ ?? _package.FindEntry( basePath + ".sw.vtx" );
+ if ( vtxEntry != null )
+ _package.ReadEntry( vtxEntry, out vtxData );
+
+ var aniEntry = _package.FindEntry( basePath + ".ani" );
+ if ( aniEntry != null )
+ _package.ReadEntry( aniEntry, out aniData );
+
+ var phyEntry = _package.FindEntry( basePath + ".phy" );
+ if ( phyEntry != null )
+ _package.ReadEntry( phyEntry, out phyData );
+ }
+ else
+ {
+ mdlData = File.ReadAllBytes( _filePath );
+ var basePath = _filePath[..^4];
+
+ if ( File.Exists( basePath + ".vvd" ) )
+ vvdData = File.ReadAllBytes( basePath + ".vvd" );
+
+ var vtxPath = basePath + ".dx90.vtx";
+ if ( !File.Exists( vtxPath ) )
+ vtxPath = basePath + ".dx80.vtx";
+ if ( !File.Exists( vtxPath ) )
+ vtxPath = basePath + ".sw.vtx";
+ if ( File.Exists( vtxPath ) )
+ vtxData = File.ReadAllBytes( vtxPath );
+
+ if ( File.Exists( basePath + ".ani" ) )
+ aniData = File.ReadAllBytes( basePath + ".ani" );
+
+ if ( File.Exists( basePath + ".phy" ) )
+ phyData = File.ReadAllBytes( basePath + ".phy" );
+ }
+
+ return MdlLoader.Load( mdlData, vvdData, vtxData, aniData, phyData, Path, Host );
+ }
+}
+
+internal static class MdlLoader
+{
+ private const int IDST = 0x54534449;
+ private const int IDSV = 0x56534449;
+
+ public static Model Load( byte[] mdlData, byte[] vvdData, byte[] vtxData, byte[] aniData, byte[] phyData, string path, HL2Mount mount )
+ {
+ if ( mdlData == null || mdlData.Length < StudioHeader.Size )
+ return Model.Error;
+
+ var mdl = new StudioHeader( mdlData, aniData );
+ if ( mdl.Id != IDST || mdl.Version < 44 || mdl.Version > 49 )
+ return Model.Error;
+
+ if ( vvdData == null || vtxData == null )
+ return Model.Error;
+
+ var vvd = new VvdFileHeader( vvdData );
+ if ( vvd.Id != IDSV || vvd.Version != 4 )
+ return Model.Error;
+
+ var vtx = new VtxFileHeader( vtxData );
+ if ( vtx.Version != 7 )
+ return Model.Error;
+
+ if ( mdl.Checksum != vvd.Checksum || mdl.Checksum != vtx.Checksum )
+ return Model.Error;
+
+ var vertices = vvd.GetVertices( mdl.RootLod );
+ var tangents = vvd.GetTangents( mdl.RootLod );
+
+ return BuildModel( mdl, vtx, vertices, tangents, phyData, path, mount );
+ }
+
+ private static Model BuildModel( StudioHeader mdl, VtxFileHeader vtx, StudioVertex[] vertices, Vector4[] tangents, byte[] phyData, string path, HL2Mount mount )
+ {
+ var builder = Model.Builder.WithName( path );
+
+ var allPositions = new List();
+ var meshInfos = new List<(Mesh mesh, string bodyPartName, int modelIndex)>();
+
+ var boneNames = new string[mdl.BoneCount];
+ var boneTransforms = new Transform[mdl.BoneCount];
+ for ( int i = 0; i < mdl.BoneCount; i++ )
+ {
+ var bone = mdl.GetBone( i );
+ boneNames[i] = bone.Name;
+
+ var localTransform = new Transform( bone.Position, bone.Rotation );
+ boneTransforms[i] = bone.Parent >= 0 ? boneTransforms[bone.Parent].ToWorld( localTransform ) : localTransform;
+
+ var parentName = bone.Parent >= 0 ? boneNames[bone.Parent] : null;
+ builder.AddBone( bone.Name, boneTransforms[i].Position, boneTransforms[i].Rotation, parentName );
+ }
+
+ for ( int bodyPartIdx = 0; bodyPartIdx < mdl.BodyPartCount; bodyPartIdx++ )
+ {
+ var mdlBodyPart = mdl.GetBodyPart( bodyPartIdx );
+ var vtxBodyPart = vtx.GetBodyPart( bodyPartIdx );
+ var bodyPartName = mdlBodyPart.Name;
+
+ for ( int modelIdx = 0; modelIdx < mdlBodyPart.ModelCount; modelIdx++ )
+ {
+ var mdlModel = mdlBodyPart.GetModel( modelIdx );
+ var vtxModel = vtxBodyPart.GetModel( modelIdx );
+
+ if ( mdlModel.MeshCount == 0 )
+ continue;
+
+ var eyeballsByTexture = new Dictionary>();
+ for ( int eyeIdx = 0; eyeIdx < mdlModel.EyeballCount; eyeIdx++ )
+ {
+ var eyeball = mdlModel.GetEyeball( eyeIdx );
+ if ( !eyeballsByTexture.TryGetValue( eyeball.Texture, out var list ) )
+ {
+ list = [];
+ eyeballsByTexture[eyeball.Texture] = list;
+ }
+ list.Add( eyeIdx );
+ }
+
+ var vtxLod = vtxModel.GetLod( mdl.RootLod );
+ int vertexOffset = mdlModel.VertexIndex / 48;
+
+ for ( int meshIdx = 0; meshIdx < mdlModel.MeshCount; meshIdx++ )
+ {
+ var mdlMesh = mdlModel.GetMesh( meshIdx );
+ var vtxMesh = vtxLod.GetMesh( meshIdx );
+
+ var material = LoadMaterial( mdl, mdlMesh.Material, mount );
+ if ( material.IsValid() && eyeballsByTexture.TryGetValue( mdlMesh.Material, out var eyeballList ) )
+ {
+ var eyeball = mdlModel.GetEyeball( eyeballList[0] );
+ material = CreateEyeMaterial( material, eyeball );
+ }
+
+ var meshVertices = new List();
+ var meshIndices = new List();
+ var vertexMap = new Dictionary();
+
+ for ( int groupIdx = 0; groupIdx < vtxMesh.StripGroupCount; groupIdx++ )
+ {
+ var stripGroup = vtxMesh.GetStripGroup( groupIdx );
+
+ for ( int stripIdx = 0; stripIdx < stripGroup.StripCount; stripIdx++ )
+ {
+ var strip = stripGroup.GetStrip( stripIdx );
+
+ if ( (strip.Flags & 0x01) != 0 )
+ {
+ for ( int i = 0; i < strip.IndexCount; i += 3 )
+ {
+ AddTriangle( stripGroup, strip, i,
+ mdlMesh.VertexOffset, vertexOffset,
+ vertices, tangents,
+ meshVertices, meshIndices, vertexMap );
+ }
+ }
+ else if ( (strip.Flags & 0x02) != 0 )
+ {
+ for ( int i = 0; i < strip.IndexCount - 2; i++ )
+ {
+ if ( i % 2 == 0 )
+ AddTriangleStrip( stripGroup, strip, i, 0, 1, 2,
+ mdlMesh.VertexOffset, vertexOffset,
+ vertices, tangents,
+ meshVertices, meshIndices, vertexMap );
+ else
+ AddTriangleStrip( stripGroup, strip, i, 1, 0, 2,
+ mdlMesh.VertexOffset, vertexOffset,
+ vertices, tangents,
+ meshVertices, meshIndices, vertexMap );
+ }
+ }
+ }
+ }
+
+ if ( meshVertices.Count == 0 )
+ continue;
+
+ foreach ( var v in meshVertices )
+ allPositions.Add( v.Position );
+
+ var mesh = new Mesh( material );
+ mesh.CreateVertexBuffer( meshVertices.Count, meshVertices );
+ mesh.CreateIndexBuffer( meshIndices.Count, meshIndices );
+
+ meshInfos.Add( (mesh, bodyPartName, modelIdx) );
+ }
+ }
+ }
+
+ var bounds = BBox.FromPoints( allPositions );
+
+ foreach ( var (mesh, bodyPartName, modelIndex) in meshInfos )
+ {
+ mesh.Bounds = bounds;
+ builder.AddMesh( mesh, 0, bodyPartName, modelIndex );
+ }
+
+ LoadPhysics( builder, phyData, boneNames, boneTransforms );
+ LoadAnimations( mdl, builder, boneNames, mount );
+
+ return builder.Create();
+ }
+
+ private static void LoadPhysics( ModelBuilder builder, byte[] phyData, string[] boneNames, Transform[] boneTransforms )
+ {
+ if ( phyData == null || phyData.Length < 16 )
+ return;
+
+ int headerSize = BitConverter.ToInt32( phyData, 0 );
+ int solidCount = BitConverter.ToInt32( phyData, 8 );
+
+ if ( headerSize != 16 || solidCount <= 0 || solidCount > 128 )
+ return;
+
+ var boneNameToIndex = new Dictionary( StringComparer.OrdinalIgnoreCase );
+ for ( int i = 0; i < boneNames.Length; i++ )
+ boneNameToIndex[boneNames[i]] = i;
+
+ var solidHulls = new List>>();
+ int offset = 16;
+
+ for ( int solidIdx = 0; solidIdx < solidCount && offset + 4 <= phyData.Length; solidIdx++ )
+ {
+ int solidSize = BitConverter.ToInt32( phyData, offset );
+ offset += 4;
+
+ if ( solidSize <= 0 || offset + solidSize > phyData.Length )
+ break;
+
+ var convexHulls = ParseSolidCollision( phyData, offset, solidSize );
+ solidHulls.Add( convexHulls );
+
+ offset += solidSize;
+ }
+
+ var (solidInfos, constraints) = ParsePhyKeyValues( phyData, offset );
+
+ var solidToBodyIndex = new Dictionary();
+ int bodyIndex = 0;
+
+ for ( int i = 0; i < solidHulls.Count; i++ )
+ {
+ var convexHulls = solidHulls[i];
+ var info = i < solidInfos.Count ? solidInfos[i] : new PhySolidInfo { Mass = 1.0f };
+
+ if ( convexHulls == null || convexHulls.Count == 0 )
+ continue;
+
+ var validHulls = new List>();
+ foreach ( var hull in convexHulls )
+ {
+ if ( hull == null || hull.Count < 4 )
+ continue;
+
+ var bounds = new BBox( hull[0], hull[0] );
+ foreach ( var v in hull )
+ bounds = bounds.AddPoint( v );
+
+ var sz = bounds.Size;
+ if ( sz.x < 0.01f && sz.y < 0.01f && sz.z < 0.01f )
+ continue;
+
+ validHulls.Add( hull );
+ }
+
+ if ( validHulls.Count == 0 )
+ continue;
+
+ Surface surface = null;
+ if ( !string.IsNullOrEmpty( info.SurfaceProp ) )
+ surface = Surface.FindByName( info.SurfaceProp );
+
+ var body = builder.AddBody( info.Mass, surface, info.BoneName );
+
+ foreach ( var hull in validHulls )
+ {
+ body.AddHull( CollectionsMarshal.AsSpan( hull ), Transform.Zero, new PhysicsBodyBuilder.HullSimplify
+ {
+ Method = PhysicsBodyBuilder.SimplifyMethod.QEM
+ } );
+ }
+
+ solidToBodyIndex[i] = bodyIndex;
+ bodyIndex++;
+ }
+
+ var solidToBoneIndex = new Dictionary();
+ for ( int i = 0; i < solidInfos.Count; i++ )
+ {
+ var info = solidInfos[i];
+ if ( !string.IsNullOrEmpty( info.BoneName ) && boneNameToIndex.TryGetValue( info.BoneName, out int boneIdx ) )
+ solidToBoneIndex[i] = boneIdx;
+ }
+
+ foreach ( var constraint in constraints )
+ {
+ if ( !solidToBodyIndex.TryGetValue( constraint.Parent, out int parentBodyIdx ) )
+ continue;
+ if ( !solidToBodyIndex.TryGetValue( constraint.Child, out int childBodyIdx ) )
+ continue;
+ if ( parentBodyIdx == childBodyIdx )
+ continue;
+
+ Transform frame1 = Transform.Zero;
+ Transform frame2 = Transform.Zero;
+
+ if ( solidToBoneIndex.TryGetValue( constraint.Parent, out int parentBoneIdx ) &&
+ solidToBoneIndex.TryGetValue( constraint.Child, out int childBoneIdx ) )
+ {
+ var parentBoneWorld = boneTransforms[parentBoneIdx];
+ var childBoneWorld = boneTransforms[childBoneIdx];
+ var childPosInParent = parentBoneWorld.PointToLocal( childBoneWorld.Position );
+ var childRotInParent = parentBoneWorld.RotationToLocal( childBoneWorld.Rotation );
+
+ frame1 = new Transform( childPosInParent, childRotInParent );
+ frame2 = Transform.Zero;
+ }
+
+ float twistMin = constraint.XMin;
+ float twistMax = constraint.XMax;
+
+ float twistRange = Math.Abs( twistMax - twistMin );
+ float swingYRange = Math.Abs( constraint.YMax - constraint.YMin );
+ float swingZRange = Math.Abs( constraint.ZMax - constraint.ZMin );
+
+ const float DofThreshold = 5f;
+ int dofCount = 0;
+ int dofMask = 0;
+ if ( twistRange > DofThreshold ) { dofCount++; dofMask |= 1; }
+ if ( swingYRange > DofThreshold ) { dofCount++; dofMask |= 2; }
+ if ( swingZRange > DofThreshold ) { dofCount++; dofMask |= 4; }
+
+ if ( dofCount == 0 )
+ {
+ builder.AddFixedJoint( parentBodyIdx, childBodyIdx, frame1, frame2 );
+ }
+ else if ( dofCount == 1 )
+ {
+ float hingeMin, hingeMax;
+ if ( (dofMask & 1) != 0 )
+ {
+ hingeMin = twistMin;
+ hingeMax = twistMax;
+ }
+ else if ( (dofMask & 2) != 0 )
+ {
+ hingeMin = constraint.YMin;
+ hingeMax = constraint.YMax;
+ }
+ else
+ {
+ hingeMin = constraint.ZMin;
+ hingeMax = constraint.ZMax;
+ }
+
+ builder.AddHingeJoint( parentBodyIdx, childBodyIdx, frame1, frame2 )
+ .WithTwistLimit( hingeMin, hingeMax );
+ }
+ else
+ {
+ float swingY = Math.Max( Math.Abs( constraint.YMin ), Math.Abs( constraint.YMax ) );
+ float swingZ = Math.Max( Math.Abs( constraint.ZMin ), Math.Abs( constraint.ZMax ) );
+ float swingLimit = Math.Max( swingY, swingZ );
+
+ builder.AddBallJoint( parentBodyIdx, childBodyIdx, frame1, frame2 )
+ .WithSwingLimit( swingLimit )
+ .WithTwistLimit( twistMin, twistMax );
+ }
+ }
+ }
+
+ private struct PhySolidInfo
+ {
+ public int Index;
+ public string BoneName;
+ public string ParentBoneName;
+ public float Mass;
+ public string SurfaceProp;
+ }
+
+ private struct PhyConstraintInfo
+ {
+ public int Parent;
+ public int Child;
+ public float XMin, XMax;
+ public float YMin, YMax;
+ public float ZMin, ZMax;
+ public float XFriction, YFriction, ZFriction;
+ }
+
+ private static (List solids, List constraints) ParsePhyKeyValues( byte[] data, int keyValuesOffset )
+ {
+ var solids = new List();
+ var constraints = new List();
+
+ if ( keyValuesOffset >= data.Length )
+ return (solids, constraints);
+
+ string text = Encoding.ASCII.GetString( data, keyValuesOffset, data.Length - keyValuesOffset );
+ var kv = KeyValues.Parse( "phyroot { " + text + " }" );
+ if ( kv == null )
+ return (solids, constraints);
+
+ foreach ( var child in kv.Children )
+ {
+ if ( child.Name.Equals( "solid", StringComparison.OrdinalIgnoreCase ) )
+ {
+ solids.Add( new PhySolidInfo
+ {
+ Index = child.GetInt( "index" ),
+ BoneName = child.GetString( "name" ),
+ ParentBoneName = child.GetString( "parent" ),
+ Mass = child.GetFloat( "mass", 1.0f ),
+ SurfaceProp = child.GetString( "surfaceprop" )
+ } );
+ }
+ else if ( child.Name.Equals( "ragdollconstraint", StringComparison.OrdinalIgnoreCase ) )
+ {
+ constraints.Add( new PhyConstraintInfo
+ {
+ Parent = child.GetInt( "parent", -1 ),
+ Child = child.GetInt( "child", -1 ),
+ XMin = child.GetFloat( "xmin" ),
+ XMax = child.GetFloat( "xmax" ),
+ YMin = child.GetFloat( "ymin" ),
+ YMax = child.GetFloat( "ymax" ),
+ ZMin = child.GetFloat( "zmin" ),
+ ZMax = child.GetFloat( "zmax" ),
+ XFriction = child.GetFloat( "xfriction" ),
+ YFriction = child.GetFloat( "yfriction" ),
+ ZFriction = child.GetFloat( "zfriction" )
+ } );
+ }
+ }
+
+ return (solids, constraints);
+ }
+
+ private const int VPHY_ID = 0x59485056; // 'VPHY'
+ private const int IVPS_ID = 0x53505649; // 'IVPS'
+ private const int SPVI_ID = 0x49565053; // 'SPVI'
+
+ private static List> ParseSolidCollision( byte[] data, int offset, int size )
+ {
+ if ( size < 8 )
+ return null;
+
+ int magic = BitConverter.ToInt32( data, offset );
+ if ( magic == VPHY_ID )
+ {
+ // VPHY format: collideheader_t(8) + compactsurfaceheader_t(20) + IVP_Compact_Surface(48+)
+ short modelType = BitConverter.ToInt16( data, offset + 6 );
+ if ( modelType != 0 ) // modelType 0 = convex hull
+ return null;
+
+ const int CollideHeaderSize = 8;
+ const int SurfaceHeaderSize = 20;
+
+ if ( size < CollideHeaderSize + SurfaceHeaderSize + 48 )
+ return null;
+
+ int compactSurfaceOffset = offset + CollideHeaderSize + SurfaceHeaderSize;
+ return ParseCompactSurface( data, compactSurfaceOffset, size - CollideHeaderSize - SurfaceHeaderSize );
+ }
+ else
+ {
+ // Legacy format: raw IVP_Compact_Surface
+ if ( size < 48 )
+ return null;
+
+ int legacyId = BitConverter.ToInt32( data, offset + 44 );
+ return legacyId == 0 || legacyId == IVPS_ID || legacyId == SPVI_ID
+ ? ParseCompactSurface( data, offset, size )
+ : null;
+ }
+ }
+
+ private static List> ParseCompactSurface( byte[] data, int offset, int size )
+ {
+ // IVP_Compact_Surface: offset_ledgetree_root at byte 32
+ const int CompactSurfaceSize = 48;
+ if ( size < CompactSurfaceSize )
+ return null;
+
+ int ledgetreeOffset = BitConverter.ToInt32( data, offset + 32 );
+ if ( ledgetreeOffset <= 0 || ledgetreeOffset >= size )
+ return null;
+
+ int nodeOffset = offset + ledgetreeOffset;
+
+ var allLedges = new List<(int offset, int triangleCount)>();
+ CollectLedges( data, nodeOffset, allLedges );
+
+ var result = new List>();
+ foreach ( var (ledgeOffset, _) in allLedges )
+ {
+ var vertices = ParseCompactLedge( data, ledgeOffset );
+ if ( vertices != null && vertices.Count >= 4 )
+ result.Add( vertices );
+ }
+
+ return result.Count > 0 ? result : null;
+ }
+
+ private static void CollectLedges( byte[] data, int nodeOffset, List<(int offset, int triangleCount)> ledges )
+ {
+ // IVP_Compact_Ledgetree_Node (28 bytes):
+ // offset_right_node(4) + offset_compact_ledge(4) + center(12) + radius(4) + box_sizes(3) + free_0(1)
+ const int NodeSize = 28;
+
+ var nodeStack = new Stack();
+ nodeStack.Push( nodeOffset );
+
+ while ( nodeStack.Count > 0 )
+ {
+ int currentOffset = nodeStack.Pop();
+
+ if ( currentOffset < 0 || currentOffset + NodeSize > data.Length )
+ continue;
+
+ int offsetRightNode = BitConverter.ToInt32( data, currentOffset );
+ int offsetCompactLedge = BitConverter.ToInt32( data, currentOffset + 4 );
+
+ if ( offsetCompactLedge != 0 )
+ {
+ int ledgeOffset = currentOffset + offsetCompactLedge;
+ if ( ledgeOffset >= 0 && ledgeOffset + 16 <= data.Length )
+ {
+ short numTriangles = BitConverter.ToInt16( data, ledgeOffset + 12 );
+ if ( numTriangles > 0 )
+ ledges.Add( (ledgeOffset, numTriangles) );
+ }
+ }
+
+ if ( offsetRightNode != 0 )
+ {
+ // Right child is at offset_right_node from current node
+ int rightOffset = currentOffset + offsetRightNode;
+ if ( rightOffset >= 0 && rightOffset + NodeSize <= data.Length )
+ nodeStack.Push( rightOffset );
+
+ // Left child is immediately after this node
+ int leftOffset = currentOffset + NodeSize;
+ if ( leftOffset >= 0 && leftOffset + NodeSize <= data.Length )
+ nodeStack.Push( leftOffset );
+ }
+ }
+ }
+
+ private static List ParseCompactLedge( byte[] data, int offset )
+ {
+ // IVP_Compact_Ledge (16 bytes): c_point_offset(4) + client_data(4) + flags:size_div_16(4) + n_triangles(2) + reserved(2)
+ if ( offset + 16 > data.Length )
+ return null;
+
+ int pointOffset = BitConverter.ToInt32( data, offset );
+ short numTriangles = BitConverter.ToInt16( data, offset + 12 );
+
+ if ( numTriangles <= 0 || pointOffset == 0 )
+ return null;
+
+ int pointArrayOffset = offset + pointOffset;
+ if ( pointArrayOffset < 0 || pointArrayOffset >= data.Length )
+ return null;
+
+ int trianglesOffset = offset + 16;
+
+ // IVP_Compact_Triangle (16 bytes): indices(4) + c_three_edges[3](12)
+ // IVP_Compact_Edge (4 bytes): start_point_index:16 + opposite_index:15 + is_virtual:1
+ const int TriangleSize = 16;
+
+ if ( trianglesOffset + numTriangles * TriangleSize > data.Length )
+ return null;
+
+ var vertexSet = new HashSet();
+ var vertices = new List();
+
+ const float MetersToInches = 39.3701f;
+
+ for ( int i = 0; i < numTriangles; i++ )
+ {
+ int triOffset = trianglesOffset + i * TriangleSize;
+
+ for ( int j = 0; j < 3; j++ )
+ {
+ int edgeOffset = triOffset + 4 + j * 4;
+ if ( edgeOffset + 4 > data.Length )
+ continue;
+
+ uint edgeData = BitConverter.ToUInt32( data, edgeOffset );
+ int pointIndex = (int)(edgeData & 0xFFFF);
+
+ if ( !vertexSet.Add( pointIndex ) )
+ continue;
+
+ // IVP_Compact_Poly_Point (16 bytes): x,y,z floats + hesse_val
+ int ptOffset = pointArrayOffset + pointIndex * 16;
+ if ( ptOffset + 12 > data.Length || ptOffset < 0 )
+ continue;
+
+ float ivpX = BitConverter.ToSingle( data, ptOffset );
+ float ivpY = BitConverter.ToSingle( data, ptOffset + 4 );
+ float ivpZ = BitConverter.ToSingle( data, ptOffset + 8 );
+
+ // IVP to Source: (X, Z, -Y) * MetersToInches
+ vertices.Add( new Vector3( ivpX * MetersToInches, ivpZ * MetersToInches, -ivpY * MetersToInches ) );
+ }
+ }
+
+ return vertices.Count > 0 ? vertices : null;
+ }
+
+ private static void LoadAnimations( StudioHeader mdl, ModelBuilder builder, string[] boneNames, HL2Mount mount )
+ {
+ var mainBoneNameToIndex = new Dictionary( StringComparer.OrdinalIgnoreCase );
+ for ( int i = 0; i < boneNames.Length; i++ )
+ mainBoneNameToIndex[boneNames[i]] = i;
+
+ var mainBasePoses = new (Vector3 pos, Rotation rot)[boneNames.Length];
+ for ( int i = 0; i < boneNames.Length; i++ )
+ {
+ var bone = mdl.GetBone( i );
+ mainBasePoses[i] = (bone.Position, bone.Rotation);
+ }
+
+ LoadModelAnimations( mdl, builder, boneNames.Length, null, mainBasePoses );
+ LoadIncludeModelAnimations( mdl, builder, boneNames.Length, mainBoneNameToIndex, mainBasePoses, mount );
+ }
+
+ private static void LoadModelAnimations( StudioHeader mdl, ModelBuilder builder, int mainBoneCount, int[] boneMapping, (Vector3 pos, Rotation rot)[] mainBasePoses )
+ {
+ // Sequences reference animations and contain the actual name/flags. Animations are just raw bone data
+ var aniData = mdl.GetAniData();
+ bool hasAniFile = !aniData.IsEmpty;
+ var mdlData = mdl.GetData();
+
+ for ( int seqIdx = 0; seqIdx < mdl.LocalSeqCount; seqIdx++ )
+ {
+ var seqDesc = mdl.GetSeqDesc( seqIdx );
+ var seqName = seqDesc.Label;
+
+ int animIndex = seqDesc.GetAnimIndex( 0, 0 );
+ if ( animIndex < 0 || animIndex >= mdl.LocalAnimCount )
+ continue;
+
+ var animDesc = mdl.GetAnimDesc( animIndex );
+
+ if ( animDesc.NumFrames <= 0 )
+ continue;
+
+ var anim = builder.AddAnimation( seqName, animDesc.Fps )
+ .WithLooping( seqDesc.Looping )
+ .WithDelta( seqDesc.Delta );
+
+ LoadAnimationFrames( mdl, animDesc, mdlData, hasAniFile ? aniData : ReadOnlySpan.Empty, anim, mainBoneCount, boneMapping, mainBasePoses );
+ }
+ }
+
+ private static void LoadAnimationFrames( StudioHeader mdl, StudioAnimDesc animDesc, ReadOnlySpan mdlData, ReadOnlySpan aniData, AnimationBuilder anim, int mainBoneCount, int[] boneMapping, (Vector3 pos, Rotation rot)[] mainBasePoses )
+ {
+ int numFrames = animDesc.NumFrames;
+ bool isDelta = (animDesc.Flags & 0x0004) != 0; // STUDIO_DELTA
+ bool hasSections = animDesc.SectionFrames != 0;
+ bool hasAniFile = !aniData.IsEmpty;
+
+ int localBoneCount = mdl.BoneCount;
+ var basePoses = new (Vector3 pos, Rotation rot, Vector3 euler, Vector3 posScale, Vector3 rotScale)[localBoneCount];
+ for ( int i = 0; i < localBoneCount; i++ )
+ {
+ var bone = mdl.GetBone( i );
+ basePoses[i] = (bone.Position, bone.Rotation, bone.EulerRotation, bone.PosScale, bone.RotScale);
+ }
+
+ for ( int frame = 0; frame < numFrames; frame++ )
+ {
+ ReadOnlySpan animData;
+ int animDataOffset;
+ int sectionRelativeFrame = frame;
+
+ if ( hasSections )
+ {
+ var (block, index) = animDesc.GetAnimBlockForFrame( frame );
+ sectionRelativeFrame = animDesc.GetSectionRelativeFrame( frame );
+
+ if ( block == 0 )
+ {
+ animData = mdlData;
+ animDataOffset = animDesc.AnimDataOffset;
+ }
+ else if ( hasAniFile )
+ {
+ int blockStart = mdl.GetAnimBlockDataStart( block );
+ if ( blockStart < 0 )
+ continue;
+ animData = aniData;
+ animDataOffset = blockStart + index;
+ }
+ else
+ {
+ continue;
+ }
+ }
+ else
+ {
+ if ( animDesc.AnimBlock == 0 )
+ {
+ animData = mdlData;
+ animDataOffset = animDesc.AnimDataOffset;
+ }
+ else if ( hasAniFile )
+ {
+ int blockStart = mdl.GetAnimBlockDataStart( animDesc.AnimBlock );
+ if ( blockStart < 0 )
+ continue;
+ animData = aniData;
+ animDataOffset = blockStart + animDesc.AnimIndex;
+ }
+ else
+ {
+ continue;
+ }
+ }
+
+ if ( animDataOffset < 0 || animDataOffset >= animData.Length - 4 )
+ continue;
+
+ var boneAnims = new List<(int localBone, byte flags, int dataOffset)>();
+ int offset = animDataOffset;
+ while ( offset >= 0 && offset < animData.Length - 4 )
+ {
+ int bone = animData[offset];
+ byte flags = animData[offset + 1];
+ short nextOffset = BitConverter.ToInt16( animData.Slice( offset + 2, 2 ) );
+
+ boneAnims.Add( (bone, flags, offset + 4) );
+
+ if ( nextOffset == 0 ) break;
+ offset += nextOffset;
+ }
+
+ var transforms = new Transform[mainBoneCount];
+
+ for ( int i = 0; i < mainBoneCount; i++ )
+ {
+ transforms[i] = isDelta ? new Transform( Vector3.Zero, Rotation.Identity ) : new Transform( mainBasePoses[i].pos, mainBasePoses[i].rot );
+ }
+
+ foreach ( var (localBoneIndex, flags, dataOffset) in boneAnims )
+ {
+ int mainBoneIndex;
+ if ( boneMapping != null )
+ {
+ if ( localBoneIndex < 0 || localBoneIndex >= boneMapping.Length )
+ continue;
+ mainBoneIndex = boneMapping[localBoneIndex];
+ if ( mainBoneIndex < 0 )
+ continue;
+ }
+ else
+ {
+ mainBoneIndex = localBoneIndex;
+ if ( mainBoneIndex < 0 || mainBoneIndex >= mainBoneCount )
+ continue;
+ }
+
+ if ( localBoneIndex < 0 || localBoneIndex >= localBoneCount )
+ continue;
+
+ var (basePos, baseRot, baseEuler, posScale, rotScale) = basePoses[localBoneIndex];
+
+ Vector3 pos;
+ Rotation rot;
+
+ if ( (flags & StudioAnimFlags.RawRot) != 0 )
+ {
+ rot = StudioAnimReader.DecodeQuaternion48( animData, dataOffset );
+ }
+ else if ( (flags & StudioAnimFlags.RawRot2) != 0 )
+ {
+ rot = StudioAnimReader.DecodeQuaternion64( animData, dataOffset );
+ }
+ else if ( (flags & StudioAnimFlags.AnimRot) != 0 )
+ {
+ int rotDataOffset = dataOffset;
+ var euler = ExtractCompressedEuler( animData, rotDataOffset, sectionRelativeFrame, rotScale );
+ if ( !isDelta )
+ euler += baseEuler;
+ rot = EulerToQuaternion( euler );
+ }
+ else
+ {
+ rot = isDelta ? Rotation.Identity : baseRot;
+ }
+
+ int posDataOffset = dataOffset;
+ if ( (flags & StudioAnimFlags.RawRot) != 0 )
+ posDataOffset += 6;
+ else if ( (flags & StudioAnimFlags.RawRot2) != 0 )
+ posDataOffset += 8;
+ else if ( (flags & StudioAnimFlags.AnimRot) != 0 )
+ posDataOffset += 6;
+
+ if ( (flags & StudioAnimFlags.RawPos) != 0 )
+ {
+ pos = StudioAnimReader.DecodeVector48( animData, posDataOffset );
+ }
+ else if ( (flags & StudioAnimFlags.AnimPos) != 0 )
+ {
+ pos = ExtractCompressedPosition( animData, posDataOffset, sectionRelativeFrame, posScale );
+ if ( !isDelta )
+ pos += basePos;
+ }
+ else
+ {
+ pos = isDelta ? Vector3.Zero : basePos;
+ }
+
+ transforms[mainBoneIndex] = new Transform( pos, rot );
+ }
+
+ anim.AddFrame( transforms );
+ }
+ }
+
+ private static Vector3 ExtractCompressedEuler( ReadOnlySpan data, int valuePtr, int frame, Vector3 scale )
+ {
+ short offX = BitConverter.ToInt16( data.Slice( valuePtr, 2 ) );
+ short offY = BitConverter.ToInt16( data.Slice( valuePtr + 2, 2 ) );
+ short offZ = BitConverter.ToInt16( data.Slice( valuePtr + 4, 2 ) );
+
+ float x = offX > 0 ? ExtractAnimValue( data, valuePtr + offX, frame ) * scale.x : 0;
+ float y = offY > 0 ? ExtractAnimValue( data, valuePtr + offY, frame ) * scale.y : 0;
+ float z = offZ > 0 ? ExtractAnimValue( data, valuePtr + offZ, frame ) * scale.z : 0;
+
+ return new Vector3( x, y, z );
+ }
+
+ private static Vector3 ExtractCompressedPosition( ReadOnlySpan data, int valuePtr, int frame, Vector3 scale )
+ {
+ short offX = BitConverter.ToInt16( data.Slice( valuePtr, 2 ) );
+ short offY = BitConverter.ToInt16( data.Slice( valuePtr + 2, 2 ) );
+ short offZ = BitConverter.ToInt16( data.Slice( valuePtr + 4, 2 ) );
+
+ float x = offX > 0 ? ExtractAnimValue( data, valuePtr + offX, frame ) * scale.x : 0;
+ float y = offY > 0 ? ExtractAnimValue( data, valuePtr + offY, frame ) * scale.y : 0;
+ float z = offZ > 0 ? ExtractAnimValue( data, valuePtr + offZ, frame ) * scale.z : 0;
+
+ return new Vector3( x, y, z );
+ }
+
+ private static float ExtractAnimValue( ReadOnlySpan data, int offset, int frame )
+ {
+ if ( offset < 0 || offset >= data.Length - 2 )
+ return 0;
+
+ int k = frame;
+
+ while ( true )
+ {
+ byte valid = data[offset];
+ byte total = data[offset + 1];
+
+ if ( total == 0 )
+ return 0;
+
+ if ( k < total )
+ {
+ if ( k < valid )
+ {
+ int valueOffset = offset + 2 + k * 2;
+ return valueOffset + 2 > data.Length ? 0 : BitConverter.ToInt16( data.Slice( valueOffset, 2 ) );
+ }
+ else
+ {
+ int valueOffset = offset + 2 + (valid - 1) * 2;
+ return valueOffset + 2 > data.Length ? 0 : BitConverter.ToInt16( data.Slice( valueOffset, 2 ) );
+ }
+ }
+
+ k -= total;
+ offset += 2 + valid * 2;
+
+ if ( offset >= data.Length - 2 )
+ return 0;
+ }
+ }
+
+ private static Rotation EulerToQuaternion( Vector3 euler )
+ {
+ float sr, cr, sp, cp, sy, cy;
+
+ sr = MathF.Sin( euler.x * 0.5f );
+ cr = MathF.Cos( euler.x * 0.5f );
+ sp = MathF.Sin( euler.y * 0.5f );
+ cp = MathF.Cos( euler.y * 0.5f );
+ sy = MathF.Sin( euler.z * 0.5f );
+ cy = MathF.Cos( euler.z * 0.5f );
+
+ float srXcp = sr * cp;
+ float crXsp = cr * sp;
+ float crXcp = cr * cp;
+ float srXsp = sr * sp;
+
+ return new Rotation(
+ srXcp * cy - crXsp * sy,
+ crXsp * cy + srXcp * sy,
+ crXcp * sy - srXsp * cy,
+ crXcp * cy + srXsp * sy
+ );
+ }
+
+ private static void LoadIncludeModelAnimations( StudioHeader mainMdl, ModelBuilder builder, int mainBoneCount, Dictionary mainBoneNameToIndex, (Vector3 pos, Rotation rot)[] mainBasePoses, HL2Mount mount )
+ {
+ if ( mount == null )
+ return;
+
+ for ( int i = 0; i < mainMdl.IncludeModelCount; i++ )
+ {
+ string includePath = mainMdl.GetIncludeModelPath( i );
+ if ( string.IsNullOrEmpty( includePath ) )
+ continue;
+
+ byte[] includeMdlData = mount.ReadFile( includePath );
+ if ( includeMdlData == null || includeMdlData.Length < StudioHeader.Size )
+ continue;
+
+ string aniPath = includePath.Replace( ".mdl", ".ani" );
+ byte[] includeAniData = mount.ReadFile( aniPath );
+
+ var includeMdl = new StudioHeader( includeMdlData, includeAniData );
+ if ( includeMdl.Id != IDST || includeMdl.Version < 44 || includeMdl.Version > 49 )
+ continue;
+
+ int includeBoneCount = includeMdl.BoneCount;
+ int[] boneMapping = new int[includeBoneCount];
+ for ( int b = 0; b < includeBoneCount; b++ )
+ {
+ string boneName = includeMdl.GetBone( b ).Name;
+ boneMapping[b] = mainBoneNameToIndex.TryGetValue( boneName, out int mainIndex ) ? mainIndex : -1;
+ }
+
+ LoadModelAnimations( includeMdl, builder, mainBoneCount, boneMapping, mainBasePoses );
+ }
+ }
+
+ private static void AddTriangle(
+ VtxStripGroupHeader stripGroup, VtxStripHeader strip, int baseIndex,
+ int meshVertexOffset, int modelVertexOffset,
+ StudioVertex[] vertices, Vector4[] tangents,
+ List outVertices, List outIndices, Dictionary vertexMap )
+ {
+ int idx0 = strip.IndexOffset + baseIndex;
+ int idx1 = strip.IndexOffset + baseIndex + 1;
+ int idx2 = strip.IndexOffset + baseIndex + 2;
+
+ int vi0 = stripGroup.GetIndex( idx0 );
+ int vi1 = stripGroup.GetIndex( idx1 );
+ int vi2 = stripGroup.GetIndex( idx2 );
+
+ var v0 = stripGroup.GetVertex( vi0 );
+ var v1 = stripGroup.GetVertex( vi1 );
+ var v2 = stripGroup.GetVertex( vi2 );
+
+ int global0 = modelVertexOffset + meshVertexOffset + v0.OrigMeshVertId;
+ int global1 = modelVertexOffset + meshVertexOffset + v1.OrigMeshVertId;
+ int global2 = modelVertexOffset + meshVertexOffset + v2.OrigMeshVertId;
+
+ outIndices.Add( GetOrAddVertex( outVertices, vertexMap, vertices, tangents, global0 ) );
+ outIndices.Add( GetOrAddVertex( outVertices, vertexMap, vertices, tangents, global2 ) );
+ outIndices.Add( GetOrAddVertex( outVertices, vertexMap, vertices, tangents, global1 ) );
+ }
+
+ private static void AddTriangleStrip(
+ VtxStripGroupHeader stripGroup, VtxStripHeader strip, int baseIndex, int a, int b, int c,
+ int meshVertexOffset, int modelVertexOffset,
+ StudioVertex[] vertices, Vector4[] tangents,
+ List outVertices, List outIndices, Dictionary vertexMap )
+ {
+ int idx0 = strip.IndexOffset + baseIndex + a;
+ int idx1 = strip.IndexOffset + baseIndex + b;
+ int idx2 = strip.IndexOffset + baseIndex + c;
+
+ int vi0 = stripGroup.GetIndex( idx0 );
+ int vi1 = stripGroup.GetIndex( idx1 );
+ int vi2 = stripGroup.GetIndex( idx2 );
+
+ var v0 = stripGroup.GetVertex( vi0 );
+ var v1 = stripGroup.GetVertex( vi1 );
+ var v2 = stripGroup.GetVertex( vi2 );
+
+ int global0 = modelVertexOffset + meshVertexOffset + v0.OrigMeshVertId;
+ int global1 = modelVertexOffset + meshVertexOffset + v1.OrigMeshVertId;
+ int global2 = modelVertexOffset + meshVertexOffset + v2.OrigMeshVertId;
+
+ if ( global0 == global1 || global1 == global2 || global0 == global2 )
+ return;
+
+ outIndices.Add( GetOrAddVertex( outVertices, vertexMap, vertices, tangents, global0 ) );
+ outIndices.Add( GetOrAddVertex( outVertices, vertexMap, vertices, tangents, global2 ) );
+ outIndices.Add( GetOrAddVertex( outVertices, vertexMap, vertices, tangents, global1 ) );
+ }
+
+ private static int GetOrAddVertex(
+ List outVertices, Dictionary map,
+ StudioVertex[] srcVerts, Vector4[] srcTangents, int globalIndex )
+ {
+ if ( map.TryGetValue( globalIndex, out int existing ) )
+ return existing;
+
+ var v = srcVerts[globalIndex];
+ var t = srcTangents[globalIndex];
+ var weights = NormalizeWeights( v.BoneWeights[0], v.BoneWeights[1], v.BoneWeights[2] );
+
+ int idx = outVertices.Count;
+ outVertices.Add( new Vertex
+ {
+ Position = v.Position,
+ Normal = v.Normal,
+ Tangent = new Vector3( t.x, t.y, t.z ),
+ TexCoord = v.TexCoord,
+ BlendIndices = new Color32( v.BoneIds[0], v.BoneIds[1], v.BoneIds[2], 0 ),
+ BlendWeights = weights
+ } );
+
+ map[globalIndex] = idx;
+ return idx;
+ }
+
+ private static Color32 NormalizeWeights( float w0, float w1, float w2 )
+ {
+ var iw0 = (int)(w0 * 255 + 0.5f);
+ var iw1 = (int)(w1 * 255 + 0.5f);
+ var iw2 = (int)(w2 * 255 + 0.5f);
+
+ var diff = 255 - (iw0 + iw1 + iw2);
+ if ( diff != 0 )
+ {
+ if ( iw0 >= iw1 && iw0 >= iw2 ) iw0 += diff;
+ else if ( iw1 >= iw2 ) iw1 += diff;
+ else iw2 += diff;
+ }
+
+ return new Color32( (byte)iw0, (byte)iw1, (byte)iw2, 0 );
+ }
+
+ private static Material CreateEyeMaterial( Material material, StudioEyeball eyeball )
+ {
+ var origin = eyeball.Origin;
+ var forward = eyeball.Forward.Normal;
+ var up = eyeball.Up.Normal;
+ var right = Vector3.Cross( forward, up ).Normal;
+
+ float irisRadius = eyeball.Radius * eyeball.IrisScale;
+ float scale = 0.5f / irisRadius;
+
+ var irisU = new Vector4( right.x * scale, right.y * scale, right.z * scale, 0.5f - Vector3.Dot( right, origin ) * scale );
+ var irisV = new Vector4( up.x * scale, up.y * scale, up.z * scale, 0.5f - Vector3.Dot( up, origin ) * scale );
+
+ material.Set( "g_vIrisU", irisU );
+ material.Set( "g_vIrisV", irisV );
+
+ return material;
+ }
+
+ private static Material LoadMaterial( StudioHeader mdl, int materialIndex, HL2Mount mount )
+ {
+ var textureName = mdl.GetTexture( materialIndex ).Name.ToLowerInvariant().Replace( '\\', '/' );
+ int cdTextureCount = mdl.CdTextureCount;
+ var searchPaths = new string[cdTextureCount];
+
+ for ( int i = 0; i < cdTextureCount; i++ )
+ {
+ searchPaths[i] = mdl.GetCdTexture( i ).ToLowerInvariant().Replace( '\\', '/' ).TrimEnd( '/' );
+ }
+
+ for ( int i = 0; i < searchPaths.Length; i++ )
+ {
+ var searchPath = searchPaths[i];
+ var path = string.IsNullOrEmpty( searchPath ) ? textureName : $"{searchPath}/{textureName}";
+
+ var vmtPath = $"materials/{path}.vmt";
+ if ( mount == null || !mount.FileExists( vmtPath ) )
+ continue;
+
+ var fullPath = $"mount://hl2/materials/{path}.vmat";
+ var material = Material.Load( fullPath );
+ if ( material.IsValid() )
+ return material;
+ }
+
+ return null;
+ }
+
+ [StructLayout( LayoutKind.Sequential )]
+ private struct Vertex
+ {
+ [VertexLayout.Position] public Vector3 Position;
+ [VertexLayout.Normal] public Vector3 Normal;
+ [VertexLayout.Tangent] public Vector3 Tangent;
+ [VertexLayout.TexCoord] public Vector2 TexCoord;
+ [VertexLayout.BlendIndices] public Color32 BlendIndices;
+ [VertexLayout.BlendWeight] public Color32 BlendWeights;
+ }
+}
+
+///
+/// studiohdr_t - MDL file header (408 bytes)
+///
+internal readonly ref struct StudioHeader( byte[] data, byte[] aniData = null )
+{
+ public const int Size = 408;
+ private readonly ReadOnlySpan _data = data;
+ private readonly ReadOnlySpan _aniData = aniData ?? ReadOnlySpan.Empty;
+
+ public ReadOnlySpan GetData() => _data;
+ public ReadOnlySpan GetAniData() => _aniData;
+
+ public int Id => BitConverter.ToInt32( _data[0..4] );
+ public int Version => BitConverter.ToInt32( _data[4..8] );
+ public int Checksum => BitConverter.ToInt32( _data[8..12] );
+
+ public Vector3 EyePosition => ReadVector3( 80 );
+ public Vector3 IllumPosition => ReadVector3( 92 );
+ public Vector3 HullMin => ReadVector3( 104 );
+ public Vector3 HullMax => ReadVector3( 116 );
+ public Vector3 ViewBBMin => ReadVector3( 128 );
+ public Vector3 ViewBBMax => ReadVector3( 140 );
+ public int Flags => BitConverter.ToInt32( _data[152..156] );
+
+ public int BoneCount => BitConverter.ToInt32( _data[156..160] );
+ public int BoneOffset => BitConverter.ToInt32( _data[160..164] );
+
+ public int BoneControllerCount => BitConverter.ToInt32( _data[164..168] );
+ public int BoneControllerOffset => BitConverter.ToInt32( _data[168..172] );
+
+ public int HitboxSetCount => BitConverter.ToInt32( _data[172..176] );
+ public int HitboxSetOffset => BitConverter.ToInt32( _data[176..180] );
+
+ public int LocalAnimCount => BitConverter.ToInt32( _data[180..184] );
+ public int LocalAnimOffset => BitConverter.ToInt32( _data[184..188] );
+ public int LocalSeqCount => BitConverter.ToInt32( _data[188..192] );
+ public int LocalSeqOffset => BitConverter.ToInt32( _data[192..196] );
+
+ public int NumAnimBlocks => BitConverter.ToInt32( _data[352..356] );
+ public int AnimBlockOffset => BitConverter.ToInt32( _data[356..360] );
+
+ public int TextureCount => BitConverter.ToInt32( _data[204..208] );
+ public int TextureOffset => BitConverter.ToInt32( _data[208..212] );
+ public int CdTextureCount => BitConverter.ToInt32( _data[212..216] );
+ public int CdTextureOffset => BitConverter.ToInt32( _data[216..220] );
+ public int SkinRefCount => BitConverter.ToInt32( _data[220..224] );
+ public int SkinFamilyCount => BitConverter.ToInt32( _data[224..228] );
+ public int SkinOffset => BitConverter.ToInt32( _data[228..232] );
+ public int BodyPartCount => BitConverter.ToInt32( _data[232..236] );
+ public int BodyPartOffset => BitConverter.ToInt32( _data[236..240] );
+
+ public int IncludeModelCount => BitConverter.ToInt32( _data[336..340] );
+ public int IncludeModelOffset => BitConverter.ToInt32( _data[340..344] );
+
+ public byte RootLod => _data[377];
+
+ public StudioBone GetBone( int index )
+ {
+ int offset = BoneOffset + index * StudioBone.Size;
+ return new StudioBone( _data, offset );
+ }
+
+ public StudioBodyParts GetBodyPart( int index )
+ {
+ int offset = BodyPartOffset + index * StudioBodyParts.Size;
+ return new StudioBodyParts( _data, offset );
+ }
+
+ public StudioTexture GetTexture( int index )
+ {
+ int offset = TextureOffset + index * StudioTexture.Size;
+ return new StudioTexture( _data, offset );
+ }
+
+ public string GetCdTexture( int index )
+ {
+ int ptrOffset = CdTextureOffset + index * 4;
+ int stringOffset = BitConverter.ToInt32( _data.Slice( ptrOffset, 4 ) );
+ return ReadString( stringOffset );
+ }
+
+ public int GetSkinRef( int family, int index )
+ {
+ if ( family < 0 || family >= SkinFamilyCount || index < 0 || index >= SkinRefCount )
+ return index;
+ int offset = SkinOffset + (family * SkinRefCount + index) * 2;
+ return BitConverter.ToInt16( _data.Slice( offset, 2 ) );
+ }
+
+ public StudioAnimDesc GetAnimDesc( int index )
+ {
+ int offset = LocalAnimOffset + index * StudioAnimDesc.Size;
+ return new StudioAnimDesc( _data, offset );
+ }
+
+ public StudioSeqDesc GetSeqDesc( int index )
+ {
+ int offset = LocalSeqOffset + index * StudioSeqDesc.Size;
+ return new StudioSeqDesc( _data, offset );
+ }
+
+ public string GetIncludeModelPath( int index )
+ {
+ if ( index < 0 || index >= IncludeModelCount )
+ return null;
+
+ int structOffset = IncludeModelOffset + index * 8;
+ int nameOffset = structOffset + BitConverter.ToInt32( _data.Slice( structOffset + 4, 4 ) );
+ return ReadString( nameOffset );
+ }
+
+ public int GetAnimBlockDataStart( int blockIndex )
+ {
+ if ( blockIndex <= 0 || blockIndex >= NumAnimBlocks || AnimBlockOffset == 0 )
+ return -1;
+
+ int blockTableOffset = AnimBlockOffset + blockIndex * 8;
+ return BitConverter.ToInt32( _data.Slice( blockTableOffset, 4 ) );
+ }
+
+ private Vector3 ReadVector3( int offset ) => new(
+ BitConverter.ToSingle( _data.Slice( offset, 4 ) ),
+ BitConverter.ToSingle( _data.Slice( offset + 4, 4 ) ),
+ BitConverter.ToSingle( _data.Slice( offset + 8, 4 ) )
+ );
+
+ private string ReadString( int offset )
+ {
+ int end = offset;
+ while ( end < _data.Length && _data[end] != 0 ) end++;
+ return Encoding.ASCII.GetString( _data.Slice( offset, end - offset ) );
+ }
+}
+
+///
+/// mstudiobone_t - Bone definition (216 bytes)
+///
+internal readonly ref struct StudioBone
+{
+ public const int Size = 216;
+ private readonly ReadOnlySpan _data;
+ private readonly int _offset;
+
+ public StudioBone( ReadOnlySpan data, int offset )
+ {
+ _data = data;
+ _offset = offset;
+ }
+
+ private int NameOffset => BitConverter.ToInt32( _data.Slice( _offset, 4 ) );
+ public int Parent => BitConverter.ToInt32( _data.Slice( _offset + 4, 4 ) );
+
+ public Vector3 Position => new(
+ BitConverter.ToSingle( _data.Slice( _offset + 32, 4 ) ),
+ BitConverter.ToSingle( _data.Slice( _offset + 36, 4 ) ),
+ BitConverter.ToSingle( _data.Slice( _offset + 40, 4 ) )
+ );
+
+ public Rotation Rotation => new(
+ BitConverter.ToSingle( _data.Slice( _offset + 44, 4 ) ),
+ BitConverter.ToSingle( _data.Slice( _offset + 48, 4 ) ),
+ BitConverter.ToSingle( _data.Slice( _offset + 52, 4 ) ),
+ BitConverter.ToSingle( _data.Slice( _offset + 56, 4 ) )
+ );
+
+ public Vector3 EulerRotation => new(
+ BitConverter.ToSingle( _data.Slice( _offset + 60, 4 ) ),
+ BitConverter.ToSingle( _data.Slice( _offset + 64, 4 ) ),
+ BitConverter.ToSingle( _data.Slice( _offset + 68, 4 ) )
+ );
+
+ public Vector3 PosScale => new(
+ BitConverter.ToSingle( _data.Slice( _offset + 72, 4 ) ),
+ BitConverter.ToSingle( _data.Slice( _offset + 76, 4 ) ),
+ BitConverter.ToSingle( _data.Slice( _offset + 80, 4 ) )
+ );
+
+ public Vector3 RotScale => new(
+ BitConverter.ToSingle( _data.Slice( _offset + 84, 4 ) ),
+ BitConverter.ToSingle( _data.Slice( _offset + 88, 4 ) ),
+ BitConverter.ToSingle( _data.Slice( _offset + 92, 4 ) )
+ );
+
+ public int Flags => BitConverter.ToInt32( _data.Slice( _offset + 160, 4 ) );
+
+ public string Name
+ {
+ get
+ {
+ int offset = _offset + NameOffset;
+ int end = offset;
+ while ( end < _data.Length && _data[end] != 0 ) end++;
+ return Encoding.ASCII.GetString( _data[offset..end] );
+ }
+ }
+}
+
+///
+/// mstudiobodyparts_t - Body part group (16 bytes)
+///
+internal readonly ref struct StudioBodyParts
+{
+ public const int Size = 16;
+ private readonly ReadOnlySpan _data;
+ private readonly int _offset;
+
+ public StudioBodyParts( ReadOnlySpan data, int offset )
+ {
+ _data = data;
+ _offset = offset;
+ }
+
+ private int NameOffset => BitConverter.ToInt32( _data.Slice( _offset, 4 ) );
+ public int ModelCount => BitConverter.ToInt32( _data.Slice( _offset + 4, 4 ) );
+ public int Base => BitConverter.ToInt32( _data.Slice( _offset + 8, 4 ) );
+ private int ModelOffset => BitConverter.ToInt32( _data.Slice( _offset + 12, 4 ) );
+
+ public string Name
+ {
+ get
+ {
+ int offset = _offset + NameOffset;
+ int end = offset;
+ while ( end < _data.Length && _data[end] != 0 ) end++;
+ return Encoding.ASCII.GetString( _data[offset..end] );
+ }
+ }
+
+ public StudioModel GetModel( int index )
+ {
+ int offset = _offset + ModelOffset + index * StudioModel.Size;
+ return new StudioModel( _data, offset );
+ }
+}
+
+///
+/// mstudiomodel_t - Model within a body part (148 bytes)
+///
+internal readonly ref struct StudioModel
+{
+ public const int Size = 148;
+ private readonly ReadOnlySpan _data;
+ private readonly int _offset;
+
+ public StudioModel( ReadOnlySpan data, int offset )
+ {
+ _data = data;
+ _offset = offset;
+ }
+
+ public string Name
+ {
+ get
+ {
+ int end = 0;
+ while ( end < 64 && _data[_offset + end] != 0 ) end++;
+ return Encoding.ASCII.GetString( _data.Slice( _offset, end ) );
+ }
+ }
+
+ public int MeshCount => BitConverter.ToInt32( _data.Slice( _offset + 72, 4 ) );
+ private int MeshOffset => BitConverter.ToInt32( _data.Slice( _offset + 76, 4 ) );
+ public int VertexCount => BitConverter.ToInt32( _data.Slice( _offset + 80, 4 ) );
+ public int VertexIndex => BitConverter.ToInt32( _data.Slice( _offset + 84, 4 ) );
+ public int EyeballCount => BitConverter.ToInt32( _data.Slice( _offset + 100, 4 ) );
+ private int EyeballOffset => BitConverter.ToInt32( _data.Slice( _offset + 104, 4 ) );
+
+ public StudioMesh GetMesh( int index )
+ {
+ int offset = _offset + MeshOffset + index * StudioMesh.Size;
+ return new StudioMesh( _data, offset );
+ }
+
+ public StudioEyeball GetEyeball( int index )
+ {
+ int offset = _offset + EyeballOffset + index * StudioEyeball.Size;
+ return new StudioEyeball( _data, offset );
+ }
+}
+
+///
+/// mstudiomesh_t - Mesh definition (116 bytes)
+///
+internal readonly ref struct StudioMesh
+{
+ public const int Size = 116;
+ private readonly ReadOnlySpan _data;
+ private readonly int _offset;
+
+ public StudioMesh( ReadOnlySpan data, int offset )
+ {
+ _data = data;
+ _offset = offset;
+ }
+
+ public int Material => BitConverter.ToInt32( _data.Slice( _offset, 4 ) );
+ public int VertexCount => BitConverter.ToInt32( _data.Slice( _offset + 8, 4 ) );
+ public int VertexOffset => BitConverter.ToInt32( _data.Slice( _offset + 12, 4 ) );
+}
+
+///
+/// mstudioeyeball_t - Eyeball definition (172 bytes)
+///
+internal readonly ref struct StudioEyeball
+{
+ public const int Size = 172;
+ private readonly ReadOnlySpan _data;
+ private readonly int _offset;
+
+ public StudioEyeball( ReadOnlySpan data, int offset )
+ {
+ _data = data;
+ _offset = offset;
+ }
+
+ public int Bone => BitConverter.ToInt32( _data.Slice( _offset + 4, 4 ) );
+ public Vector3 Origin => new(
+ BitConverter.ToSingle( _data.Slice( _offset + 8, 4 ) ),
+ BitConverter.ToSingle( _data.Slice( _offset + 12, 4 ) ),
+ BitConverter.ToSingle( _data.Slice( _offset + 16, 4 ) )
+ );
+ public float ZOffset => BitConverter.ToSingle( _data.Slice( _offset + 20, 4 ) );
+ public float Radius => BitConverter.ToSingle( _data.Slice( _offset + 24, 4 ) );
+ public Vector3 Up => new(
+ BitConverter.ToSingle( _data.Slice( _offset + 28, 4 ) ),
+ BitConverter.ToSingle( _data.Slice( _offset + 32, 4 ) ),
+ BitConverter.ToSingle( _data.Slice( _offset + 36, 4 ) )
+ );
+ public Vector3 Forward => new(
+ BitConverter.ToSingle( _data.Slice( _offset + 40, 4 ) ),
+ BitConverter.ToSingle( _data.Slice( _offset + 44, 4 ) ),
+ BitConverter.ToSingle( _data.Slice( _offset + 48, 4 ) )
+ );
+ public int Texture => BitConverter.ToInt32( _data.Slice( _offset + 52, 4 ) );
+ public float IrisScale => BitConverter.ToSingle( _data.Slice( _offset + 60, 4 ) );
+}
+
+///
+/// mstudiotexture_t - Texture/material reference (64 bytes)
+///
+internal readonly ref struct StudioTexture
+{
+ public const int Size = 64;
+ private readonly ReadOnlySpan _data;
+ private readonly int _offset;
+
+ public StudioTexture( ReadOnlySpan data, int offset )
+ {
+ _data = data;
+ _offset = offset;
+ }
+
+ public string Name
+ {
+ get
+ {
+ int nameOffset = _offset + BitConverter.ToInt32( _data.Slice( _offset, 4 ) );
+ int end = nameOffset;
+ while ( end < _data.Length && _data[end] != 0 ) end++;
+ return Encoding.ASCII.GetString( _data.Slice( nameOffset, end - nameOffset ) );
+ }
+ }
+}
+
+///
+/// mstudiovertex_t - Vertex data from VVD file (48 bytes)
+///
+internal readonly struct StudioVertex
+{
+ public readonly float[] BoneWeights;
+ public readonly byte[] BoneIds;
+ public readonly byte NumBones;
+ public readonly Vector3 Position;
+ public readonly Vector3 Normal;
+ public readonly Vector2 TexCoord;
+
+ public StudioVertex( ReadOnlySpan data, int offset )
+ {
+ BoneWeights = [
+ BitConverter.ToSingle( data.Slice( offset, 4 ) ),
+ BitConverter.ToSingle( data.Slice( offset + 4, 4 ) ),
+ BitConverter.ToSingle( data.Slice( offset + 8, 4 ) )
+ ];
+
+ BoneIds = [data[offset + 12], data[offset + 13], data[offset + 14]];
+ NumBones = data[offset + 15];
+
+ Position = new Vector3(
+ BitConverter.ToSingle( data.Slice( offset + 16, 4 ) ),
+ BitConverter.ToSingle( data.Slice( offset + 20, 4 ) ),
+ BitConverter.ToSingle( data.Slice( offset + 24, 4 ) )
+ );
+
+ Normal = new Vector3(
+ BitConverter.ToSingle( data.Slice( offset + 28, 4 ) ),
+ BitConverter.ToSingle( data.Slice( offset + 32, 4 ) ),
+ BitConverter.ToSingle( data.Slice( offset + 36, 4 ) )
+ );
+
+ TexCoord = new Vector2(
+ BitConverter.ToSingle( data.Slice( offset + 40, 4 ) ),
+ BitConverter.ToSingle( data.Slice( offset + 44, 4 ) )
+ );
+ }
+}
+
+///
+/// vertexFileHeader_t - VVD file header
+///
+internal readonly struct VvdFileHeader( byte[] data )
+{
+ private readonly byte[] _data = data;
+
+ public int Id => BitConverter.ToInt32( _data, 0 );
+ public int Version => BitConverter.ToInt32( _data, 4 );
+ public int Checksum => BitConverter.ToInt32( _data, 8 );
+ public int LodCount => BitConverter.ToInt32( _data, 12 );
+ public int FixupCount => BitConverter.ToInt32( _data, 48 );
+ public int FixupTableOffset => BitConverter.ToInt32( _data, 52 );
+ public int VertexDataOffset => BitConverter.ToInt32( _data, 56 );
+ public int TangentDataOffset => BitConverter.ToInt32( _data, 60 );
+
+ public int GetLodVertexCount( int lod ) => BitConverter.ToInt32( _data, 16 + lod * 4 );
+
+ public StudioVertex[] GetVertices( int rootLod )
+ {
+ int totalVerts = GetLodVertexCount( rootLod );
+ var result = new StudioVertex[totalVerts];
+
+ if ( FixupCount == 0 )
+ {
+ for ( int i = 0; i < totalVerts; i++ )
+ result[i] = new StudioVertex( _data, VertexDataOffset + i * 48 );
+ }
+ else
+ {
+ int destIndex = 0;
+ for ( int f = 0; f < FixupCount; f++ )
+ {
+ int fixupOffset = FixupTableOffset + f * 12;
+ int fixupLod = BitConverter.ToInt32( _data, fixupOffset );
+ int vertex = BitConverter.ToInt32( _data, fixupOffset + 4 );
+ int vertexCount = BitConverter.ToInt32( _data, fixupOffset + 8 );
+
+ if ( fixupLod >= rootLod )
+ {
+ for ( int v = 0; v < vertexCount; v++ )
+ result[destIndex++] = new StudioVertex( _data, VertexDataOffset + (vertex + v) * 48 );
+ }
+ }
+ }
+
+ return result;
+ }
+
+ public Vector4[] GetTangents( int rootLod )
+ {
+ int totalVerts = GetLodVertexCount( rootLod );
+ var result = new Vector4[totalVerts];
+
+ if ( FixupCount == 0 )
+ {
+ for ( int i = 0; i < totalVerts; i++ )
+ {
+ int offset = TangentDataOffset + i * 16;
+ result[i] = new Vector4(
+ BitConverter.ToSingle( _data, offset ),
+ BitConverter.ToSingle( _data, offset + 4 ),
+ BitConverter.ToSingle( _data, offset + 8 ),
+ BitConverter.ToSingle( _data, offset + 12 )
+ );
+ }
+ }
+ else
+ {
+ int destIndex = 0;
+ for ( int f = 0; f < FixupCount; f++ )
+ {
+ int fixupOffset = FixupTableOffset + f * 12;
+ int fixupLod = BitConverter.ToInt32( _data, fixupOffset );
+ int vertex = BitConverter.ToInt32( _data, fixupOffset + 4 );
+ int vertexCount = BitConverter.ToInt32( _data, fixupOffset + 8 );
+
+ if ( fixupLod >= rootLod )
+ {
+ for ( int v = 0; v < vertexCount; v++ )
+ {
+ int offset = TangentDataOffset + (vertex + v) * 16;
+ result[destIndex++] = new Vector4(
+ BitConverter.ToSingle( _data, offset ),
+ BitConverter.ToSingle( _data, offset + 4 ),
+ BitConverter.ToSingle( _data, offset + 8 ),
+ BitConverter.ToSingle( _data, offset + 12 )
+ );
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+}
+
+///
+/// OptimizedModel::FileHeader_t - VTX file header
+///
+internal readonly struct VtxFileHeader( byte[] data )
+{
+ private readonly byte[] _data = data;
+
+ public int Version => BitConverter.ToInt32( _data, 0 );
+ public int Checksum => BitConverter.ToInt32( _data, 16 );
+ public int LodCount => BitConverter.ToInt32( _data, 20 );
+ public int BodyPartCount => BitConverter.ToInt32( _data, 28 );
+ public int BodyPartOffset => BitConverter.ToInt32( _data, 32 );
+
+ public VtxBodyPartHeader GetBodyPart( int index )
+ {
+ int offset = BodyPartOffset + index * 8;
+ return new VtxBodyPartHeader( _data, offset );
+ }
+}
+
+///
+/// OptimizedModel::BodyPartHeader_t - VTX body part (8 bytes)
+///
+internal readonly struct VtxBodyPartHeader( byte[] data, int offset )
+{
+ private readonly byte[] _data = data;
+ private readonly int _offset = offset;
+
+ public int ModelCount => BitConverter.ToInt32( _data, _offset );
+ private int ModelOffset => BitConverter.ToInt32( _data, _offset + 4 );
+
+ public VtxModelHeader GetModel( int index )
+ {
+ int offset = _offset + ModelOffset + index * 8;
+ return new VtxModelHeader( _data, offset );
+ }
+}
+
+///
+/// OptimizedModel::ModelHeader_t - VTX model (8 bytes)
+///
+internal readonly struct VtxModelHeader( byte[] data, int offset )
+{
+ private readonly byte[] _data = data;
+ private readonly int _offset = offset;
+
+ public int LodCount => BitConverter.ToInt32( _data, _offset );
+ private int LodOffset => BitConverter.ToInt32( _data, _offset + 4 );
+
+ public VtxModelLODHeader GetLod( int index )
+ {
+ int offset = _offset + LodOffset + index * 12;
+ return new VtxModelLODHeader( _data, offset );
+ }
+}
+
+///
+/// OptimizedModel::ModelLODHeader_t - VTX LOD (12 bytes)
+///
+internal readonly struct VtxModelLODHeader( byte[] data, int offset )
+{
+ private readonly byte[] _data = data;
+ private readonly int _offset = offset;
+
+ public int MeshCount => BitConverter.ToInt32( _data, _offset );
+ private int MeshOffset => BitConverter.ToInt32( _data, _offset + 4 );
+
+ public VtxMeshHeader GetMesh( int index )
+ {
+ int offset = _offset + MeshOffset + index * 9;
+ return new VtxMeshHeader( _data, offset );
+ }
+}
+
+///
+/// OptimizedModel::MeshHeader_t - VTX mesh (9 bytes)
+///
+internal readonly struct VtxMeshHeader( byte[] data, int offset )
+{
+ private readonly byte[] _data = data;
+ private readonly int _offset = offset;
+
+ public int StripGroupCount => BitConverter.ToInt32( _data, _offset );
+ private int StripGroupOffset => BitConverter.ToInt32( _data, _offset + 4 );
+
+ public VtxStripGroupHeader GetStripGroup( int index )
+ {
+ int offset = _offset + StripGroupOffset + index * 25;
+ return new VtxStripGroupHeader( _data, offset );
+ }
+}
+
+///
+/// OptimizedModel::StripGroupHeader_t - VTX strip group (25 bytes)
+///
+internal readonly struct VtxStripGroupHeader( byte[] data, int offset )
+{
+ private readonly byte[] _data = data;
+ private readonly int _offset = offset;
+
+ public int VertexCount => BitConverter.ToInt32( _data, _offset );
+ private int VertexOffset => BitConverter.ToInt32( _data, _offset + 4 );
+ public int IndexCount => BitConverter.ToInt32( _data, _offset + 8 );
+ private int IndexOffset => BitConverter.ToInt32( _data, _offset + 12 );
+ public int StripCount => BitConverter.ToInt32( _data, _offset + 16 );
+ private int StripOffset => BitConverter.ToInt32( _data, _offset + 20 );
+
+ public VtxVertex GetVertex( int index )
+ {
+ int offset = _offset + VertexOffset + index * 9;
+ return new VtxVertex( _data, offset );
+ }
+
+ public int GetIndex( int index )
+ {
+ int offset = _offset + IndexOffset + index * 2;
+ return BitConverter.ToUInt16( _data, offset );
+ }
+
+ public VtxStripHeader GetStrip( int index )
+ {
+ int offset = _offset + StripOffset + index * 27;
+ return new VtxStripHeader( _data, offset );
+ }
+}
+
+///
+/// OptimizedModel::StripHeader_t - VTX strip (27 bytes)
+/// Flags: STRIP_IS_TRILIST = 0x01, STRIP_IS_TRISTRIP = 0x02
+///
+internal readonly struct VtxStripHeader( byte[] data, int offset )
+{
+ private readonly byte[] _data = data;
+ private readonly int _offset = offset;
+
+ public int IndexCount => BitConverter.ToInt32( _data, _offset );
+ public int IndexOffset => BitConverter.ToInt32( _data, _offset + 4 );
+ public int VertexCount => BitConverter.ToInt32( _data, _offset + 8 );
+ public int VertexOffset => BitConverter.ToInt32( _data, _offset + 12 );
+ public short BoneCount => BitConverter.ToInt16( _data, _offset + 16 );
+ public byte Flags => _data[_offset + 18];
+}
+
+///
+/// OptimizedModel::Vertex_t - VTX vertex reference (9 bytes)
+///
+internal readonly struct VtxVertex( byte[] data, int offset )
+{
+ public readonly int OrigMeshVertId = BitConverter.ToUInt16( data, offset + 4 );
+}
+
+///
+/// mstudioanimdesc_t - Animation description (100 bytes)
+///
+internal readonly ref struct StudioAnimDesc
+{
+ public const int Size = 100;
+ private readonly ReadOnlySpan _data;
+ private readonly int _offset;
+
+ public StudioAnimDesc( ReadOnlySpan data, int offset )
+ {
+ _data = data;
+ _offset = offset;
+ }
+
+ private int NameOffset => BitConverter.ToInt32( _data.Slice( _offset + 4, 4 ) );
+ public float Fps => BitConverter.ToSingle( _data.Slice( _offset + 8, 4 ) );
+ public int Flags => BitConverter.ToInt32( _data.Slice( _offset + 12, 4 ) );
+ public int NumFrames => BitConverter.ToInt32( _data.Slice( _offset + 16, 4 ) );
+ public int AnimBlock => BitConverter.ToInt32( _data.Slice( _offset + 52, 4 ) );
+ public int AnimIndex => BitConverter.ToInt32( _data.Slice( _offset + 56, 4 ) );
+ public int SectionIndex => BitConverter.ToInt32( _data.Slice( _offset + 80, 4 ) );
+ public int SectionFrames => BitConverter.ToInt32( _data.Slice( _offset + 84, 4 ) );
+
+ public string Name
+ {
+ get
+ {
+ int offset = _offset + NameOffset;
+ int end = offset;
+ while ( end < _data.Length && _data[end] != 0 ) end++;
+ return Encoding.ASCII.GetString( _data[offset..end] );
+ }
+ }
+
+ public int AnimDataOffset => _offset + AnimIndex;
+
+ public (int block, int index) GetAnimBlockForFrame( int frame )
+ {
+ if ( SectionFrames == 0 )
+ {
+ return (AnimBlock, AnimIndex);
+ }
+
+ int section = NumFrames > SectionFrames && frame == NumFrames - 1
+ ? (NumFrames / SectionFrames) + 1
+ : frame / SectionFrames;
+ int sectionOffset = _offset + SectionIndex + section * 8;
+ int sectionBlock = BitConverter.ToInt32( _data.Slice( sectionOffset, 4 ) );
+ int sectionAnimIndex = BitConverter.ToInt32( _data.Slice( sectionOffset + 4, 4 ) );
+
+ return (sectionBlock, sectionAnimIndex);
+ }
+
+ public int GetSectionRelativeFrame( int frame )
+ {
+ return SectionFrames == 0
+ ? frame
+ : NumFrames > SectionFrames && frame == NumFrames - 1
+ ? 0
+ : frame % SectionFrames;
+ }
+}
+
+///
+/// mstudioseqdesc_t - Sequence description (212 bytes)
+///
+internal readonly ref struct StudioSeqDesc
+{
+ public const int Size = 212;
+ private readonly ReadOnlySpan _data;
+ private readonly int _offset;
+
+ public StudioSeqDesc( ReadOnlySpan data, int offset )
+ {
+ _data = data;
+ _offset = offset;
+ }
+
+ private int LabelOffset => BitConverter.ToInt32( _data.Slice( _offset + 4, 4 ) );
+ private int ActivityNameOffset => BitConverter.ToInt32( _data.Slice( _offset + 8, 4 ) );
+ public int Flags => BitConverter.ToInt32( _data.Slice( _offset + 12, 4 ) );
+ public int Activity => BitConverter.ToInt32( _data.Slice( _offset + 16, 4 ) );
+ public int ActivityWeight => BitConverter.ToInt32( _data.Slice( _offset + 20, 4 ) );
+ public int NumBlends => BitConverter.ToInt32( _data.Slice( _offset + 56, 4 ) );
+ private int AnimIndexOffset => BitConverter.ToInt32( _data.Slice( _offset + 60, 4 ) );
+ public int GroupSize0 => BitConverter.ToInt32( _data.Slice( _offset + 68, 4 ) );
+ public int GroupSize1 => BitConverter.ToInt32( _data.Slice( _offset + 72, 4 ) );
+ public float FadeInTime => BitConverter.ToSingle( _data.Slice( _offset + 104, 4 ) );
+ public float FadeOutTime => BitConverter.ToSingle( _data.Slice( _offset + 108, 4 ) );
+
+ public bool Looping => (Flags & 0x0001) != 0;
+ public bool Delta => (Flags & 0x0004) != 0;
+
+ public string Label
+ {
+ get
+ {
+ int offset = _offset + LabelOffset;
+ int end = offset;
+ while ( end < _data.Length && _data[end] != 0 ) end++;
+ return Encoding.ASCII.GetString( _data[offset..end] );
+ }
+ }
+
+ public string ActivityName
+ {
+ get
+ {
+ int offset = _offset + ActivityNameOffset;
+ int end = offset;
+ while ( end < _data.Length && _data[end] != 0 ) end++;
+ return Encoding.ASCII.GetString( _data[offset..end] );
+ }
+ }
+
+ public int GetAnimIndex( int x, int y )
+ {
+ int offset = _offset + AnimIndexOffset + (y * GroupSize0 + x) * 2;
+ return BitConverter.ToInt16( _data.Slice( offset, 2 ) );
+ }
+}
+
+internal static class StudioAnimFlags
+{
+ public const byte RawPos = 0x01;
+ public const byte RawRot = 0x02;
+ public const byte AnimPos = 0x04;
+ public const byte AnimRot = 0x08;
+ public const byte Delta = 0x10;
+ public const byte RawRot2 = 0x20;
+}
+
+internal ref struct StudioAnimReader
+{
+ private readonly ReadOnlySpan _data;
+ private int _offset;
+
+ public StudioAnimReader( ReadOnlySpan data, int offset )
+ {
+ _data = data;
+ _offset = offset;
+ }
+
+ public bool ReadNext( out int bone, out byte flags, out Vector3 position, out Rotation rotation )
+ {
+ bone = _data[_offset];
+ flags = _data[_offset + 1];
+ short nextOffset = BitConverter.ToInt16( _data.Slice( _offset + 2, 2 ) );
+
+ position = Vector3.Zero;
+ rotation = Rotation.Identity;
+
+ int dataOffset = _offset + 4;
+
+ if ( (flags & StudioAnimFlags.RawRot) != 0 )
+ {
+ rotation = DecodeQuaternion48( _data, dataOffset );
+ dataOffset += 6;
+ }
+ else if ( (flags & StudioAnimFlags.RawRot2) != 0 )
+ {
+ rotation = DecodeQuaternion64( _data, dataOffset );
+ dataOffset += 8;
+ }
+
+ if ( (flags & StudioAnimFlags.RawPos) != 0 )
+ {
+ position = DecodeVector48( _data, dataOffset );
+ }
+
+ if ( nextOffset == 0 )
+ {
+ return false;
+ }
+
+ _offset += nextOffset;
+ return true;
+ }
+
+ public static Rotation DecodeQuaternion48( ReadOnlySpan data, int offset )
+ {
+ ushort xRaw = BitConverter.ToUInt16( data.Slice( offset, 2 ) );
+ ushort yRaw = BitConverter.ToUInt16( data.Slice( offset + 2, 2 ) );
+ ushort zRaw = BitConverter.ToUInt16( data.Slice( offset + 4, 2 ) );
+
+ float x = (xRaw - 32768) * (1.0f / 32768.0f);
+ float y = (yRaw - 32768) * (1.0f / 32768.0f);
+ float z = ((zRaw & 0x7FFF) - 16384) * (1.0f / 16384.0f);
+
+ bool wNeg = (zRaw & 0x8000) != 0;
+ float wSq = 1.0f - x * x - y * y - z * z;
+ float w = MathF.Sqrt( MathF.Max( 0, wSq ) );
+ if ( wNeg ) w = -w;
+
+ return new Rotation( x, y, z, w );
+ }
+
+ public static Rotation DecodeQuaternion64( ReadOnlySpan data, int offset )
+ {
+ ulong packed = BitConverter.ToUInt64( data.Slice( offset, 8 ) );
+
+ int xRaw = (int)(packed & 0x1FFFFF);
+ int yRaw = (int)((packed >> 21) & 0x1FFFFF);
+ int zRaw = (int)((packed >> 42) & 0x1FFFFF);
+ bool wNeg = ((packed >> 63) & 1) != 0;
+
+ float x = (xRaw - 1048576) * (1.0f / 1048576.5f);
+ float y = (yRaw - 1048576) * (1.0f / 1048576.5f);
+ float z = (zRaw - 1048576) * (1.0f / 1048576.5f);
+
+ float wSq = 1.0f - x * x - y * y - z * z;
+ float w = MathF.Sqrt( MathF.Max( 0, wSq ) );
+ if ( wNeg ) w = -w;
+
+ return new Rotation( x, y, z, w );
+ }
+
+ public static Vector3 DecodeVector48( ReadOnlySpan data, int offset )
+ {
+ float x = HalfToFloat( BitConverter.ToUInt16( data.Slice( offset, 2 ) ) );
+ float y = HalfToFloat( BitConverter.ToUInt16( data.Slice( offset + 2, 2 ) ) );
+ float z = HalfToFloat( BitConverter.ToUInt16( data.Slice( offset + 4, 2 ) ) );
+ return new Vector3( x, y, z );
+ }
+
+ private static float HalfToFloat( ushort half )
+ {
+ return (float)BitConverter.UInt16BitsToHalf( half );
+ }
+}
+
diff --git a/engine/Mounting/Sandbox.Mounting.HL2/Resource/HL2Sound.cs b/engine/Mounting/Sandbox.Mounting.HL2/Resource/HL2Sound.cs
new file mode 100644
index 000000000..7975d9133
--- /dev/null
+++ b/engine/Mounting/Sandbox.Mounting.HL2/Resource/HL2Sound.cs
@@ -0,0 +1,45 @@
+using Sandbox;
+using ValvePak;
+
+internal class HL2Sound : ResourceLoader
+{
+ private readonly VpkArchive _package;
+ private readonly VpkEntry _entry;
+ private readonly string _filePath;
+
+ public HL2Sound( VpkArchive package, VpkEntry entry )
+ {
+ _package = package;
+ _entry = entry;
+ }
+
+ public HL2Sound( string filePath )
+ {
+ _filePath = filePath;
+ }
+
+ protected override object Load()
+ {
+ byte[] data;
+ if ( _package != null )
+ {
+ _package.ReadEntry( _entry, out data );
+ }
+ else
+ {
+ data = File.ReadAllBytes( _filePath );
+ }
+ return LoadSound( data, Path );
+ }
+
+ internal static SoundFile LoadSound( byte[] data, string path )
+ {
+ return data == null || data.Length < 4
+ ? null
+ : data[0] == 'R' && data[1] == 'I' && data[2] == 'F' && data[3] == 'F'
+ ? SoundFile.FromWav( path, data, false )
+ : data[0] == 'I' && data[1] == 'D' && data[2] == '3' || data[0] == 0xFF && (data[1] & 0xE0) == 0xE0
+ ? SoundFile.FromMp3( path, data, false )
+ : null;
+ }
+}
diff --git a/engine/Mounting/Sandbox.Mounting.HL2/Resource/HL2Texture.cs b/engine/Mounting/Sandbox.Mounting.HL2/Resource/HL2Texture.cs
new file mode 100644
index 000000000..c297d8518
--- /dev/null
+++ b/engine/Mounting/Sandbox.Mounting.HL2/Resource/HL2Texture.cs
@@ -0,0 +1,1148 @@
+using System;
+using Sandbox;
+using ValvePak;
+
+internal class HL2Texture : ResourceLoader
+{
+ private readonly VpkArchive _package;
+ private readonly VpkEntry _entry;
+ private readonly string _filePath;
+
+ public HL2Texture( VpkArchive package, VpkEntry entry )
+ {
+ _package = package;
+ _entry = entry;
+ }
+
+ public HL2Texture( string filePath )
+ {
+ _filePath = filePath;
+ }
+
+ protected override object Load()
+ {
+ byte[] data;
+ if ( _package != null )
+ {
+ _package.ReadEntry( _entry, out data );
+ }
+ else
+ {
+ data = File.ReadAllBytes( _filePath );
+ }
+ return VtfLoader.Load( data );
+ }
+}
+
+internal static class VtfLoader
+{
+ private enum VtfImageFormat
+ {
+ Unknown = -1,
+ RGBA8888 = 0,
+ ABGR8888 = 1,
+ RGB888 = 2,
+ BGR888 = 3,
+ RGB565 = 4,
+ I8 = 5,
+ IA88 = 6,
+ P8 = 7,
+ A8 = 8,
+ RGB888_BLUESCREEN = 9,
+ BGR888_BLUESCREEN = 10,
+ ARGB8888 = 11,
+ BGRA8888 = 12,
+ DXT1 = 13,
+ DXT3 = 14,
+ DXT5 = 15,
+ BGRX8888 = 16,
+ BGR565 = 17,
+ BGRX5551 = 18,
+ BGRA4444 = 19,
+ DXT1_ONEBITALPHA = 20,
+ BGRA5551 = 21,
+ UV88 = 22,
+ UVWQ8888 = 23,
+ RGBA16161616F = 24,
+ RGBA16161616 = 25,
+ UVLX8888 = 26,
+ R32F = 27,
+ RGB323232F = 28,
+ RGBA32323232F = 29,
+ // 30-33: Depth-stencil formats (NV_DST16, NV_DST24, NV_INTZ, NV_RAWZ, ATI_DST16, ATI_DST24, NV_NULL) - not used in VTF files
+ ATI2N = 34,
+ ATI1N = 35,
+ }
+
+ [Flags]
+ private enum TextureFlags : uint
+ {
+ PointSample = 0x00000001,
+ Trilinear = 0x00000002,
+ ClampS = 0x00000004,
+ ClampT = 0x00000008,
+ Anisotropic = 0x00000010,
+ HintDxt5 = 0x00000020,
+ Srgb = 0x00000040,
+ Normal = 0x00000080,
+ NoMip = 0x00000100,
+ NoLod = 0x00000200,
+ AllMips = 0x00000400,
+ Procedural = 0x00000800,
+ OneBitAlpha = 0x00001000,
+ EightBitAlpha = 0x00002000,
+ EnvMap = 0x00004000,
+ RenderTarget = 0x00008000,
+ DepthRenderTarget = 0x00010000,
+ NoDebugOverride = 0x00020000,
+ SingleCopy = 0x00040000,
+ StagingMemory = 0x00080000,
+ ImmediateCleanup = 0x00100000,
+ IgnorePicmip = 0x00200000,
+ NoDepthBuffer = 0x00800000,
+ ClampU = 0x02000000,
+ VertexTexture = 0x04000000,
+ SSBump = 0x08000000,
+ Border = 0x20000000,
+ StreamableCoarse = 0x40000000,
+ StreamableFine = 0x80000000,
+ }
+
+ private struct VtfHeader
+ {
+ public int Width;
+ public int Height;
+ public int Depth;
+ public TextureFlags Flags;
+ public int FrameCount;
+ public ushort FirstFrame;
+ public int VersionMinor;
+ public VtfImageFormat ImageFormat;
+ public int MipCount;
+ public int ImageDataOffset;
+ }
+
+ public static Texture Load( byte[] data )
+ {
+ return !TryParseHeader( data, out var header )
+ ? null
+ : header.Flags.HasFlag( TextureFlags.EnvMap )
+ ? LoadCubemap( data, header )
+ : LoadTexture2D( data, header );
+ }
+
+ private static bool TryParseHeader( byte[] data, out VtfHeader header )
+ {
+ header = default;
+
+ if ( data == null || data.Length < 64 )
+ return false;
+
+ using var stream = new MemoryStream( data );
+ using var reader = new BinaryReader( stream );
+
+ // VTFFileBaseHeader_t
+ var signature = reader.ReadBytes( 4 );
+ if ( signature[0] != 'V' || signature[1] != 'T' || signature[2] != 'F' || signature[3] != 0 )
+ return false;
+
+ var versionMajor = reader.ReadInt32();
+ var versionMinor = reader.ReadInt32();
+ var headerSize = reader.ReadInt32();
+
+ if ( versionMajor != 7 || versionMinor < 0 || versionMinor > 5 )
+ return false;
+
+ // VTFFileHeaderV7_1_t
+ header.VersionMinor = versionMinor;
+ header.Width = reader.ReadUInt16();
+ header.Height = reader.ReadUInt16();
+ header.Flags = (TextureFlags)reader.ReadUInt32();
+ header.FrameCount = reader.ReadUInt16();
+ header.FirstFrame = reader.ReadUInt16();
+
+ reader.ReadBytes( 4 ); // padding before reflectivity (VectorAligned)
+ reader.ReadBytes( 12 ); // reflectivity Vector (3 floats)
+ reader.ReadBytes( 4 ); // padding after reflectivity
+
+ reader.ReadSingle(); // bumpScale
+ header.ImageFormat = (VtfImageFormat)reader.ReadInt32();
+ header.MipCount = reader.ReadByte();
+
+ // Low-res thumbnail info (DXT1 format, used for fast loading)
+ var lowResFormat = (VtfImageFormat)reader.ReadInt32();
+ var lowResWidth = reader.ReadByte();
+ var lowResHeight = reader.ReadByte();
+
+ header.Depth = 1;
+ uint numResources = 0;
+
+ // VTFFileHeaderV7_2_t adds depth
+ if ( versionMinor >= 2 )
+ {
+ header.Depth = reader.ReadUInt16();
+ }
+
+ // VTFFileHeaderV7_3_t adds resource entries
+ if ( versionMinor >= 3 )
+ {
+ reader.ReadBytes( 3 ); // padding
+ numResources = reader.ReadUInt32();
+ reader.ReadBytes( 8 ); // padding for alignment
+ }
+
+ int lowResDataSize = CalculateImageSize( lowResFormat, lowResWidth, lowResHeight );
+
+ header.ImageDataOffset = versionMinor >= 3 && numResources > 0
+ ? FindImageDataOffset( reader, numResources, headerSize )
+ : headerSize + lowResDataSize;
+
+ return true;
+ }
+
+ private static Texture LoadTexture2D( byte[] data, VtfHeader header )
+ {
+ using var stream = new MemoryStream( data );
+ using var reader = new BinaryReader( stream );
+
+ int highResMipOffset = CalculateMipDataOffset( header.ImageFormat, header.Width, header.Height, header.Depth, header.MipCount, header.FrameCount, 1 );
+
+ long targetPosition = header.ImageDataOffset + highResMipOffset;
+ if ( targetPosition >= data.Length )
+ return null;
+ stream.Position = targetPosition;
+
+ int imageSize = CalculateImageSize( header.ImageFormat, header.Width, header.Height ) * header.Depth;
+ if ( stream.Position + imageSize > data.Length )
+ return null;
+
+ byte[] imageData = reader.ReadBytes( imageSize );
+
+ var rgbaData = ConvertToRgba( imageData, header.ImageFormat, header.Width, header.Height );
+ if ( rgbaData == null || rgbaData.Length == 0 )
+ return null;
+
+ var builder = Texture.Create( header.Width, header.Height )
+ .WithData( rgbaData );
+
+ if ( !header.Flags.HasFlag( TextureFlags.NoMip ) && header.MipCount > 1 )
+ {
+ builder = builder.WithMips();
+ }
+
+ return builder.Finish();
+ }
+
+ private static Texture LoadCubemap( byte[] data, VtfHeader header )
+ {
+ using var stream = new MemoryStream( data );
+ using var reader = new BinaryReader( stream );
+
+ // Older cubemaps (pre-7.5) with firstFrame == 0xFFFF have a 7th spheremap face
+ int faceCount = header.FirstFrame == 0xFFFF && header.VersionMinor < 5 ? 7 : 6;
+ int highResMipOffset = CalculateMipDataOffset( header.ImageFormat, header.Width, header.Height, header.Depth, header.MipCount, header.FrameCount, faceCount );
+
+ long targetPosition = header.ImageDataOffset + highResMipOffset;
+ if ( targetPosition >= data.Length )
+ return null;
+ stream.Position = targetPosition;
+
+ int faceSize = CalculateImageSize( header.ImageFormat, header.Width, header.Height );
+ int totalSize = faceSize * faceCount;
+
+ if ( stream.Position + totalSize > data.Length )
+ return null;
+
+ var rgbaFaces = new byte[6][];
+ int rgbaFaceSize = header.Width * header.Height * 4;
+
+ for ( int face = 0; face < 6; face++ )
+ {
+ var faceData = reader.ReadBytes( faceSize );
+ rgbaFaces[face] = ConvertToRgba( faceData, header.ImageFormat, header.Width, header.Height );
+
+ if ( rgbaFaces[face] == null )
+ return null;
+ }
+
+ var combinedData = new byte[rgbaFaceSize * 6];
+ for ( int face = 0; face < 6; face++ )
+ {
+ Array.Copy( rgbaFaces[face], 0, combinedData, face * rgbaFaceSize, rgbaFaceSize );
+ }
+
+ return Texture.CreateCube( header.Width, header.Height, ImageFormat.RGBA8888 )
+ .WithData( combinedData )
+ .Finish();
+ }
+
+ private static int FindImageDataOffset( BinaryReader reader, uint numResources, int headerSize )
+ {
+ const uint VTF_LEGACY_RSRC_IMAGE = 0x00000030;
+
+ for ( int i = 0; i < numResources; i++ )
+ {
+ var type = reader.ReadUInt32();
+ var data = reader.ReadUInt32();
+
+ // Strip flags from type
+ var resourceType = type & 0x00FFFFFF;
+
+ if ( resourceType == VTF_LEGACY_RSRC_IMAGE )
+ {
+ return (int)data;
+ }
+ }
+
+ return headerSize;
+ }
+
+ private static int CalculateMipDataOffset( VtfImageFormat format, int width, int height, int depth, int mipCount, int frameCount, int faceCount )
+ {
+ int offset = 0;
+
+ for ( int mip = mipCount - 1; mip > 0; mip-- )
+ {
+ int mipWidth = Math.Max( 1, width >> mip );
+ int mipHeight = Math.Max( 1, height >> mip );
+ int mipDepth = Math.Max( 1, depth >> mip );
+
+ int mipSize = CalculateImageSize( format, mipWidth, mipHeight ) * mipDepth;
+ offset += mipSize * faceCount * frameCount;
+ }
+
+ return offset;
+ }
+
+ private static int CalculateImageSize( VtfImageFormat format, int width, int height )
+ {
+ int blockWidth = Math.Max( 4, width );
+ int blockHeight = Math.Max( 4, height );
+ int numBlocksX = (blockWidth + 3) / 4;
+ int numBlocksY = (blockHeight + 3) / 4;
+
+ return format switch
+ {
+ VtfImageFormat.DXT1 or VtfImageFormat.DXT1_ONEBITALPHA or VtfImageFormat.ATI1N =>
+ numBlocksX * numBlocksY * 8,
+
+ VtfImageFormat.DXT3 or VtfImageFormat.DXT5 or VtfImageFormat.ATI2N =>
+ numBlocksX * numBlocksY * 16,
+
+ VtfImageFormat.RGBA8888 or VtfImageFormat.ABGR8888 or VtfImageFormat.ARGB8888 or
+ VtfImageFormat.BGRA8888 or VtfImageFormat.BGRX8888 or VtfImageFormat.UVWQ8888 or
+ VtfImageFormat.UVLX8888 =>
+ width * height * 4,
+
+ VtfImageFormat.RGB888 or VtfImageFormat.BGR888 or VtfImageFormat.RGB888_BLUESCREEN or
+ VtfImageFormat.BGR888_BLUESCREEN =>
+ width * height * 3,
+
+ VtfImageFormat.RGB565 or VtfImageFormat.BGR565 or VtfImageFormat.BGRA4444 or
+ VtfImageFormat.BGRA5551 or VtfImageFormat.BGRX5551 or VtfImageFormat.IA88 or
+ VtfImageFormat.UV88 =>
+ width * height * 2,
+
+ VtfImageFormat.I8 or VtfImageFormat.A8 or VtfImageFormat.P8 =>
+ width * height,
+
+ VtfImageFormat.RGBA16161616 or VtfImageFormat.RGBA16161616F =>
+ width * height * 8,
+
+ VtfImageFormat.R32F =>
+ width * height * 4,
+
+ VtfImageFormat.RGB323232F =>
+ width * height * 12,
+
+ VtfImageFormat.RGBA32323232F =>
+ width * height * 16,
+
+ _ => width * height * 4
+ };
+ }
+
+ private static byte[] ConvertToRgba( byte[] data, VtfImageFormat format, int width, int height )
+ {
+ return format switch
+ {
+ VtfImageFormat.DXT1 => DecompressDxt1( data, width, height ),
+ VtfImageFormat.DXT1_ONEBITALPHA => DecompressDxt1( data, width, height, hasAlpha: true ),
+ VtfImageFormat.DXT3 => DecompressDxt3( data, width, height ),
+ VtfImageFormat.DXT5 => DecompressDxt5( data, width, height ),
+ VtfImageFormat.RGBA8888 => data,
+ VtfImageFormat.BGRA8888 => ConvertBgraToRgba( data ),
+ VtfImageFormat.ABGR8888 => ConvertAbgrToRgba( data ),
+ VtfImageFormat.ARGB8888 => ConvertArgbToRgba( data ),
+ VtfImageFormat.RGB888 or VtfImageFormat.RGB888_BLUESCREEN => ConvertRgb888ToRgba( data ),
+ VtfImageFormat.BGR888 or VtfImageFormat.BGR888_BLUESCREEN => ConvertBgr888ToRgba( data ),
+ VtfImageFormat.BGRX8888 => ConvertBgrxToRgba( data ),
+ VtfImageFormat.I8 or VtfImageFormat.P8 => ConvertI8ToRgba( data ),
+ VtfImageFormat.IA88 => ConvertIa88ToRgba( data ),
+ VtfImageFormat.A8 => ConvertA8ToRgba( data ),
+ VtfImageFormat.RGB565 => ConvertRgb565ToRgba( data ),
+ VtfImageFormat.BGR565 => ConvertBgr565ToRgba( data ),
+ VtfImageFormat.BGRA4444 => ConvertBgra4444ToRgba( data ),
+ VtfImageFormat.BGRA5551 => ConvertBgra5551ToRgba( data ),
+ VtfImageFormat.BGRX5551 => ConvertBgrx5551ToRgba( data ),
+ VtfImageFormat.UV88 => ConvertUv88ToRgba( data ),
+ VtfImageFormat.UVWQ8888 or VtfImageFormat.UVLX8888 => ConvertUvwq8888ToRgba( data ),
+ VtfImageFormat.ATI1N => DecompressAti1n( data, width, height ),
+ VtfImageFormat.ATI2N => DecompressAti2n( data, width, height ),
+ VtfImageFormat.RGBA16161616F => ConvertRgba16161616FToRgba( data, width, height ),
+ VtfImageFormat.RGBA16161616 => ConvertRgba16161616ToRgba( data, width, height ),
+ VtfImageFormat.R32F => ConvertR32FToRgba( data, width, height ),
+ VtfImageFormat.RGB323232F => ConvertRgb323232FToRgba( data, width, height ),
+ VtfImageFormat.RGBA32323232F => ConvertRgba32323232FToRgba( data, width, height ),
+ _ => null
+ };
+ }
+
+ private static byte[] DecompressDxt1( byte[] data, int width, int height, bool hasAlpha = false )
+ {
+ var output = new byte[width * height * 4];
+ int blockCountX = (width + 3) / 4;
+ int blockCountY = (height + 3) / 4;
+ int offset = 0;
+
+ for ( int by = 0; by < blockCountY; by++ )
+ {
+ for ( int bx = 0; bx < blockCountX; bx++ )
+ {
+ ushort c0 = BitConverter.ToUInt16( data, offset );
+ ushort c1 = BitConverter.ToUInt16( data, offset + 2 );
+ uint lookupTable = BitConverter.ToUInt32( data, offset + 4 );
+ offset += 8;
+
+ var colors = new byte[4][];
+ colors[0] = Rgb565ToRgba( c0 );
+ colors[1] = Rgb565ToRgba( c1 );
+
+ if ( c0 > c1 )
+ {
+ colors[2] = LerpColor( colors[0], colors[1], 1, 3 );
+ colors[3] = LerpColor( colors[0], colors[1], 2, 3 );
+ }
+ else
+ {
+ colors[2] = LerpColor( colors[0], colors[1], 1, 2 );
+ colors[3] = hasAlpha ? [0, 0, 0, 0] : LerpColor( colors[0], colors[1], 1, 2 );
+ }
+
+ for ( int y = 0; y < 4; y++ )
+ {
+ for ( int x = 0; x < 4; x++ )
+ {
+ int px = bx * 4 + x;
+ int py = by * 4 + y;
+
+ if ( px >= width || py >= height )
+ continue;
+
+ int index = (int)((lookupTable >> (2 * (y * 4 + x))) & 0x3);
+ int destOffset = (py * width + px) * 4;
+
+ output[destOffset + 0] = colors[index][0];
+ output[destOffset + 1] = colors[index][1];
+ output[destOffset + 2] = colors[index][2];
+ output[destOffset + 3] = colors[index][3];
+ }
+ }
+ }
+ }
+
+ return output;
+ }
+
+ private static byte[] DecompressDxt3( byte[] data, int width, int height )
+ {
+ var output = new byte[width * height * 4];
+ int blockCountX = (width + 3) / 4;
+ int blockCountY = (height + 3) / 4;
+ int offset = 0;
+
+ for ( int by = 0; by < blockCountY; by++ )
+ {
+ for ( int bx = 0; bx < blockCountX; bx++ )
+ {
+ var alphaData = new byte[8];
+ Array.Copy( data, offset, alphaData, 0, 8 );
+ offset += 8;
+
+ ushort c0 = BitConverter.ToUInt16( data, offset );
+ ushort c1 = BitConverter.ToUInt16( data, offset + 2 );
+ uint lookupTable = BitConverter.ToUInt32( data, offset + 4 );
+ offset += 8;
+
+ var colors = new byte[4][];
+ colors[0] = Rgb565ToRgba( c0 );
+ colors[1] = Rgb565ToRgba( c1 );
+ colors[2] = LerpColor( colors[0], colors[1], 1, 3 );
+ colors[3] = LerpColor( colors[0], colors[1], 2, 3 );
+
+ for ( int y = 0; y < 4; y++ )
+ {
+ for ( int x = 0; x < 4; x++ )
+ {
+ int px = bx * 4 + x;
+ int py = by * 4 + y;
+
+ if ( px >= width || py >= height )
+ continue;
+
+ int index = (int)((lookupTable >> (2 * (y * 4 + x))) & 0x3);
+ int destOffset = (py * width + px) * 4;
+
+ int alphaIndex = y * 4 + x;
+ int alphaByte = alphaIndex / 2;
+ int alphaShift = alphaIndex % 2 * 4;
+ int alpha = ((alphaData[alphaByte] >> alphaShift) & 0xF) * 17;
+
+ output[destOffset + 0] = colors[index][0];
+ output[destOffset + 1] = colors[index][1];
+ output[destOffset + 2] = colors[index][2];
+ output[destOffset + 3] = (byte)alpha;
+ }
+ }
+ }
+ }
+
+ return output;
+ }
+
+ private static byte[] DecompressDxt5( byte[] data, int width, int height )
+ {
+ var output = new byte[width * height * 4];
+ int blockCountX = (width + 3) / 4;
+ int blockCountY = (height + 3) / 4;
+ int offset = 0;
+
+ for ( int by = 0; by < blockCountY; by++ )
+ {
+ for ( int bx = 0; bx < blockCountX; bx++ )
+ {
+ byte alpha0 = data[offset];
+ byte alpha1 = data[offset + 1];
+
+ ulong alphaLookup = 0;
+ for ( int i = 0; i < 6; i++ )
+ {
+ alphaLookup |= (ulong)data[offset + 2 + i] << (i * 8);
+ }
+ offset += 8;
+
+ var alphas = new byte[8];
+ alphas[0] = alpha0;
+ alphas[1] = alpha1;
+
+ if ( alpha0 > alpha1 )
+ {
+ for ( int i = 2; i < 8; i++ )
+ {
+ alphas[i] = (byte)(((8 - i) * alpha0 + (i - 1) * alpha1) / 7);
+ }
+ }
+ else
+ {
+ for ( int i = 2; i < 6; i++ )
+ {
+ alphas[i] = (byte)(((6 - i) * alpha0 + (i - 1) * alpha1) / 5);
+ }
+ alphas[6] = 0;
+ alphas[7] = 255;
+ }
+
+ ushort c0 = BitConverter.ToUInt16( data, offset );
+ ushort c1 = BitConverter.ToUInt16( data, offset + 2 );
+ uint lookupTable = BitConverter.ToUInt32( data, offset + 4 );
+ offset += 8;
+
+ var colors = new byte[4][];
+ colors[0] = Rgb565ToRgba( c0 );
+ colors[1] = Rgb565ToRgba( c1 );
+ colors[2] = LerpColor( colors[0], colors[1], 1, 3 );
+ colors[3] = LerpColor( colors[0], colors[1], 2, 3 );
+
+ for ( int y = 0; y < 4; y++ )
+ {
+ for ( int x = 0; x < 4; x++ )
+ {
+ int px = bx * 4 + x;
+ int py = by * 4 + y;
+
+ if ( px >= width || py >= height )
+ continue;
+
+ int colorIndex = (int)((lookupTable >> (2 * (y * 4 + x))) & 0x3);
+ int alphaIndex = (int)((alphaLookup >> (3 * (y * 4 + x))) & 0x7);
+ int destOffset = (py * width + px) * 4;
+
+ output[destOffset + 0] = colors[colorIndex][0];
+ output[destOffset + 1] = colors[colorIndex][1];
+ output[destOffset + 2] = colors[colorIndex][2];
+ output[destOffset + 3] = alphas[alphaIndex];
+ }
+ }
+ }
+ }
+
+ return output;
+ }
+
+ private static byte[] DecompressAti1n( byte[] data, int width, int height )
+ {
+ var output = new byte[width * height * 4];
+ int blockCountX = (width + 3) / 4;
+ int blockCountY = (height + 3) / 4;
+ int offset = 0;
+
+ for ( int by = 0; by < blockCountY; by++ )
+ {
+ for ( int bx = 0; bx < blockCountX; bx++ )
+ {
+ byte red0 = data[offset];
+ byte red1 = data[offset + 1];
+
+ ulong redLookup = 0;
+ for ( int i = 0; i < 6; i++ )
+ {
+ redLookup |= (ulong)data[offset + 2 + i] << (i * 8);
+ }
+ offset += 8;
+
+ var reds = new byte[8];
+ reds[0] = red0;
+ reds[1] = red1;
+
+ if ( red0 > red1 )
+ {
+ for ( int i = 2; i < 8; i++ )
+ {
+ reds[i] = (byte)(((8 - i) * red0 + (i - 1) * red1) / 7);
+ }
+ }
+ else
+ {
+ for ( int i = 2; i < 6; i++ )
+ {
+ reds[i] = (byte)(((6 - i) * red0 + (i - 1) * red1) / 5);
+ }
+ reds[6] = 0;
+ reds[7] = 255;
+ }
+
+ for ( int y = 0; y < 4; y++ )
+ {
+ for ( int x = 0; x < 4; x++ )
+ {
+ int px = bx * 4 + x;
+ int py = by * 4 + y;
+
+ if ( px >= width || py >= height )
+ continue;
+
+ int redIndex = (int)((redLookup >> (3 * (y * 4 + x))) & 0x7);
+ int destOffset = (py * width + px) * 4;
+
+ output[destOffset + 0] = reds[redIndex];
+ output[destOffset + 1] = reds[redIndex];
+ output[destOffset + 2] = reds[redIndex];
+ output[destOffset + 3] = 255;
+ }
+ }
+ }
+ }
+
+ return output;
+ }
+
+ private static byte[] DecompressAti2n( byte[] data, int width, int height )
+ {
+ var output = new byte[width * height * 4];
+ int blockCountX = (width + 3) / 4;
+ int blockCountY = (height + 3) / 4;
+ int offset = 0;
+
+ for ( int by = 0; by < blockCountY; by++ )
+ {
+ for ( int bx = 0; bx < blockCountX; bx++ )
+ {
+ byte red0 = data[offset];
+ byte red1 = data[offset + 1];
+ ulong redLookup = 0;
+ for ( int i = 0; i < 6; i++ )
+ {
+ redLookup |= (ulong)data[offset + 2 + i] << (i * 8);
+ }
+ offset += 8;
+
+ byte green0 = data[offset];
+ byte green1 = data[offset + 1];
+ ulong greenLookup = 0;
+ for ( int i = 0; i < 6; i++ )
+ {
+ greenLookup |= (ulong)data[offset + 2 + i] << (i * 8);
+ }
+ offset += 8;
+
+ var reds = InterpolateAlphaBlock( red0, red1 );
+ var greens = InterpolateAlphaBlock( green0, green1 );
+
+ for ( int y = 0; y < 4; y++ )
+ {
+ for ( int x = 0; x < 4; x++ )
+ {
+ int px = bx * 4 + x;
+ int py = by * 4 + y;
+
+ if ( px >= width || py >= height )
+ continue;
+
+ int redIndex = (int)((redLookup >> (3 * (y * 4 + x))) & 0x7);
+ int greenIndex = (int)((greenLookup >> (3 * (y * 4 + x))) & 0x7);
+ int destOffset = (py * width + px) * 4;
+
+ float nx = reds[redIndex] / 255.0f * 2.0f - 1.0f;
+ float ny = greens[greenIndex] / 255.0f * 2.0f - 1.0f;
+ float nz = (float)Math.Sqrt( Math.Max( 0, 1.0f - nx * nx - ny * ny ) );
+
+ output[destOffset + 0] = reds[redIndex];
+ output[destOffset + 1] = greens[greenIndex];
+ output[destOffset + 2] = (byte)((nz * 0.5f + 0.5f) * 255);
+ output[destOffset + 3] = 255;
+ }
+ }
+ }
+ }
+
+ return output;
+ }
+
+ private static byte[] InterpolateAlphaBlock( byte a0, byte a1 )
+ {
+ var result = new byte[8];
+ result[0] = a0;
+ result[1] = a1;
+
+ if ( a0 > a1 )
+ {
+ for ( int i = 2; i < 8; i++ )
+ {
+ result[i] = (byte)(((8 - i) * a0 + (i - 1) * a1) / 7);
+ }
+ }
+ else
+ {
+ for ( int i = 2; i < 6; i++ )
+ {
+ result[i] = (byte)(((6 - i) * a0 + (i - 1) * a1) / 5);
+ }
+ result[6] = 0;
+ result[7] = 255;
+ }
+
+ return result;
+ }
+
+ private static byte[] Rgb565ToRgba( ushort color )
+ {
+ int r = ((color >> 11) & 0x1F) * 255 / 31;
+ int g = ((color >> 5) & 0x3F) * 255 / 63;
+ int b = (color & 0x1F) * 255 / 31;
+ return [(byte)r, (byte)g, (byte)b, 255];
+ }
+
+ private static byte[] LerpColor( byte[] c0, byte[] c1, int num, int denom )
+ {
+ return
+ [
+ (byte)((c0[0] * (denom - num) + c1[0] * num) / denom),
+ (byte)((c0[1] * (denom - num) + c1[1] * num) / denom),
+ (byte)((c0[2] * (denom - num) + c1[2] * num) / denom),
+ 255
+ ];
+ }
+
+ private static byte[] ConvertBgraToRgba( byte[] data )
+ {
+ var output = new byte[data.Length];
+ for ( int i = 0; i < data.Length; i += 4 )
+ {
+ output[i + 0] = data[i + 2]; // R
+ output[i + 1] = data[i + 1]; // G
+ output[i + 2] = data[i + 0]; // B
+ output[i + 3] = data[i + 3]; // A
+ }
+ return output;
+ }
+
+ private static byte[] ConvertAbgrToRgba( byte[] data )
+ {
+ var output = new byte[data.Length];
+ for ( int i = 0; i < data.Length; i += 4 )
+ {
+ output[i + 0] = data[i + 3]; // R
+ output[i + 1] = data[i + 2]; // G
+ output[i + 2] = data[i + 1]; // B
+ output[i + 3] = data[i + 0]; // A
+ }
+ return output;
+ }
+
+ private static byte[] ConvertArgbToRgba( byte[] data )
+ {
+ var output = new byte[data.Length];
+ for ( int i = 0; i < data.Length; i += 4 )
+ {
+ output[i + 0] = data[i + 1]; // R
+ output[i + 1] = data[i + 2]; // G
+ output[i + 2] = data[i + 3]; // B
+ output[i + 3] = data[i + 0]; // A
+ }
+ return output;
+ }
+
+ private static byte[] ConvertBgrxToRgba( byte[] data )
+ {
+ var output = new byte[data.Length];
+ for ( int i = 0; i < data.Length; i += 4 )
+ {
+ output[i + 0] = data[i + 2]; // R
+ output[i + 1] = data[i + 1]; // G
+ output[i + 2] = data[i + 0]; // B
+ output[i + 3] = 255; // A
+ }
+ return output;
+ }
+
+ private static byte[] ConvertRgb888ToRgba( byte[] data )
+ {
+ var output = new byte[data.Length / 3 * 4];
+ for ( int i = 0, j = 0; i < data.Length; i += 3, j += 4 )
+ {
+ output[j + 0] = data[i + 0];
+ output[j + 1] = data[i + 1];
+ output[j + 2] = data[i + 2];
+ output[j + 3] = 255;
+ }
+ return output;
+ }
+
+ private static byte[] ConvertBgr888ToRgba( byte[] data )
+ {
+ var output = new byte[data.Length / 3 * 4];
+ for ( int i = 0, j = 0; i < data.Length; i += 3, j += 4 )
+ {
+ output[j + 0] = data[i + 2]; // R
+ output[j + 1] = data[i + 1]; // G
+ output[j + 2] = data[i + 0]; // B
+ output[j + 3] = 255;
+ }
+ return output;
+ }
+
+ private static byte[] ConvertI8ToRgba( byte[] data )
+ {
+ var output = new byte[data.Length * 4];
+ for ( int i = 0; i < data.Length; i++ )
+ {
+ output[i * 4 + 0] = data[i];
+ output[i * 4 + 1] = data[i];
+ output[i * 4 + 2] = data[i];
+ output[i * 4 + 3] = 255;
+ }
+ return output;
+ }
+
+ private static byte[] ConvertIa88ToRgba( byte[] data )
+ {
+ var output = new byte[data.Length * 2];
+ for ( int i = 0; i < data.Length; i += 2 )
+ {
+ int j = i * 2;
+ output[j + 0] = data[i];
+ output[j + 1] = data[i];
+ output[j + 2] = data[i];
+ output[j + 3] = data[i + 1];
+ }
+ return output;
+ }
+
+ private static byte[] ConvertA8ToRgba( byte[] data )
+ {
+ var output = new byte[data.Length * 4];
+ for ( int i = 0; i < data.Length; i++ )
+ {
+ output[i * 4 + 0] = 255;
+ output[i * 4 + 1] = 255;
+ output[i * 4 + 2] = 255;
+ output[i * 4 + 3] = data[i];
+ }
+ return output;
+ }
+
+ private static byte[] ConvertRgb565ToRgba( byte[] data )
+ {
+ var output = new byte[data.Length * 2];
+ for ( int i = 0; i < data.Length; i += 2 )
+ {
+ ushort pixel = BitConverter.ToUInt16( data, i );
+ int j = i * 2;
+ output[j + 0] = (byte)(((pixel >> 11) & 0x1F) * 255 / 31);
+ output[j + 1] = (byte)(((pixel >> 5) & 0x3F) * 255 / 63);
+ output[j + 2] = (byte)((pixel & 0x1F) * 255 / 31);
+ output[j + 3] = 255;
+ }
+ return output;
+ }
+
+ private static byte[] ConvertBgr565ToRgba( byte[] data )
+ {
+ var output = new byte[data.Length * 2];
+ for ( int i = 0; i < data.Length; i += 2 )
+ {
+ ushort pixel = BitConverter.ToUInt16( data, i );
+ int j = i * 2;
+ output[j + 0] = (byte)((pixel & 0x1F) * 255 / 31);
+ output[j + 1] = (byte)(((pixel >> 5) & 0x3F) * 255 / 63);
+ output[j + 2] = (byte)(((pixel >> 11) & 0x1F) * 255 / 31);
+ output[j + 3] = 255;
+ }
+ return output;
+ }
+
+ private static byte[] ConvertBgra4444ToRgba( byte[] data )
+ {
+ var output = new byte[data.Length * 2];
+ for ( int i = 0; i < data.Length; i += 2 )
+ {
+ ushort pixel = BitConverter.ToUInt16( data, i );
+ int j = i * 2;
+ output[j + 0] = (byte)(((pixel >> 8) & 0xF) * 17);
+ output[j + 1] = (byte)(((pixel >> 4) & 0xF) * 17);
+ output[j + 2] = (byte)((pixel & 0xF) * 17);
+ output[j + 3] = (byte)(((pixel >> 12) & 0xF) * 17);
+ }
+ return output;
+ }
+
+ private static byte[] ConvertBgra5551ToRgba( byte[] data )
+ {
+ var output = new byte[data.Length * 2];
+ for ( int i = 0; i < data.Length; i += 2 )
+ {
+ ushort pixel = BitConverter.ToUInt16( data, i );
+ int j = i * 2;
+ output[j + 0] = (byte)(((pixel >> 10) & 0x1F) * 255 / 31);
+ output[j + 1] = (byte)(((pixel >> 5) & 0x1F) * 255 / 31);
+ output[j + 2] = (byte)((pixel & 0x1F) * 255 / 31);
+ output[j + 3] = (byte)((pixel >> 15) * 255);
+ }
+ return output;
+ }
+
+ private static byte[] ConvertBgrx5551ToRgba( byte[] data )
+ {
+ var output = new byte[data.Length * 2];
+ for ( int i = 0; i < data.Length; i += 2 )
+ {
+ ushort pixel = BitConverter.ToUInt16( data, i );
+ int j = i * 2;
+ output[j + 0] = (byte)(((pixel >> 10) & 0x1F) * 255 / 31);
+ output[j + 1] = (byte)(((pixel >> 5) & 0x1F) * 255 / 31);
+ output[j + 2] = (byte)((pixel & 0x1F) * 255 / 31);
+ output[j + 3] = 255;
+ }
+ return output;
+ }
+
+ private static byte[] ConvertUv88ToRgba( byte[] data )
+ {
+ var output = new byte[data.Length * 2];
+ for ( int i = 0; i < data.Length; i += 2 )
+ {
+ int j = i * 2;
+ output[j + 0] = data[i]; // X (U)
+ output[j + 1] = data[i + 1]; // Y (V)
+ output[j + 2] = 255; // Z (computed as up)
+ output[j + 3] = 255;
+ }
+ return output;
+ }
+
+ private static byte[] ConvertUvwq8888ToRgba( byte[] data )
+ {
+ var output = new byte[data.Length];
+ for ( int i = 0; i < data.Length; i += 4 )
+ {
+ output[i + 0] = data[i + 0]; // U -> R
+ output[i + 1] = data[i + 1]; // V -> G
+ output[i + 2] = data[i + 2]; // W -> B
+ output[i + 3] = 255;
+ }
+ return output;
+ }
+
+ private static float HalfToFloat( ushort half )
+ {
+ int sign = (half >> 15) & 1;
+ int exponent = (half >> 10) & 0x1F;
+ int mantissa = half & 0x3FF;
+
+ if ( exponent == 0 )
+ {
+ if ( mantissa == 0 )
+ return sign == 1 ? -0.0f : 0.0f;
+
+ float value = mantissa / 1024.0f * (float)Math.Pow( 2, -14 );
+ return sign == 1 ? -value : value;
+ }
+ else if ( exponent == 31 )
+ {
+ return mantissa == 0 ? (sign == 1 ? float.NegativeInfinity : float.PositiveInfinity) : float.NaN;
+ }
+ else
+ {
+ float value = (1.0f + mantissa / 1024.0f) * (float)Math.Pow( 2, exponent - 15 );
+ return sign == 1 ? -value : value;
+ }
+ }
+
+ private static byte ToneMapToByte( float hdrValue )
+ {
+ float ldr = hdrValue / (1.0f + hdrValue);
+ return (byte)Math.Clamp( ldr * 255.0f, 0, 255 );
+ }
+
+ private static byte[] ConvertRgba16161616FToRgba( byte[] data, int width, int height )
+ {
+ int pixelCount = width * height;
+ var output = new byte[pixelCount * 4];
+
+ for ( int i = 0; i < pixelCount; i++ )
+ {
+ int srcOffset = i * 8;
+ int dstOffset = i * 4;
+
+ if ( srcOffset + 8 > data.Length )
+ break;
+
+ ushort rHalf = BitConverter.ToUInt16( data, srcOffset );
+ ushort gHalf = BitConverter.ToUInt16( data, srcOffset + 2 );
+ ushort bHalf = BitConverter.ToUInt16( data, srcOffset + 4 );
+ ushort aHalf = BitConverter.ToUInt16( data, srcOffset + 6 );
+
+ float r = HalfToFloat( rHalf );
+ float g = HalfToFloat( gHalf );
+ float b = HalfToFloat( bHalf );
+ float a = HalfToFloat( aHalf );
+
+ output[dstOffset + 0] = ToneMapToByte( r );
+ output[dstOffset + 1] = ToneMapToByte( g );
+ output[dstOffset + 2] = ToneMapToByte( b );
+ output[dstOffset + 3] = (byte)Math.Clamp( a * 255.0f, 0, 255 );
+ }
+
+ return output;
+ }
+
+ private static byte[] ConvertRgba16161616ToRgba( byte[] data, int width, int height )
+ {
+ int pixelCount = width * height;
+ var output = new byte[pixelCount * 4];
+
+ for ( int i = 0; i < pixelCount; i++ )
+ {
+ int srcOffset = i * 8;
+ int dstOffset = i * 4;
+
+ if ( srcOffset + 8 > data.Length )
+ break;
+
+ output[dstOffset + 0] = data[srcOffset + 1]; // R high byte
+ output[dstOffset + 1] = data[srcOffset + 3]; // G high byte
+ output[dstOffset + 2] = data[srcOffset + 5]; // B high byte
+ output[dstOffset + 3] = data[srcOffset + 7]; // A high byte
+ }
+
+ return output;
+ }
+
+ private static byte[] ConvertR32FToRgba( byte[] data, int width, int height )
+ {
+ int pixelCount = width * height;
+ var output = new byte[pixelCount * 4];
+
+ for ( int i = 0; i < pixelCount; i++ )
+ {
+ int srcOffset = i * 4;
+ int dstOffset = i * 4;
+
+ if ( srcOffset + 4 > data.Length )
+ break;
+
+ float r = BitConverter.ToSingle( data, srcOffset );
+ byte rByte = ToneMapToByte( r );
+
+ output[dstOffset + 0] = rByte;
+ output[dstOffset + 1] = rByte;
+ output[dstOffset + 2] = rByte;
+ output[dstOffset + 3] = 255;
+ }
+
+ return output;
+ }
+
+ private static byte[] ConvertRgb323232FToRgba( byte[] data, int width, int height )
+ {
+ int pixelCount = width * height;
+ var output = new byte[pixelCount * 4];
+
+ for ( int i = 0; i < pixelCount; i++ )
+ {
+ int srcOffset = i * 12;
+ int dstOffset = i * 4;
+
+ if ( srcOffset + 12 > data.Length )
+ break;
+
+ float r = BitConverter.ToSingle( data, srcOffset );
+ float g = BitConverter.ToSingle( data, srcOffset + 4 );
+ float b = BitConverter.ToSingle( data, srcOffset + 8 );
+
+ output[dstOffset + 0] = ToneMapToByte( r );
+ output[dstOffset + 1] = ToneMapToByte( g );
+ output[dstOffset + 2] = ToneMapToByte( b );
+ output[dstOffset + 3] = 255;
+ }
+
+ return output;
+ }
+
+ private static byte[] ConvertRgba32323232FToRgba( byte[] data, int width, int height )
+ {
+ int pixelCount = width * height;
+ var output = new byte[pixelCount * 4];
+
+ for ( int i = 0; i < pixelCount; i++ )
+ {
+ int srcOffset = i * 16;
+ int dstOffset = i * 4;
+
+ if ( srcOffset + 16 > data.Length )
+ break;
+
+ float r = BitConverter.ToSingle( data, srcOffset );
+ float g = BitConverter.ToSingle( data, srcOffset + 4 );
+ float b = BitConverter.ToSingle( data, srcOffset + 8 );
+ float a = BitConverter.ToSingle( data, srcOffset + 12 );
+
+ output[dstOffset + 0] = ToneMapToByte( r );
+ output[dstOffset + 1] = ToneMapToByte( g );
+ output[dstOffset + 2] = ToneMapToByte( b );
+ output[dstOffset + 3] = (byte)Math.Clamp( a * 255.0f, 0, 255 );
+ }
+
+ return output;
+ }
+}
diff --git a/engine/Mounting/Sandbox.Mounting.HL2/Sandbox.Mounting.HL2.csproj b/engine/Mounting/Sandbox.Mounting.HL2/Sandbox.Mounting.HL2.csproj
new file mode 100644
index 000000000..0ca19ffac
--- /dev/null
+++ b/engine/Mounting/Sandbox.Mounting.HL2/Sandbox.Mounting.HL2.csproj
@@ -0,0 +1,33 @@
+
+
+
+ 1.0.1
+ net10.0
+ false
+ true
+ true
+ 1701;1702;1591
+ 14
+ true
+
+
+
+ True
+
+
+
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/engine/Mounting/Sandbox.Mounting.HL2/ValveKeyValue.cs b/engine/Mounting/Sandbox.Mounting.HL2/ValveKeyValue.cs
new file mode 100644
index 000000000..67d1f45e8
--- /dev/null
+++ b/engine/Mounting/Sandbox.Mounting.HL2/ValveKeyValue.cs
@@ -0,0 +1,337 @@
+using System;
+using System.Globalization;
+using System.Text;
+
+namespace ValveKeyValue;
+
+internal sealed class KeyValues( string name, string value = null )
+{
+ private static readonly string[] ApplyConditionals =
+ [
+ ">=dx90_20b", ">=dx90", ">=dx80", ">=dx70",
+ ">dx90", ">dx80", ">dx70", "dx9",
+ "hdr", "hdr_dx9", "srgb",
+ "gpu>=1", "gpu>=2", "gpu>=3",
+ ];
+
+ private static readonly string[] RemoveConditionals =
+ [
+ " Children { get; } = [];
+
+ public KeyValues this[string key]
+ {
+ get
+ {
+ foreach ( var child in Children )
+ {
+ if ( child.Name.Equals( key, StringComparison.OrdinalIgnoreCase ) )
+ return child;
+ }
+ return null;
+ }
+ }
+
+ public string GetString( string key, string defaultValue = null )
+ {
+ var child = this[key];
+ return child?.Value ?? defaultValue;
+ }
+
+ public int GetInt( string key, int defaultValue = 0 )
+ {
+ var child = this[key];
+ return child?.Value == null
+ ? defaultValue
+ : int.TryParse( child.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result ) ? result : defaultValue;
+ }
+
+ public float GetFloat( string key, float defaultValue = 0f )
+ {
+ var child = this[key];
+ return child?.Value == null
+ ? defaultValue
+ : float.TryParse( child.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var result ) ? result : defaultValue;
+ }
+
+ public bool GetBool( string key, bool defaultValue = false )
+ {
+ var child = this[key];
+ if ( child?.Value == null )
+ return defaultValue;
+
+ if ( child.Value == "1" || child.Value.Equals( "true", StringComparison.OrdinalIgnoreCase ) )
+ return true;
+ if ( child.Value == "0" || child.Value.Equals( "false", StringComparison.OrdinalIgnoreCase ) )
+ return false;
+
+ return defaultValue;
+ }
+
+ public void MergeConditionals()
+ {
+ foreach ( var blockName in ApplyConditionals )
+ {
+ var block = FindChildIgnoreCase( blockName );
+ if ( block == null )
+ continue;
+
+ foreach ( var child in block.Children )
+ {
+ Children.RemoveAll( c => c.Name.Equals( child.Name, StringComparison.OrdinalIgnoreCase ) );
+ Children.Add( child );
+ }
+
+ Children.Remove( block );
+ }
+
+ foreach ( var blockName in RemoveConditionals )
+ {
+ var block = FindChildIgnoreCase( blockName );
+ if ( block != null )
+ Children.Remove( block );
+ }
+
+ ProcessInlineConditionals();
+ }
+
+ private void ProcessInlineConditionals()
+ {
+ var toAdd = new List();
+ var toRemove = new List();
+
+ foreach ( var child in Children )
+ {
+ if ( child.Name == null || !child.Name.Contains( '?' ) )
+ continue;
+
+ var parts = child.Name.Split( '?', 2 );
+ if ( parts.Length != 2 )
+ continue;
+
+ var condition = parts[0].ToLowerInvariant();
+ var paramName = parts[1];
+
+ if ( EvaluateCondition( condition ) )
+ {
+ toRemove.Add( child );
+ toAdd.Add( new KeyValues( paramName, child.Value ) );
+ }
+ else
+ {
+ toRemove.Add( child );
+ }
+ }
+
+ foreach ( var child in toRemove )
+ Children.Remove( child );
+
+ foreach ( var child in toAdd )
+ {
+ Children.RemoveAll( c => c.Name.Equals( child.Name, StringComparison.OrdinalIgnoreCase ) );
+ Children.Add( child );
+ }
+ }
+
+ private static bool EvaluateCondition( string condition )
+ {
+ bool negate = condition.StartsWith( '!' );
+ if ( negate )
+ condition = condition[1..];
+
+ bool result = condition switch
+ {
+ "hdr" or "srgb" or "srgb_pc" => true,
+ "ldr" or "360" or "sonyps3" or "gameconsole" or "srgb_gameconsole" => false,
+ "lowfill" => false,
+ "highqualitycsm" => true,
+ "lowqualitycsm" => false,
+ _ when condition.StartsWith( "gpu>=" ) => true,
+ _ when condition.StartsWith( "gpu<" ) => false,
+ _ => false
+ };
+
+ return negate ? !result : result;
+ }
+
+ private KeyValues FindChildIgnoreCase( string name )
+ {
+ foreach ( var child in Children )
+ {
+ if ( child.Name.Equals( name, StringComparison.OrdinalIgnoreCase ) )
+ return child;
+ }
+ return null;
+ }
+
+ public static KeyValues Parse( string text )
+ {
+ var reader = new KeyValuesReader( text );
+ return reader.Parse();
+ }
+
+ public static KeyValues Parse( byte[] data )
+ {
+ return Parse( Encoding.UTF8.GetString( data ) );
+ }
+
+ public static KeyValues Parse( Stream stream )
+ {
+ using var reader = new StreamReader( stream, Encoding.UTF8 );
+ return Parse( reader.ReadToEnd() );
+ }
+
+ private sealed class KeyValuesReader( string text )
+ {
+ private readonly string _text = text ?? string.Empty;
+ private int _pos = 0;
+
+ public KeyValues Parse()
+ {
+ SkipWhitespaceAndComments();
+
+ var name = ReadToken();
+ if ( name == null )
+ return null;
+
+ SkipWhitespaceAndComments();
+
+ if ( _pos < _text.Length && _text[_pos] == '{' )
+ {
+ _pos++;
+ var kv = new KeyValues( name );
+ ParseChildren( kv );
+ return kv;
+ }
+
+ var value = ReadToken();
+ return new KeyValues( name, value );
+ }
+
+ private void ParseChildren( KeyValues parent )
+ {
+ while ( _pos < _text.Length )
+ {
+ SkipWhitespaceAndComments();
+
+ if ( _pos >= _text.Length )
+ break;
+
+ if ( _text[_pos] == '}' )
+ {
+ _pos++;
+ break;
+ }
+
+ var key = ReadToken();
+ if ( key == null )
+ break;
+
+ SkipWhitespaceAndComments();
+
+ if ( _pos < _text.Length && _text[_pos] == '{' )
+ {
+ _pos++;
+ var child = new KeyValues( key );
+ ParseChildren( child );
+ parent.Children.Add( child );
+ }
+ else
+ {
+ var value = ReadToken();
+ parent.Children.Add( new KeyValues( key, value ) );
+ }
+ }
+ }
+
+ private string ReadToken()
+ {
+ SkipWhitespaceAndComments();
+
+ return _pos >= _text.Length ? null : _text[_pos] == '"' ? ReadQuotedString() : ReadUnquotedString();
+ }
+
+ private string ReadQuotedString()
+ {
+ _pos++;
+
+ var sb = new StringBuilder();
+ while ( _pos < _text.Length )
+ {
+ var c = _text[_pos];
+
+ if ( c == '"' )
+ {
+ _pos++;
+ break;
+ }
+
+ sb.Append( c );
+ _pos++;
+ }
+
+ return sb.ToString();
+ }
+
+ private string ReadUnquotedString()
+ {
+ var start = _pos;
+ while ( _pos < _text.Length )
+ {
+ var c = _text[_pos];
+ if ( char.IsWhiteSpace( c ) || c == '{' || c == '}' || c == '"' )
+ break;
+ _pos++;
+ }
+
+ return _text[start.._pos];
+ }
+
+ private void SkipWhitespaceAndComments()
+ {
+ while ( _pos < _text.Length )
+ {
+ var c = _text[_pos];
+
+ if ( char.IsWhiteSpace( c ) )
+ {
+ _pos++;
+ continue;
+ }
+
+ // Line comment
+ if ( c == '/' && _pos + 1 < _text.Length && _text[_pos + 1] == '/' )
+ {
+ _pos += 2;
+ while ( _pos < _text.Length && _text[_pos] != '\n' )
+ _pos++;
+ continue;
+ }
+
+ // Block comment
+ if ( c == '/' && _pos + 1 < _text.Length && _text[_pos + 1] == '*' )
+ {
+ _pos += 2;
+ while ( _pos + 1 < _text.Length )
+ {
+ if ( _text[_pos] == '*' && _text[_pos + 1] == '/' )
+ {
+ _pos += 2;
+ break;
+ }
+ _pos++;
+ }
+ continue;
+ }
+
+ break;
+ }
+ }
+ }
+}
diff --git a/engine/Mounting/Sandbox.Mounting.HL2/ValvePak.cs b/engine/Mounting/Sandbox.Mounting.HL2/ValvePak.cs
new file mode 100644
index 000000000..33d2b3777
--- /dev/null
+++ b/engine/Mounting/Sandbox.Mounting.HL2/ValvePak.cs
@@ -0,0 +1,241 @@
+using System;
+using System.Text;
+
+namespace ValvePak;
+
+internal sealed class VpkEntry
+{
+ public string FileName { get; init; }
+ public string DirectoryName { get; init; }
+ public string TypeName { get; init; }
+ public uint CRC32 { get; init; }
+ public uint Length { get; init; }
+ public uint Offset { get; init; }
+ public ushort ArchiveIndex { get; init; }
+ public byte[] SmallData { get; init; }
+
+ public uint TotalLength => Length + (uint)(SmallData?.Length ?? 0);
+
+ public string GetFileName() => TypeName == " " ? FileName : $"{FileName}.{TypeName}";
+
+ public string GetFullPath() => DirectoryName == " " ? GetFileName() : $"{DirectoryName}/{GetFileName()}";
+}
+
+internal sealed class VpkArchive : IDisposable
+{
+ private const uint MAGIC = 0x55AA1234;
+
+ private BinaryReader _reader;
+ private string _fileName;
+
+ public uint Version { get; private set; }
+ public uint TreeSize { get; private set; }
+ public Dictionary> Entries { get; private set; }
+
+ public void Read( string filename )
+ {
+ SetFileName( filename );
+
+ var fs = new FileStream( filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite );
+ Read( fs );
+ }
+
+ public void Read( Stream input )
+ {
+ if ( _fileName == null )
+ throw new InvalidOperationException( "SetFileName() must be called before Read() with a stream." );
+
+ _reader = new BinaryReader( input );
+
+ if ( _reader.ReadUInt32() != MAGIC )
+ throw new InvalidDataException( "Not a VPK file." );
+
+ Version = _reader.ReadUInt32();
+ TreeSize = _reader.ReadUInt32();
+
+ if ( Version == 1 )
+ {
+ // Version 1 has no additional header fields
+ }
+ else if ( Version == 2 )
+ {
+ // Skip v2 header fields we don't need
+ _reader.ReadUInt32(); // FileDataSectionSize
+ _reader.ReadUInt32(); // ArchiveMD5SectionSize
+ _reader.ReadUInt32(); // OtherMD5SectionSize
+ _reader.ReadUInt32(); // SignatureSectionSize
+ }
+ else
+ {
+ throw new InvalidDataException( $"Unsupported VPK version: {Version}" );
+ }
+
+ ReadEntries();
+ }
+
+ public void SetFileName( string filename )
+ {
+ if ( filename.EndsWith( ".vpk", StringComparison.OrdinalIgnoreCase ) )
+ filename = filename[..^4];
+
+ if ( filename.EndsWith( "_dir", StringComparison.OrdinalIgnoreCase ) )
+ filename = filename[..^4];
+
+ _fileName = filename;
+ }
+
+ public VpkEntry FindEntry( string filePath )
+ {
+ filePath = filePath.Replace( '\\', '/' );
+
+ var lastSeparator = filePath.LastIndexOf( '/' );
+ var directory = lastSeparator > -1 ? filePath[..lastSeparator] : " ";
+ var fileName = filePath[(lastSeparator + 1)..];
+
+ var dot = fileName.LastIndexOf( '.' );
+ string extension;
+
+ if ( dot > -1 )
+ {
+ extension = fileName[(dot + 1)..];
+ fileName = fileName[..dot];
+ }
+ else
+ {
+ extension = " ";
+ }
+
+ if ( Entries == null || !Entries.TryGetValue( extension, out var entries ) )
+ return null;
+
+ directory = directory.Trim( '/' );
+ if ( directory.Length == 0 )
+ directory = " ";
+
+ foreach ( var entry in entries )
+ {
+ if ( entry.DirectoryName == directory && entry.FileName == fileName )
+ return entry;
+ }
+
+ return null;
+ }
+
+ public void ReadEntry( VpkEntry entry, out byte[] output )
+ {
+ output = new byte[entry.TotalLength];
+ ReadEntry( entry, output );
+ }
+
+ public void ReadEntry( VpkEntry entry, byte[] output )
+ {
+ var totalLength = (int)entry.TotalLength;
+
+ if ( output.Length < totalLength )
+ throw new ArgumentException( "Output buffer too small." );
+
+ if ( entry.SmallData?.Length > 0 )
+ entry.SmallData.CopyTo( output, 0 );
+
+ if ( entry.Length > 0 )
+ {
+ using var fs = GetArchiveStream( entry.ArchiveIndex );
+ fs.Seek( entry.Offset, SeekOrigin.Begin );
+
+ int offset = entry.SmallData?.Length ?? 0;
+ int remaining = (int)entry.Length;
+ while ( remaining > 0 )
+ {
+ int read = fs.Read( output, offset, remaining );
+ if ( read == 0 )
+ break;
+ offset += read;
+ remaining -= read;
+ }
+ }
+ }
+
+ private Stream GetArchiveStream( ushort archiveIndex )
+ {
+ if ( archiveIndex == 0x7FFF )
+ return _reader.BaseStream;
+
+ var archivePath = $"{_fileName}_{archiveIndex:D3}.vpk";
+ return new FileStream( archivePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite );
+ }
+
+ private void ReadEntries()
+ {
+ var entries = new Dictionary>( StringComparer.OrdinalIgnoreCase );
+
+ while ( true )
+ {
+ var typeName = ReadNullTermString();
+ if ( string.IsNullOrEmpty( typeName ) )
+ break;
+
+ var typeEntries = new List();
+
+ while ( true )
+ {
+ var directoryName = ReadNullTermString();
+ if ( string.IsNullOrEmpty( directoryName ) )
+ break;
+
+ while ( true )
+ {
+ var fileName = ReadNullTermString();
+ if ( string.IsNullOrEmpty( fileName ) )
+ break;
+
+ var crc32 = _reader.ReadUInt32();
+ var smallDataSize = _reader.ReadUInt16();
+ var archiveIndex = _reader.ReadUInt16();
+ var offset = _reader.ReadUInt32();
+ var length = _reader.ReadUInt32();
+ var terminator = _reader.ReadUInt16();
+
+ if ( terminator != 0xFFFF )
+ throw new InvalidDataException( $"Invalid entry terminator: 0x{terminator:X4}" );
+
+ byte[] smallData = null;
+ if ( smallDataSize > 0 )
+ {
+ smallData = _reader.ReadBytes( smallDataSize );
+ }
+
+ typeEntries.Add( new VpkEntry
+ {
+ FileName = fileName,
+ DirectoryName = directoryName,
+ TypeName = typeName,
+ CRC32 = crc32,
+ Length = length,
+ Offset = offset,
+ ArchiveIndex = archiveIndex,
+ SmallData = smallData ?? []
+ } );
+ }
+ }
+
+ entries[typeName] = typeEntries;
+ }
+
+ Entries = entries;
+ }
+
+ private string ReadNullTermString()
+ {
+ var bytes = new List();
+ byte b;
+ while ( (b = _reader.ReadByte()) != 0 )
+ bytes.Add( b );
+ return Encoding.UTF8.GetString( [.. bytes] );
+ }
+
+ public void Dispose()
+ {
+ _reader?.Dispose();
+ _reader = null;
+ }
+}
diff --git a/engine/Sandbox-Engine.slnx b/engine/Sandbox-Engine.slnx
index 1a91c9aea..cc07559ca 100644
--- a/engine/Sandbox-Engine.slnx
+++ b/engine/Sandbox-Engine.slnx
@@ -46,6 +46,7 @@
+
diff --git a/engine/Sandbox.Engine/Resources/Sound/SoundData.cs b/engine/Sandbox.Engine/Resources/Sound/SoundData.cs
index 7cbb9a872..1266f4e44 100644
--- a/engine/Sandbox.Engine/Resources/Sound/SoundData.cs
+++ b/engine/Sandbox.Engine/Resources/Sound/SoundData.cs
@@ -4,7 +4,7 @@
namespace Sandbox;
///
-/// Raw PCM sound data, kind of like a bitmap but for sounds
+/// Raw sound data, kind of like a bitmap but for sounds
///
internal class SoundData
{
@@ -100,4 +100,130 @@ public static SoundData FromWav( Span data )
private static readonly byte[] WAVE = Encoding.ASCII.GetBytes( "WAVE" );
private static readonly byte[] FMT = Encoding.ASCII.GetBytes( "fmt " );
private static readonly byte[] DATA = Encoding.ASCII.GetBytes( "data" );
+
+ private static readonly int[,] Mp3SampleRates =
+ {
+ { 11025, 12000, 8000 },
+ { 0, 0, 0 },
+ { 22050, 24000, 16000 },
+ { 44100, 48000, 32000 }
+ };
+
+ private static readonly int[,,] Mp3Bitrates =
+ {
+ {
+ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+ { 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0 },
+ { 0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 0 },
+ { 0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 0 }
+ },
+ {
+ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+ { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0 },
+ { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0 },
+ { 0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0 }
+ }
+ };
+
+ private static readonly int[,] Mp3SamplesPerFrame =
+ {
+ { 0, 1152, 1152, 384 },
+ { 0, 576, 1152, 384 }
+ };
+
+ public static SoundData FromMp3( Span data )
+ {
+ int offset = 0;
+ int audioEnd = data.Length;
+
+ if ( data.Length >= 10 && data[0] == 'I' && data[1] == 'D' && data[2] == '3' )
+ {
+ int id3Size = ((data[6] & 0x7F) << 21) | ((data[7] & 0x7F) << 14) | ((data[8] & 0x7F) << 7) | (data[9] & 0x7F);
+ offset = 10 + id3Size;
+ }
+
+ if ( data.Length >= 128 && data[^128] == 'T' && data[^127] == 'A' && data[^126] == 'G' )
+ audioEnd = data.Length - 128;
+
+ while ( offset < audioEnd - 4 && !(data[offset] == 0xFF && (data[offset + 1] & 0xE0) == 0xE0) )
+ offset++;
+
+ if ( offset >= audioEnd - 4 )
+ throw new ArgumentException( "Invalid MP3: no frame sync found" );
+
+ int version = (data[offset + 1] >> 3) & 0x03;
+ int layer = (data[offset + 1] >> 1) & 0x03;
+ int channelMode = (data[offset + 3] >> 6) & 0x03;
+
+ if ( version == 1 || layer == 0 )
+ throw new ArgumentException( "Invalid MP3: reserved version or layer" );
+
+ int versionType = version == 3 ? 0 : 1;
+ int samplesPerFrame = Mp3SamplesPerFrame[versionType, layer];
+ ushort channels = (ushort)(channelMode == 3 ? 1 : 2);
+
+ int frameCount = 0;
+ int pos = offset;
+
+ while ( pos < audioEnd - 4 && frameCount < 10000 )
+ {
+ if ( data[pos] != 0xFF || (data[pos + 1] & 0xE0) != 0xE0 )
+ break;
+
+ int v = (data[pos + 1] >> 3) & 0x03;
+ int l = (data[pos + 1] >> 1) & 0x03;
+ int brIdx = (data[pos + 2] >> 4) & 0x0F;
+ int srIdx = (data[pos + 2] >> 2) & 0x03;
+ int padding = (data[pos + 2] >> 1) & 0x01;
+
+ if ( v == 1 || l == 0 )
+ break;
+
+ int vt = v == 3 ? 0 : 1;
+ int sr = Mp3SampleRates[v, srIdx];
+ int br = Mp3Bitrates[vt, l, brIdx];
+
+ if ( sr == 0 || br == 0 )
+ break;
+
+ int spf = Mp3SamplesPerFrame[vt, l];
+ int frameSize = l == 3
+ ? (12 * br * 1000 / sr + padding) * 4
+ : spf / 8 * br * 1000 / sr + padding;
+
+ if ( frameSize <= 0 )
+ break;
+
+ frameCount++;
+ pos += frameSize;
+ }
+
+ if ( frameCount >= 10000 && pos > offset )
+ {
+ int avgFrameSize = (pos - offset) / frameCount;
+ frameCount = (audioEnd - offset) / avgFrameSize;
+ }
+
+ int bitrateIdx = (data[offset + 2] >> 4) & 0x0F;
+ int sampleIdx = (data[offset + 2] >> 2) & 0x03;
+ int sampleRate = Mp3SampleRates[version, sampleIdx];
+ int bitrate = Mp3Bitrates[versionType, layer, bitrateIdx];
+
+ if ( sampleRate == 0 || bitrate == 0 )
+ throw new ArgumentException( "Invalid MP3: bad sample rate or bitrate" );
+
+ float duration = frameCount * samplesPerFrame / (float)sampleRate;
+ uint sampleCount = (uint)(frameCount * samplesPerFrame);
+
+ return new SoundData
+ {
+ Format = 2,
+ Channels = channels,
+ SampleRate = (uint)sampleRate,
+ BitsPerSample = 16,
+ SampleCount = sampleCount,
+ Duration = duration,
+ PCMData = null
+ };
+ }
}
diff --git a/engine/Sandbox.Engine/Resources/Sound/SoundFile.cs b/engine/Sandbox.Engine/Resources/Sound/SoundFile.cs
index ff099e4f3..fbe53750c 100644
--- a/engine/Sandbox.Engine/Resources/Sound/SoundFile.cs
+++ b/engine/Sandbox.Engine/Resources/Sound/SoundFile.cs
@@ -207,6 +207,27 @@ public static unsafe SoundFile FromWav( string filename, Span data, bool l
return Create( filename, pcmData, soundData.Channels, soundData.SampleRate, format, soundData.SampleCount, soundData.Duration, loop );
}
+ ///
+ /// Load from MP3.
+ ///
+ public static unsafe SoundFile FromMp3( string filename, Span data, bool loop )
+ {
+ ThreadSafe.AssertIsMainThread( "SoundFile.FromMp3" );
+
+ if ( !filename.EndsWith( ".vsnd", StringComparison.OrdinalIgnoreCase ) )
+ filename = System.IO.Path.ChangeExtension( filename, "vsnd" );
+
+ if ( Loaded.TryGetValue( filename, out var soundFile ) )
+ return soundFile;
+
+ if ( data.Length <= 0 )
+ throw new ArgumentException( "Invalid data" );
+
+ var soundData = SoundData.FromMp3( data );
+
+ return Create( filename, data, soundData.Channels, soundData.SampleRate, (int)SoundFormat.MP3, soundData.SampleCount, soundData.Duration, loop );
+ }
+
// this is a fucking mess
// TODO: Document. What's the difference beetween preloading here and precaching in Load()? What does this do that Load() doesn't?
diff --git a/game/mount/hl2/Assets/shaders/eyes.shader b/game/mount/hl2/Assets/shaders/eyes.shader
new file mode 100644
index 000000000..343c7d18f
--- /dev/null
+++ b/game/mount/hl2/Assets/shaders/eyes.shader
@@ -0,0 +1,119 @@
+HEADER
+{
+ Description = "Source Engine Eyes Shader";
+ Version = 1;
+ DevShader = false;
+}
+
+FEATURES
+{
+ #include "common/features.hlsl"
+ Feature( F_HALFLAMBERT, 0..1, "Half Lambert" );
+}
+
+MODES
+{
+ Forward();
+ Depth( S_MODE_DEPTH );
+ ToolsShadingComplexity( "tools_shading_complexity.shader" );
+}
+
+COMMON
+{
+ #include "common/shared.hlsl"
+}
+
+struct VertexInput
+{
+ #include "common/vertexinput.hlsl"
+};
+
+struct PixelInput
+{
+ #include "common/pixelinput.hlsl"
+ float2 vIrisTexCoord : TEXCOORD14;
+};
+
+VS
+{
+ #include "common/vertex.hlsl"
+
+ // Iris projection vectors
+ float4 g_vIrisU < Default4( 1.0, 0.0, 0.0, 0.5 ); >;
+ float4 g_vIrisV < Default4( 0.0, 0.0, 1.0, 0.5 ); >;
+
+ PixelInput MainVs( VS_INPUT i )
+ {
+ PixelInput o = ProcessVertex( i );
+
+ o.vIrisTexCoord.x = dot( g_vIrisU.xyz, i.vPositionOs.xyz ) + g_vIrisU.w;
+ o.vIrisTexCoord.y = dot( g_vIrisV.xyz, i.vPositionOs.xyz ) + g_vIrisV.w;
+
+ return FinalizeVertex( o );
+ }
+}
+
+PS
+{
+ #include "common/pixel.hlsl"
+
+ StaticCombo( S_HALFLAMBERT, F_HALFLAMBERT, Sys( ALL ) );
+
+ // $basetexture - eyeball/sclera texture
+ CreateInputTexture2D( TextureColor, Srgb, 8, "", "_color", "Material,10/10", Default3( 1.0, 1.0, 1.0 ) );
+ Texture2D g_tColor < Channel( RGB, Box( TextureColor ), Srgb ); Channel( A, Box( TextureColor ), Linear ); OutputFormat( BC7 ); SrgbRead( true ); >;
+ TextureAttribute( RepresentativeTexture, g_tColor );
+
+ // $iris - iris/pupil texture
+ CreateInputTexture2D( TextureIris, Srgb, 8, "", "_iris", "Eyes,10/10", Default3( 0.5, 0.3, 0.2 ) );
+ Texture2D g_tIris < Channel( RGB, Box( TextureIris ), Srgb ); Channel( A, Box( TextureIris ), Linear ); OutputFormat( BC7 ); SrgbRead( true ); >;
+
+ float4 MainPs( PixelInput i ) : SV_Target0
+ {
+ float2 vUV = i.vTextureCoords.xy;
+ float4 vBaseSample = g_tColor.Sample( g_sAniso, vUV );
+ float4 vIrisSample = g_tIris.Sample( g_sAniso, i.vIrisTexCoord );
+
+ #if S_MODE_DEPTH
+ return float4( 0, 0, 0, vBaseSample.a );
+ #endif
+
+ float3 vPositionWs = i.vPositionWithOffsetWs.xyz + g_vCameraPositionWs;
+ float3 vNormalWs = normalize( i.vNormalWs );
+
+ // Blend iris over base using iris alpha
+ float3 vResult = lerp( vBaseSample.rgb, vIrisSample.rgb, vIrisSample.a );
+
+ // Lighting
+ float3 vLighting = float3( 0, 0, 0 );
+
+ uint nLightCount = Light::Count( i.vPositionSs.xy );
+ for ( uint idx = 0; idx < nLightCount; idx++ )
+ {
+ Light light = Light::From( i.vPositionSs.xy, vPositionWs, idx );
+ float3 vLightDir = light.Direction;
+ float3 vLightColor = light.Color * light.Attenuation * light.Visibility;
+
+ float flNdotL = dot( vNormalWs, vLightDir );
+
+ #if S_HALFLAMBERT
+ flNdotL = flNdotL * 0.5 + 0.5;
+ flNdotL = flNdotL * flNdotL;
+ #else
+ flNdotL = saturate( flNdotL );
+ #endif
+
+ vLighting += vLightColor * flNdotL;
+ }
+
+ float3 vAmbient = AmbientLight::From( vPositionWs, i.vPositionSs.xy, vNormalWs );
+ vLighting += vAmbient;
+
+ vResult *= vLighting;
+
+ float4 vColor = float4( vResult, vBaseSample.a );
+ vColor = DoAtmospherics( vPositionWs, i.vPositionSs.xy, vColor );
+
+ return vColor;
+ }
+}
diff --git a/game/mount/hl2/Assets/shaders/lightmappedgeneric.shader b/game/mount/hl2/Assets/shaders/lightmappedgeneric.shader
new file mode 100644
index 000000000..a55fb42b8
--- /dev/null
+++ b/game/mount/hl2/Assets/shaders/lightmappedgeneric.shader
@@ -0,0 +1,325 @@
+HEADER
+{
+ Description = "Source Engine LightmappedGeneric Shader";
+ Version = 1;
+ DevShader = false;
+}
+
+FEATURES
+{
+ #include "common/features.hlsl"
+ Feature( F_TRANSLUCENT, 0..1, "Rendering" );
+ Feature( F_ALPHA_TEST, 0..1, "Rendering" );
+ Feature( F_BUMPMAP, 0..1, "Normal Mapping" );
+ Feature( F_SELFILLUM, 0..1, "Self Illumination" );
+ Feature( F_DETAIL, 0..1, "Detail Texture" );
+ Feature( F_ENVMAP, 0..1, "Environment Map" );
+ Feature( F_BLEND, 0..1, "Texture Blending" );
+ Feature( F_SEAMLESS, 0..1, "Seamless Mapping" );
+}
+
+MODES
+{
+ Forward();
+ Depth( S_MODE_DEPTH );
+ ToolsShadingComplexity( "tools_shading_complexity.shader" );
+}
+
+COMMON
+{
+ #include "common/shared.hlsl"
+}
+
+struct VertexInput
+{
+ #include "common/vertexinput.hlsl"
+};
+
+struct PixelInput
+{
+ #include "common/pixelinput.hlsl"
+};
+
+VS
+{
+ #include "common/vertex.hlsl"
+
+ PixelInput MainVs( VS_INPUT i )
+ {
+ PixelInput o = ProcessVertex( i );
+ return FinalizeVertex( o );
+ }
+}
+
+PS
+{
+ StaticCombo( S_TRANSLUCENT, F_TRANSLUCENT, Sys( ALL ) );
+ StaticCombo( S_ALPHA_TEST, F_ALPHA_TEST, Sys( ALL ) );
+ StaticCombo( S_ADDITIVE_BLEND, F_ADDITIVE_BLEND, Sys( ALL ) );
+ StaticCombo( S_BUMPMAP, F_BUMPMAP, Sys( ALL ) );
+ StaticCombo( S_SELFILLUM, F_SELFILLUM, Sys( ALL ) );
+ StaticCombo( S_DETAIL, F_DETAIL, Sys( ALL ) );
+ StaticCombo( S_ENVMAP, F_ENVMAP, Sys( ALL ) );
+ StaticCombo( S_BLEND, F_BLEND, Sys( ALL ) );
+ StaticCombo( S_SEAMLESS, F_SEAMLESS, Sys( ALL ) );
+
+ #include "common/pixel.hlsl"
+
+ float SourceFresnel4( float3 vNormal, float3 vEyeDir )
+ {
+ // Traditional fresnel using 4th power (square twice)
+ float fresnel = saturate( 1.0 - dot( vNormal, vEyeDir ) );
+ fresnel = fresnel * fresnel;
+ return fresnel * fresnel;
+ }
+
+ // $basetexture
+ CreateInputTexture2D( TextureColor, Srgb, 8, "", "_color", "Material,10/10", Default3( 1.0, 1.0, 1.0 ) );
+ Texture2D g_tColor < Channel( RGB, Box( TextureColor ), Srgb ); Channel( A, Box( TextureColor ), Linear ); OutputFormat( BC7 ); SrgbRead( true ); >;
+ TextureAttribute( RepresentativeTexture, g_tColor );
+
+ // $color - color tint (default [1 1 1])
+ float3 g_vColorTint < UiType( Color ); Default3( 1.0, 1.0, 1.0 ); UiGroup( "Material,10/30" ); >;
+
+ // $alpha - opacity (default 1)
+ float g_flAlpha < Default( 1.0 ); Range( 0.0, 1.0 ); UiGroup( "Material,10/40" ); >;
+
+ #if S_SEAMLESS
+ // $seamless_scale - seamless texture mapping scale (default 0)
+ float g_flSeamlessScale < Default( 1.0 ); Range( 0.001, 10.0 ); UiGroup( "Material,10/60" ); >;
+ #endif
+
+ #if S_BUMPMAP
+ // $bumpmap
+ CreateInputTexture2D( TextureNormal, Linear, 8, "NormalizeNormals", "_normal", "Normal Map,10/10", Default3( 0.5, 0.5, 1.0 ) );
+ Texture2D g_tNormal < Channel( RGB, Box( TextureNormal ), Linear ); Channel( A, Box( TextureNormal ), Linear ); OutputFormat( BC7 ); SrgbRead( false ); >;
+ #endif
+
+ #if S_BLEND
+ // $basetexture2
+ CreateInputTexture2D( TextureColor2, Srgb, 8, "", "_color2", "Blending,10/10", Default3( 1.0, 1.0, 1.0 ) );
+ Texture2D g_tColor2 < Channel( RGB, Box( TextureColor2 ), Srgb ); Channel( A, Box( TextureColor2 ), Linear ); OutputFormat( BC7 ); SrgbRead( true ); >;
+
+ #if S_BUMPMAP
+ // $bumpmap2
+ CreateInputTexture2D( TextureNormal2, Linear, 8, "NormalizeNormals", "_normal2", "Blending,10/20", Default3( 0.5, 0.5, 1.0 ) );
+ Texture2D g_tNormal2 < Channel( RGB, Box( TextureNormal2 ), Linear ); OutputFormat( BC7 ); SrgbRead( false ); >;
+ #endif
+
+ // $blendmodulatetexture - R channel = blend point, G channel = blend range
+ CreateInputTexture2D( TextureBlendModulate, Linear, 8, "", "_blendmod", "Blending,10/30", Default3( 0.5, 0.0, 0.0 ) );
+ Texture2D g_tBlendModulate < Channel( RG, Box( TextureBlendModulate ), Linear ); OutputFormat( BC7 ); SrgbRead( false ); >;
+ #endif
+
+ #if S_DETAIL
+ // $detail
+ CreateInputTexture2D( TextureDetail, Srgb, 8, "", "_detail", "Detail,10/10", Default3( 0.5, 0.5, 0.5 ) );
+ Texture2D g_tDetail < Channel( RGB, Box( TextureDetail ), Srgb ); OutputFormat( BC7 ); SrgbRead( true ); >;
+
+ // $detailscale - detail texture UV scale (default 4)
+ float g_flDetailScale < Default( 4.0 ); Range( 0.1, 32.0 ); UiGroup( "Detail,10/20" ); >;
+ // $detailblendfactor - detail blend amount (default 1)
+ float g_flDetailBlendFactor < Default( 1.0 ); Range( 0.0, 1.0 ); UiGroup( "Detail,10/30" ); >;
+ // $detailblendmode - blend mode 0-9 (default 0 = mod2x)
+ int g_nDetailBlendMode < Default( 0 ); Range( 0, 9 ); UiGroup( "Detail,10/40" ); >;
+ // $detailtint - detail color tint (default [1 1 1])
+ float3 g_vDetailTint < UiType( Color ); Default3( 1.0, 1.0, 1.0 ); UiGroup( "Detail,10/50" ); >;
+ #endif
+
+ #if S_ENVMAP
+ // $envmapmask
+ CreateInputTexture2D( TextureEnvMapMask, Linear, 8, "", "_envmapmask", "Environment Map,10/10", Default( 1.0 ) );
+ // $envmap
+ CreateInputTextureCube( TextureEnvMap, Srgb, 8, "", "_envmap", "Environment Map,10/15", Default3( 0.0, 0.0, 0.0 ) );
+ Texture2D g_tEnvMapMask < Channel( R, Box( TextureEnvMapMask ), Linear ); OutputFormat( BC7 ); SrgbRead( false ); >;
+ TextureCube g_tEnvMap < Channel( RGBA, Box( TextureEnvMap ), Srgb ); OutputFormat( BC6H ); SrgbRead( true ); >;
+
+ // $envmaptint - envmap color tint (default [1 1 1])
+ float3 g_vEnvMapTint < UiType( Color ); Default3( 1.0, 1.0, 1.0 ); UiGroup( "Environment Map,10/20" ); >;
+ // $envmapcontrast - 0=normal, 1=color*color (default 0)
+ float g_flEnvMapContrast < Default( 0.0 ); Range( 0.0, 1.0 ); UiGroup( "Environment Map,10/30" ); >;
+ // $envmapsaturation - 0=greyscale, 1=normal (default 1)
+ float g_flEnvMapSaturation < Default( 1.0 ); Range( 0.0, 1.0 ); UiGroup( "Environment Map,10/40" ); >;
+ // $fresnelreflection - 1=mirror, 0=water (default 1)
+ float g_flFresnelReflection < Default( 1.0 ); Range( 0.0, 1.0 ); UiGroup( "Environment Map,10/45" ); >;
+ // $basealphaenvmapmask - use base alpha as envmap mask (default 0)
+ int g_nBaseAlphaEnvMapMask < Default( 0 ); Range( 0, 1 ); UiGroup( "Environment Map,10/50" ); >;
+ // $normalmapalphaenvmapmask - use normalmap alpha as envmap mask (default 0)
+ int g_nNormalMapAlphaEnvMapMask < Default( 0 ); Range( 0, 1 ); UiGroup( "Environment Map,10/60" ); >;
+ #endif
+
+ #if S_SELFILLUM
+ // $selfillumtint - self-illumination color tint (default [1 1 1])
+ float3 g_vSelfIllumTint < UiType( Color ); Default3( 1.0, 1.0, 1.0 ); UiGroup( "Self Illumination,10/20" ); >;
+ #endif
+
+ float2 CalcSeamlessUV( float3 vPositionWs, float3 vNormalWs, float flScale )
+ {
+ float3 vAbsNormal = abs( vNormalWs );
+ float2 vUV;
+
+ if ( vAbsNormal.z > max( vAbsNormal.x, vAbsNormal.y ) )
+ vUV = vPositionWs.xy;
+ else if ( vAbsNormal.y > vAbsNormal.x )
+ vUV = vPositionWs.xz;
+ else
+ vUV = vPositionWs.yz;
+
+ return vUV * flScale;
+ }
+
+ float3 ApplyDetailTexture( float3 vBase, float3 vDetail, int nBlendMode, float flBlendFactor )
+ {
+ switch ( nBlendMode )
+ {
+ case 0: // Mod2x
+ default:
+ return vBase * lerp( float3( 1, 1, 1 ), vDetail * 2.0, flBlendFactor );
+ case 1: // Additive
+ case 5: // Unlit additive
+ case 6: // Unlit additive threshold fade
+ return saturate( vBase + vDetail * flBlendFactor );
+ case 2: // Translucent detail
+ case 3: // Blend factor fade
+ return lerp( vBase, vDetail, flBlendFactor );
+ case 4: // Translucent base
+ case 9: // Base over detail
+ return lerp( vDetail, vBase, flBlendFactor );
+ case 7: // Two-pattern decal modulate
+ return vBase * lerp( float3( 1, 1, 1 ), vDetail * 2.0, flBlendFactor );
+ case 8: // Multiply
+ return vBase * lerp( float3( 1, 1, 1 ), vDetail, flBlendFactor );
+ }
+ }
+
+ float4 MainPs( PixelInput i ) : SV_Target0
+ {
+ float2 vUV = i.vTextureCoords.xy;
+
+ #if S_SEAMLESS
+ vUV = CalcSeamlessUV( i.vPositionWithOffsetWs.xyz + g_vCameraPositionWs, i.vNormalWs, g_flSeamlessScale );
+ #endif
+
+ // Sample base texture
+ float4 vBaseTexture = g_tColor.Sample( g_sAniso, vUV );
+ float3 vAlbedo = vBaseTexture.rgb * g_vColorTint;
+ float flBaseAlpha = vBaseTexture.a;
+ float flAlpha = flBaseAlpha * g_flAlpha;
+
+ // Normal mapping
+ float3 vNormalTs = float3( 0.0, 0.0, 1.0 );
+ float flNormalAlpha = 1.0;
+ #if S_BUMPMAP
+ float4 vNormalSample = g_tNormal.Sample( g_sAniso, vUV );
+ vNormalTs = DecodeNormal( vNormalSample.rgb );
+ vNormalTs = normalize( vNormalTs );
+ flNormalAlpha = vNormalSample.a;
+ #endif
+
+ float3 vNormalWs = TransformNormal( vNormalTs, i.vNormalWs, i.vTangentUWs, i.vTangentVWs );
+
+ #if S_BLEND
+ {
+ float flBlendFactor = i.vVertexColor.a;
+
+ float2 vBlendMod = g_tBlendModulate.Sample( g_sAniso, vUV ).rg;
+ float flBlendMin = saturate( vBlendMod.r - vBlendMod.g );
+ float flBlendMax = saturate( vBlendMod.r + vBlendMod.g );
+
+ flBlendFactor = smoothstep( flBlendMin, flBlendMax, flBlendFactor );
+
+ float4 vBaseTexture2 = g_tColor2.Sample( g_sAniso, vUV );
+
+ vAlbedo = lerp( vAlbedo, vBaseTexture2.rgb * g_vColorTint, flBlendFactor );
+ flBaseAlpha = lerp( flBaseAlpha, vBaseTexture2.a, flBlendFactor );
+
+ #if S_BUMPMAP
+ float4 vNormalSample2 = g_tNormal2.Sample( g_sAniso, vUV );
+ float3 vNormalTs2 = DecodeNormal( vNormalSample2.rgb );
+ vNormalTs2 = normalize( vNormalTs2 );
+
+ vNormalTs = normalize( lerp( vNormalTs, vNormalTs2, flBlendFactor ) );
+ vNormalWs = TransformNormal( vNormalTs, i.vNormalWs, i.vTangentUWs, i.vTangentVWs );
+ flNormalAlpha = lerp( flNormalAlpha, vNormalSample2.a, flBlendFactor );
+ #endif
+ }
+ #endif
+
+ #if S_DETAIL
+ {
+ float2 vDetailUV = vUV * g_flDetailScale;
+ float3 vDetail = g_tDetail.Sample( g_sAniso, vDetailUV ).rgb * g_vDetailTint;
+ vAlbedo = ApplyDetailTexture( vAlbedo, vDetail, g_nDetailBlendMode, g_flDetailBlendFactor );
+ }
+ #endif
+
+ #if S_MODE_DEPTH
+ return float4( 0, 0, 0, flAlpha );
+ #endif
+
+ Material m = Material::Init( i );
+ m.Albedo = vAlbedo;
+ m.Normal = vNormalWs;
+ m.Opacity = flAlpha;
+
+ m.Roughness = 0.7;
+ m.Metalness = 0.0;
+ m.AmbientOcclusion = 1.0;
+
+ float3 vPositionWs = i.vPositionWithOffsetWs.xyz + g_vCameraPositionWs;
+ float3 vViewWs = normalize( g_vCameraPositionWs - vPositionWs );
+ float flNdotV = saturate( dot( vNormalWs, vViewWs ) );
+
+ #if S_ENVMAP
+ {
+ float flEnvMapMask = 1.0;
+
+ if ( g_nBaseAlphaEnvMapMask != 0 )
+ {
+ flEnvMapMask = 1.0 - flBaseAlpha;
+ }
+ else if ( g_nNormalMapAlphaEnvMapMask != 0 )
+ {
+ #if S_BUMPMAP
+ flEnvMapMask = flNormalAlpha;
+ #endif
+ }
+ else
+ {
+ flEnvMapMask = g_tEnvMapMask.Sample( g_sAniso, vUV ).r;
+ }
+
+ float flFresnel = SourceFresnel4( vNormalWs, vViewWs );
+ flFresnel = g_flFresnelReflection + ( 1.0 - g_flFresnelReflection ) * flFresnel;
+
+ float3 vReflectWs = reflect( -vViewWs, vNormalWs );
+
+ float3 vEnvColor = g_tEnvMap.SampleLevel( g_sAniso, vReflectWs, m.Roughness * 6.0 ).rgb;
+
+ float3 vEnvSquared = vEnvColor * vEnvColor;
+ vEnvColor = lerp( vEnvColor, vEnvSquared, g_flEnvMapContrast );
+
+ float flLuminance = dot( vEnvColor, float3( 0.299, 0.587, 0.114 ) );
+ vEnvColor = lerp( float3( flLuminance, flLuminance, flLuminance ), vEnvColor, g_flEnvMapSaturation );
+
+ m.Emission += vEnvColor * g_vEnvMapTint * flEnvMapMask * flFresnel;
+ }
+ #endif
+
+ #if S_SELFILLUM
+ {
+ float flSelfIllumMask = flBaseAlpha;
+ m.Emission += m.Albedo * flSelfIllumMask * g_vSelfIllumTint;
+ }
+ #endif
+
+ float3 vVertexLighting = i.vVertexColor.rgb;
+ float flVertexLightIntensity = dot( vVertexLighting, float3( 0.333, 0.333, 0.333 ) );
+ if ( flVertexLightIntensity < 0.99 )
+ {
+ m.Albedo *= vVertexLighting;
+ }
+
+ return ShadingModelStandard::Shade( m );
+ }
+}
diff --git a/game/mount/hl2/Assets/shaders/teeth.shader b/game/mount/hl2/Assets/shaders/teeth.shader
new file mode 100644
index 000000000..c65292163
--- /dev/null
+++ b/game/mount/hl2/Assets/shaders/teeth.shader
@@ -0,0 +1,143 @@
+HEADER
+{
+ Description = "Source Engine Teeth Shader";
+ Version = 1;
+ DevShader = false;
+}
+
+FEATURES
+{
+ #include "common/features.hlsl"
+ Feature( F_BUMPMAP, 0..1, "Normal Mapping" );
+}
+
+MODES
+{
+ Forward();
+ Depth( S_MODE_DEPTH );
+ ToolsShadingComplexity( "tools_shading_complexity.shader" );
+}
+
+COMMON
+{
+ #include "common/shared.hlsl"
+}
+
+struct VertexInput
+{
+ #include "common/vertexinput.hlsl"
+};
+
+struct PixelInput
+{
+ #include "common/pixelinput.hlsl"
+ float flDarkening : TEXCOORD14;
+};
+
+VS
+{
+ #include "common/vertex.hlsl"
+
+ // $forward - forward direction vector for teeth lighting
+ float3 g_vForward < Default3( 1.0, 0.0, 0.0 ); UiGroup( "Teeth,10/10" ); >;
+ // $illumfactor - amount to darken or brighten the teeth (default 1)
+ float g_flIllumFactor < Default( 1.0 ); Range( 0.0, 2.0 ); UiGroup( "Teeth,10/20" ); >;
+
+ PixelInput MainVs( VS_INPUT i )
+ {
+ PixelInput o = ProcessVertex( i );
+
+ float3 vNormalOs = normalize( i.vNormalOs.xyz );
+ float flForwardDot = saturate( dot( vNormalOs, normalize( g_vForward ) ) );
+ o.flDarkening = g_flIllumFactor * flForwardDot;
+
+ return FinalizeVertex( o );
+ }
+}
+
+PS
+{
+ StaticCombo( S_BUMPMAP, F_BUMPMAP, Sys( ALL ) );
+
+ #include "common/pixel.hlsl"
+
+ // $basetexture
+ CreateInputTexture2D( TextureColor, Srgb, 8, "", "_color", "Material,10/10", Default3( 1.0, 1.0, 1.0 ) );
+ Texture2D g_tColor < Channel( RGB, Box( TextureColor ), Srgb ); Channel( A, Box( TextureColor ), Linear ); OutputFormat( BC7 ); SrgbRead( true ); >;
+ TextureAttribute( RepresentativeTexture, g_tColor );
+
+ #if S_BUMPMAP
+ // $bumpmap
+ CreateInputTexture2D( TextureNormal, Linear, 8, "NormalizeNormals", "_normal", "Normal Map,10/10", Default3( 0.5, 0.5, 1.0 ) );
+ Texture2D g_tNormal < Channel( RGB, Box( TextureNormal ), Linear ); Channel( A, Box( TextureNormal ), Linear ); OutputFormat( BC7 ); SrgbRead( false ); >;
+ #endif
+
+ // $phongexponent - specular exponent (default 100)
+ float g_flPhongExponent < Default( 100.0 ); Range( 1.0, 150.0 ); UiGroup( "Teeth,10/30" ); >;
+
+ float4 MainPs( PixelInput i ) : SV_Target0
+ {
+ float2 vUV = i.vTextureCoords.xy;
+
+ float4 vBaseSample = g_tColor.Sample( g_sAniso, vUV );
+ float3 vAlbedo = vBaseSample.rgb;
+ float flAlpha = vBaseSample.a;
+
+ float3 vNormalWs = normalize( i.vNormalWs );
+ float flSpecMask = 1.0;
+
+ #if S_BUMPMAP
+ float4 vNormalSample = g_tNormal.Sample( g_sAniso, vUV );
+ float3 vNormalTs = DecodeNormal( vNormalSample.rgb );
+ vNormalTs = normalize( vNormalTs );
+ vNormalWs = TransformNormal( vNormalTs, i.vNormalWs, i.vTangentUWs, i.vTangentVWs );
+ flSpecMask = vNormalSample.a;
+ #endif
+
+ #if S_MODE_DEPTH
+ return float4( 0, 0, 0, flAlpha );
+ #endif
+
+ float3 vPositionWs = i.vPositionWithOffsetWs.xyz + g_vCameraPositionWs;
+ float3 vViewWs = normalize( g_vCameraPositionWs - vPositionWs );
+
+ float3 vDiffuse = float3( 0, 0, 0 );
+ float3 vSpecular = float3( 0, 0, 0 );
+
+ uint nLightCount = Light::Count( i.vPositionSs.xy );
+ for ( uint idx = 0; idx < nLightCount; idx++ )
+ {
+ Light light = Light::From( i.vPositionSs.xy, vPositionWs, idx );
+ float3 vLightDir = light.Direction;
+ float3 vLightColor = light.Color * light.Attenuation * light.Visibility;
+ float flNdotL = saturate( dot( vNormalWs, vLightDir ) );
+
+ vDiffuse += vLightColor * flNdotL;
+
+ #if S_BUMPMAP
+ float3 vReflect = reflect( -vViewWs, vNormalWs );
+ float flRdotL = saturate( dot( vReflect, vLightDir ) );
+ float3 vSpec = pow( flRdotL, g_flPhongExponent ) * flNdotL;
+ vSpecular += vSpec * vLightColor;
+ #endif
+ }
+
+ float3 vAmbient = AmbientLight::From( vPositionWs, i.vPositionSs.xy, vNormalWs );
+ vDiffuse += vAmbient;
+
+ float flDarkening = i.flDarkening;
+
+ float3 vResult = vAlbedo * vDiffuse;
+
+ #if S_BUMPMAP
+ vResult += vSpecular * flSpecMask;
+ #endif
+
+ vResult *= flDarkening;
+
+ float4 vColor = float4( vResult, 1.0 );
+ vColor = DoAtmospherics( vPositionWs, i.vPositionSs.xy, vColor );
+
+ return vColor;
+ }
+}
diff --git a/game/mount/hl2/Assets/shaders/unlitgeneric.shader b/game/mount/hl2/Assets/shaders/unlitgeneric.shader
new file mode 100644
index 000000000..5802d5603
--- /dev/null
+++ b/game/mount/hl2/Assets/shaders/unlitgeneric.shader
@@ -0,0 +1,180 @@
+HEADER
+{
+ Description = "Source Engine UnlitGeneric Shader";
+ Version = 1;
+ DevShader = false;
+}
+
+FEATURES
+{
+ #include "common/features.hlsl"
+ Feature( F_TRANSLUCENT, 0..1, "Rendering" );
+ Feature( F_ALPHA_TEST, 0..1, "Rendering" );
+ Feature( F_DETAIL, 0..1, "Detail Texture" );
+ Feature( F_ENVMAP, 0..1, "Environment Map" );
+ Feature( F_VERTEX_COLOR, 0..1, "Vertex Color" );
+ Feature( F_VERTEX_ALPHA, 0..1, "Vertex Alpha" );
+}
+
+MODES
+{
+ Forward();
+ Depth( S_MODE_DEPTH );
+ ToolsShadingComplexity( "tools_shading_complexity.shader" );
+}
+
+COMMON
+{
+ #define CUSTOM_MATERIAL_INPUTS
+ #include "common/shared.hlsl"
+}
+
+struct VertexInput
+{
+ #include "common/vertexinput.hlsl"
+};
+
+struct PixelInput
+{
+ #include "common/pixelinput.hlsl"
+};
+
+VS
+{
+ #include "common/vertex.hlsl"
+
+ PixelInput MainVs( VS_INPUT i )
+ {
+ PixelInput o = ProcessVertex( i );
+ return FinalizeVertex( o );
+ }
+}
+
+PS
+{
+ StaticCombo( S_TRANSLUCENT, F_TRANSLUCENT, Sys( ALL ) );
+ StaticCombo( S_ALPHA_TEST, F_ALPHA_TEST, Sys( ALL ) );
+ StaticCombo( S_ADDITIVE_BLEND, F_ADDITIVE_BLEND, Sys( ALL ) );
+ StaticCombo( S_DETAIL, F_DETAIL, Sys( ALL ) );
+ StaticCombo( S_ENVMAP, F_ENVMAP, Sys( ALL ) );
+ StaticCombo( S_VERTEX_COLOR, F_VERTEX_COLOR, Sys( ALL ) );
+ StaticCombo( S_VERTEX_ALPHA, F_VERTEX_ALPHA, Sys( ALL ) );
+
+ #include "common/pixel.hlsl"
+
+ // $basetexture
+ CreateInputTexture2D( TextureColor, Srgb, 8, "", "_color", "Material,10/10", Default3( 1.0, 1.0, 1.0 ) );
+ Texture2D g_tColor < Channel( RGB, Box( TextureColor ), Srgb ); Channel( A, Box( TextureColor ), Linear ); OutputFormat( BC7 ); SrgbRead( true ); >;
+ TextureAttribute( RepresentativeTexture, g_tColor );
+
+ // $color - color tint (default [1 1 1])
+ float3 g_vColorTint < UiType( Color ); Default3( 1.0, 1.0, 1.0 ); UiGroup( "Material,10/20" ); >;
+ // $alpha - opacity (default 1)
+ float g_flAlpha < Default( 1.0 ); Range( 0.0, 1.0 ); UiGroup( "Material,10/30" ); >;
+
+ #if S_DETAIL
+ // $detail
+ CreateInputTexture2D( TextureDetail, Srgb, 8, "", "_detail", "Detail,10/10", Default3( 0.5, 0.5, 0.5 ) );
+ Texture2D g_tDetail < Channel( RGB, Box( TextureDetail ), Srgb ); OutputFormat( BC7 ); SrgbRead( true ); >;
+
+ // $detailscale - detail texture UV scale (default 4)
+ float g_flDetailScale < Default( 4.0 ); Range( 0.1, 32.0 ); UiGroup( "Detail,10/20" ); >;
+ // $detailblendfactor - detail blend amount (default 1)
+ float g_flDetailBlendFactor < Default( 1.0 ); Range( 0.0, 1.0 ); UiGroup( "Detail,10/30" ); >;
+ // $detailblendmode - 0=mod2x, 1=additive, 2=alpha blend, 3=crossfade (default 0)
+ int g_nDetailBlendMode < Default( 0 ); Range( 0, 3 ); UiGroup( "Detail,10/40" ); >;
+ #endif
+
+ #if S_ENVMAP
+ // $envmap
+ CreateInputTextureCube( TextureEnvMap, Srgb, 8, "", "_envmap", "Environment Map,10/10", Default3( 0.0, 0.0, 0.0 ) );
+ TextureCube g_tEnvMap < Channel( RGBA, Box( TextureEnvMap ), Srgb ); OutputFormat( BC6H ); SrgbRead( true ); >;
+
+ // $envmaptint - envmap color tint (default [1 1 1])
+ float3 g_vEnvMapTint < UiType( Color ); Default3( 1.0, 1.0, 1.0 ); UiGroup( "Environment Map,10/20" ); >;
+ // $envmapcontrast - 0=normal, 1=color*color (default 0)
+ float g_flEnvMapContrast < Default( 0.0 ); Range( 0.0, 1.0 ); UiGroup( "Environment Map,10/30" ); >;
+ // $envmapsaturation - 0=greyscale, 1=normal (default 1)
+ float g_flEnvMapSaturation < Default( 1.0 ); Range( 0.0, 1.0 ); UiGroup( "Environment Map,10/40" ); >;
+ #endif
+
+ float3 ApplyUnlitDetail( float3 vBase, float3 vDetail, int nBlendMode, float flBlendFactor )
+ {
+ switch ( nBlendMode )
+ {
+ case 0: // Mod2x
+ default:
+ return vBase * lerp( float3( 1, 1, 1 ), vDetail * 2.0, flBlendFactor );
+ case 1: // Additive
+ return saturate( vBase + vDetail * flBlendFactor );
+ case 2: // Alpha blend
+ case 3: // Crossfade
+ return lerp( vBase, vDetail, flBlendFactor );
+ }
+ }
+
+ float4 MainPs( PixelInput i ) : SV_Target0
+ {
+ float4 vColor = g_tColor.Sample( g_sAniso, i.vTextureCoords.xy );
+
+ vColor.rgb *= g_vColorTint;
+
+ #if S_VERTEX_COLOR
+ vColor.rgb *= i.vVertexColor.rgb;
+ #endif
+
+ #if S_VERTEX_ALPHA
+ vColor.a *= i.vVertexColor.a;
+ #endif
+
+ vColor.a *= g_flAlpha;
+
+ #if S_DETAIL
+ {
+ float2 vDetailUV = i.vTextureCoords.xy * g_flDetailScale;
+ float3 vDetail = g_tDetail.Sample( g_sAniso, vDetailUV ).rgb;
+ vColor.rgb = ApplyUnlitDetail( vColor.rgb, vDetail, g_nDetailBlendMode, g_flDetailBlendFactor );
+ }
+ #endif
+
+ #if S_ENVMAP
+ {
+ float3 vViewRay = normalize( i.vPositionWithOffsetWs.xyz );
+ float3 vReflect = reflect( vViewRay, i.vNormalWs );
+ float3 vEnvColor = g_tEnvMap.SampleLevel( g_sAniso, vReflect, 0 ).rgb;
+
+ vEnvColor = lerp( vEnvColor, vEnvColor * vEnvColor, g_flEnvMapContrast );
+
+ float flLuminance = dot( vEnvColor, float3( 0.299, 0.587, 0.114 ) );
+ vEnvColor = lerp( float3( flLuminance, flLuminance, flLuminance ), vEnvColor, g_flEnvMapSaturation );
+
+ vColor.rgb += vEnvColor * g_vEnvMapTint;
+ }
+ #endif
+
+ #if S_MODE_DEPTH
+ return float4( 0, 0, 0, vColor.a );
+ #endif
+
+ if ( DepthNormals::WantsDepthNormals() )
+ return DepthNormals::Output( i.vNormalWs, 1.0, vColor.a );
+
+ if ( ToolsVis::WantsToolsVis() )
+ {
+ ToolsVis toolVis = ToolsVis::Init( vColor, float3(0,0,0), float3(0,0,0), vColor.rgb, float3(0,0,0), float3(0,0,0) );
+ toolVis.HandleFullbright( vColor, vColor.rgb, i.vPositionWithOffsetWs.xyz + g_vCameraPositionWs, i.vNormalWs );
+ toolVis.HandleAlbedo( vColor, vColor.rgb );
+ return vColor;
+ }
+
+ float3 vWorldPos = i.vPositionWithOffsetWs.xyz + g_vCameraPositionWs;
+
+ #if S_ADDITIVE_BLEND
+ vColor = DoAtmospherics( vWorldPos, i.vPositionSs.xy, vColor, true );
+ #else
+ vColor = DoAtmospherics( vWorldPos, i.vPositionSs.xy, vColor, false );
+ #endif
+
+ return vColor;
+ }
+}
diff --git a/game/mount/hl2/Assets/shaders/vertexlitgeneric.shader b/game/mount/hl2/Assets/shaders/vertexlitgeneric.shader
new file mode 100644
index 000000000..808c39e99
--- /dev/null
+++ b/game/mount/hl2/Assets/shaders/vertexlitgeneric.shader
@@ -0,0 +1,597 @@
+HEADER
+{
+ Description = "Source Engine VertexLitGeneric Shader";
+ Version = 1;
+ DevShader = false;
+}
+
+FEATURES
+{
+ #include "common/features.hlsl"
+ Feature( F_TRANSLUCENT, 0..1, "Rendering" );
+ Feature( F_ALPHA_TEST, 0..1, "Rendering" );
+ Feature( F_BUMPMAP, 0..1, "Normal Mapping" );
+ Feature( F_PHONG, 0..1, "Specular" );
+ Feature( F_SELFILLUM, 0..1, "Self Illumination" );
+ Feature( F_RIMLIGHT, 0..1, "Rim Lighting" );
+ Feature( F_DETAIL, 0..1, "Detail Texture" );
+ Feature( F_ENVMAP, 0..1, "Environment Map" );
+ Feature( F_HALFLAMBERT, 0..1, "Lighting" );
+ Feature( F_LIGHTWARP, 0..1, "Light Warp" );
+ Feature( F_PHONGWARP, 0..1, "Phong Warp" );
+}
+
+MODES
+{
+ Forward();
+ Depth( S_MODE_DEPTH );
+ ToolsShadingComplexity( "tools_shading_complexity.shader" );
+}
+
+COMMON
+{
+ #include "common/shared.hlsl"
+}
+
+struct VertexInput
+{
+ #include "common/vertexinput.hlsl"
+};
+
+struct PixelInput
+{
+ #include "common/pixelinput.hlsl"
+};
+
+VS
+{
+ #include "common/vertex.hlsl"
+
+ PixelInput MainVs( VS_INPUT i )
+ {
+ PixelInput o = ProcessVertex( i );
+ return FinalizeVertex( o );
+ }
+}
+
+PS
+{
+ StaticCombo( S_TRANSLUCENT, F_TRANSLUCENT, Sys( ALL ) );
+ StaticCombo( S_ALPHA_TEST, F_ALPHA_TEST, Sys( ALL ) );
+ StaticCombo( S_BUMPMAP, F_BUMPMAP, Sys( ALL ) );
+ StaticCombo( S_PHONG, F_PHONG, Sys( ALL ) );
+ StaticCombo( S_SELFILLUM, F_SELFILLUM, Sys( ALL ) );
+ StaticCombo( S_RIMLIGHT, F_RIMLIGHT, Sys( ALL ) );
+ StaticCombo( S_DETAIL, F_DETAIL, Sys( ALL ) );
+ StaticCombo( S_ENVMAP, F_ENVMAP, Sys( ALL ) );
+ StaticCombo( S_HALFLAMBERT, F_HALFLAMBERT, Sys( ALL ) );
+ StaticCombo( S_LIGHTWARP, F_LIGHTWARP, Sys( ALL ) );
+ StaticCombo( S_PHONGWARP, F_PHONGWARP, Sys( ALL ) );
+
+ #include "common/pixel.hlsl"
+
+ float SourceFresnel( float3 vNormal, float3 vEyeDir, float3 vRanges )
+ {
+ // Optimized piecewise fresnel: blends low->mid (0-0.5) and mid->high (0.5-1)
+ // vRanges is pre-encoded as ((mid-min)*2, mid, (max-mid)*2)
+ float3 vEncodedRanges = float3(
+ ( vRanges.y - vRanges.x ) * 2.0,
+ vRanges.y,
+ ( vRanges.z - vRanges.y ) * 2.0
+ );
+ float f = saturate( 1.0 - dot( vNormal, vEyeDir ) );
+ f = f * f - 0.5;
+ return vEncodedRanges.y + ( f >= 0.0 ? vEncodedRanges.z : vEncodedRanges.x ) * f;
+ }
+
+ float SourceFresnel4( float3 vNormal, float3 vEyeDir )
+ {
+ // Traditional fresnel using 4th power (square twice)
+ float fresnel = saturate( 1.0 - dot( vNormal, vEyeDir ) );
+ fresnel = fresnel * fresnel;
+ return fresnel * fresnel;
+ }
+
+ float HalfLambert( float flNdotL )
+ {
+ float flHalfLambert = flNdotL * 0.5 + 0.5;
+ return flHalfLambert * flHalfLambert;
+ }
+
+ float Lambert( float flNdotL )
+ {
+ return saturate( flNdotL );
+ }
+
+ #if S_LIGHTWARP
+ CreateInputTexture2D( TextureLightWarp, Srgb, 8, "", "_lightwarp", "Light Warp,10/10", Default3( 1.0, 1.0, 1.0 ) );
+ Texture2D g_tLightWarp < Channel( RGB, Box( TextureLightWarp ), Srgb ); OutputFormat( BC7 ); SrgbRead( true ); AddressU( CLAMP ); AddressV( CLAMP ); >;
+ #endif
+
+ #if S_PHONGWARP
+ CreateInputTexture2D( TexturePhongWarp, Srgb, 8, "", "_phongwarp", "Phong Warp,10/10", Default3( 1.0, 1.0, 1.0 ) );
+ Texture2D g_tPhongWarp < Channel( RGB, Box( TexturePhongWarp ), Srgb ); OutputFormat( BC7 ); SrgbRead( true ); AddressU( CLAMP ); AddressV( CLAMP ); >;
+ #endif
+
+ void SourceSpecularAndRimTerms(
+ float3 vWorldNormal, float3 vLightDir, float flSpecularExponent,
+ float3 vEyeDir, float3 vLightColor, float flFresnel,
+ bool bDoRimLighting, float flRimExponent,
+ bool bDoSpecularWarp,
+ out float3 specularLighting, out float3 rimLighting )
+ {
+ rimLighting = float3( 0.0, 0.0, 0.0 );
+
+ float3 vReflect = 2.0 * vWorldNormal * dot( vWorldNormal, vEyeDir ) - vEyeDir;
+ float flLdotR = saturate( dot( vReflect, vLightDir ) );
+ specularLighting = pow( flLdotR, flSpecularExponent );
+
+ if ( bDoSpecularWarp )
+ {
+ #if S_PHONGWARP
+ specularLighting *= g_tPhongWarp.Sample( g_sAniso, float2( specularLighting.x, flFresnel ) ).rgb;
+ #endif
+ }
+
+ float flNdotL = saturate( dot( vWorldNormal, vLightDir ) );
+ specularLighting *= flNdotL;
+ specularLighting *= vLightColor;
+
+ if ( bDoRimLighting )
+ {
+ rimLighting = pow( flLdotR, flRimExponent ) * flNdotL * vLightColor;
+ }
+ }
+
+ class ShadingModelSource
+ {
+ static float4 Shade(
+ PixelInput i, float3 vAlbedo, float3 vNormalWs, float flOpacity, float3 vEmission,
+ bool bHalfLambert, bool bPhong, float flPhongExponent, float flPhongBoost,
+ float3 vPhongTint, float3 vPhongFresnelRanges, float flPhongAlbedoTint, float flPhongMask,
+ bool bRimLight, float flRimExponent, float flRimBoost, float flRimMask,
+ bool bLightWarp, bool bPhongWarp, float2 vUV )
+ {
+ float4 vColor = float4( 0, 0, 0, flOpacity );
+
+ float3 vPositionWs = i.vPositionWithOffsetWs.xyz + g_vCameraPositionWs;
+ float3 vViewWs = normalize( g_vCameraPositionWs - vPositionWs );
+
+ float flFresnelRanges = 1.0;
+ float flRimFresnel = SourceFresnel4( vNormalWs, vViewWs );
+
+ if ( bPhong )
+ flFresnelRanges = SourceFresnel( vNormalWs, vViewWs, vPhongFresnelRanges );
+
+ float3 vDiffuse = float3( 0, 0, 0 );
+ float3 vSpecular = float3( 0, 0, 0 );
+ float3 vRimLighting = float3( 0, 0, 0 );
+
+ uint nLightCount = Light::Count( i.vPositionSs.xy );
+ for ( uint idx = 0; idx < nLightCount; idx++ )
+ {
+ Light light = Light::From( i.vPositionSs.xy, vPositionWs, idx );
+ float3 vLightDir = light.Direction;
+ float3 vLightColor = light.Color * light.Attenuation * light.Visibility;
+ float flNdotL = dot( vNormalWs, vLightDir );
+
+ float3 vDiffuseTerm;
+ if ( bLightWarp )
+ {
+ float flWarpCoord = bHalfLambert ? saturate( flNdotL * 0.5 + 0.5 ) : saturate( flNdotL );
+ #if S_LIGHTWARP
+ vDiffuseTerm = 2.0 * g_tLightWarp.Sample( g_sAniso, float2( flWarpCoord, 0.5 ) ).rgb;
+ #else
+ vDiffuseTerm = float3( flWarpCoord, flWarpCoord, flWarpCoord );
+ #endif
+ }
+ else
+ {
+ float flDiffuseScalar = bHalfLambert ? HalfLambert( flNdotL ) : Lambert( flNdotL );
+ vDiffuseTerm = float3( flDiffuseScalar, flDiffuseScalar, flDiffuseScalar );
+ }
+ vDiffuse += vAlbedo * vLightColor * vDiffuseTerm;
+
+ if ( bPhong || bRimLight )
+ {
+ float3 localSpec = float3( 0, 0, 0 );
+ float3 localRim = float3( 0, 0, 0 );
+
+ SourceSpecularAndRimTerms( vNormalWs, vLightDir, flPhongExponent, vViewWs, vLightColor,
+ flFresnelRanges, bRimLight, flRimExponent, bPhongWarp, localSpec, localRim );
+
+ vSpecular += localSpec;
+ vRimLighting += localRim;
+ }
+ }
+
+ if ( bPhong )
+ {
+ if ( bPhongWarp )
+ {
+ vSpecular *= flPhongMask * flPhongBoost;
+ }
+ else
+ {
+ float flFinalMask = flPhongMask * flFresnelRanges;
+ vSpecular *= flFinalMask * flPhongBoost;
+ }
+ }
+
+ if ( bRimLight )
+ {
+ float flRimMultiply = flRimMask * flRimFresnel;
+ vRimLighting *= flRimMultiply;
+ }
+
+ float3 vAmbientNormal = normalize( lerp( vNormalWs, vViewWs, 0.3 ) );
+ float3 vAmbient = AmbientLight::From( vPositionWs, i.vPositionSs.xy, vAmbientNormal );
+ float flAmbientScale = bHalfLambert ? 1.15 : 1.0;
+ float3 vIndirectDiffuse = vAlbedo * vAmbient * flAmbientScale;
+
+ float3 vAmbientRim = float3( 0, 0, 0 );
+ if ( bRimLight )
+ {
+ float flRimMultiply = flRimMask * flRimFresnel;
+ float3 vAmbientRimColor = AmbientLight::From( vPositionWs, i.vPositionSs.xy, vViewWs );
+ vAmbientRim = vAmbientRimColor * flRimBoost * saturate( flRimMultiply * vNormalWs.z );
+ }
+
+ vRimLighting = max( vRimLighting, vAmbientRim );
+ vSpecular = max( vSpecular, vRimLighting );
+
+ float3 vSpecTint = lerp( vPhongTint, vPhongTint * vAlbedo, flPhongAlbedoTint );
+ vSpecular *= vSpecTint;
+
+ vColor.rgb = vDiffuse + vIndirectDiffuse + vSpecular + vEmission;
+
+ if ( DepthNormals::WantsDepthNormals() )
+ {
+ float flRoughness = bPhong ? sqrt( 2.0 / ( flPhongExponent + 2.0 ) ) : 1.0;
+ return DepthNormals::Output( vNormalWs, flRoughness, flOpacity );
+ }
+
+ if ( ToolsVis::WantsToolsVis() )
+ {
+ ToolsVis toolVis = ToolsVis::Init( vColor, vDiffuse, vSpecular, vIndirectDiffuse, float3(0,0,0), float3(0,0,0) );
+ toolVis.HandleFullbright( vColor, vAlbedo, vPositionWs, vNormalWs );
+ toolVis.HandleDiffuseLighting( vColor );
+ toolVis.HandleSpecularLighting( vColor );
+ toolVis.HandleAlbedo( vColor, vAlbedo );
+ toolVis.HandleNormalWs( vColor, vNormalWs );
+ return vColor;
+ }
+
+ vColor = DoAtmospherics( vPositionWs, i.vPositionSs.xy, vColor );
+ return vColor;
+ }
+ };
+
+ // $basetexture
+ CreateInputTexture2D( TextureColor, Srgb, 8, "", "_color", "Material,10/10", Default3( 1.0, 1.0, 1.0 ) );
+ Texture2D g_tColor < Channel( RGB, Box( TextureColor ), Srgb ); Channel( A, Box( TextureColor ), Linear ); OutputFormat( BC7 ); SrgbRead( true ); >;
+ TextureAttribute( RepresentativeTexture, g_tColor );
+
+ // $color - color tint (default [1 1 1])
+ float3 g_vColorTint < UiType( Color ); Default3( 1.0, 1.0, 1.0 ); UiGroup( "Material,10/30" ); >;
+ // $alpha - opacity (default 1)
+ float g_flAlpha < Default( 1.0 ); Range( 0.0, 1.0 ); UiGroup( "Material,10/40" ); >;
+
+ #if S_BUMPMAP
+ // $bumpmap
+ CreateInputTexture2D( TextureNormal, Linear, 8, "NormalizeNormals", "_normal", "Normal Map,10/10", Default3( 0.5, 0.5, 1.0 ) );
+ Texture2D g_tNormal < Channel( RGB, Box( TextureNormal ), Linear ); Channel( A, Box( TextureNormal ), Linear ); OutputFormat( BC7 ); SrgbRead( false ); >;
+ #endif
+
+ #if S_PHONG
+ // $phongexponenttexture - R=exponent, G=albedotint, B=unused, A=rimmask
+ CreateInputTexture2D( TexturePhongExponent, Linear, 8, "", "_exponent", "Phong,10/10", Default4( 0.5, 0.0, 0.0, 1.0 ) );
+ Texture2D g_tPhongExponent < Channel( RGBA, Box( TexturePhongExponent ), Linear ); OutputFormat( BC7 ); SrgbRead( false ); >;
+
+ // $phongexponent - specular exponent (default 5, use -1 to read from texture)
+ float g_flPhongExponent < Default( 20.0 ); Range( -1.0, 150.0 ); UiGroup( "Phong,10/20" ); >;
+ // $phongboost - specular boost multiplier (default 1)
+ float g_flPhongBoost < Default( 1.0 ); Range( 0.0, 10.0 ); UiGroup( "Phong,10/30" ); >;
+ // $phongtint - specular color tint (default [1 1 1])
+ float3 g_vPhongTint < UiType( Color ); Default3( 1.0, 1.0, 1.0 ); UiGroup( "Phong,10/40" ); >;
+ // $phongfresnelranges - fresnel remap [min center max] (default [0 0.5 1])
+ float3 g_vPhongFresnelRanges < Default3( 0.0, 0.5, 1.0 ); UiGroup( "Phong,10/50" ); >;
+ // $phongalbedotint - tint specular by albedo (default 0)
+ float g_flPhongAlbedoTint < Default( 0.0 ); Range( 0.0, 1.0 ); UiGroup( "Phong,10/60" ); >;
+ // $basemapalphaphongmask - use base texture alpha as phong mask (default 0)
+ int g_nBaseMapAlphaPhongMask < Default( 0 ); Range( 0, 1 ); UiGroup( "Phong,10/70" ); >;
+ // $invertphongmask - invert the phong mask (default 0)
+ int g_nInvertPhongMask < Default( 0 ); Range( 0, 1 ); UiGroup( "Phong,10/80" ); >;
+ #endif
+
+ #if S_SELFILLUM
+ // $selfillummask - separate self-illumination mask texture
+ CreateInputTexture2D( TextureSelfIllumMask, Linear, 8, "", "_selfillum", "Self Illumination,10/10", Default( 0.0 ) );
+ Texture2D g_tSelfIllumMask < Channel( R, Box( TextureSelfIllumMask ), Linear ); OutputFormat( BC7 ); SrgbRead( false ); >;
+
+ // $selfillumtint - self-illumination color tint (default [1 1 1])
+ float3 g_vSelfIllumTint < UiType( Color ); Default3( 1.0, 1.0, 1.0 ); UiGroup( "Self Illumination,10/20" ); >;
+ // 0 = use base alpha, 1 = use mask texture
+ float g_flSelfIllumMaskControl < Default( 0.0 ); Range( 0.0, 1.0 ); UiGroup( "Self Illumination,10/30" ); >;
+ #endif
+
+ #if S_RIMLIGHT
+ // $rimlightexponent - rim light exponent (default 4)
+ float g_flRimLightExponent < Default( 4.0 ); Range( 0.1, 20.0 ); UiGroup( "Rim Light,10/10" ); >;
+ // $rimlightboost - rim light boost (default 1)
+ float g_flRimLightBoost < Default( 1.0 ); Range( 0.0, 10.0 ); UiGroup( "Rim Light,10/20" ); >;
+ // $rimmask - use exponent texture alpha as rim mask (default 0)
+ int g_nRimMask < Default( 0 ); Range( 0, 1 ); UiGroup( "Rim Light,10/30" ); >;
+ #endif
+
+ #if S_DETAIL
+ // $detail
+ CreateInputTexture2D( TextureDetail, Srgb, 8, "", "_detail", "Detail,10/10", Default3( 0.5, 0.5, 0.5 ) );
+ Texture2D g_tDetail < Channel( RGB, Box( TextureDetail ), Srgb ); OutputFormat( BC7 ); SrgbRead( true ); >;
+
+ // $detailscale - detail texture UV scale (default 4)
+ float g_flDetailScale < Default( 4.0 ); Range( 0.1, 32.0 ); UiGroup( "Detail,10/20" ); >;
+ // $detailblendfactor - detail blend amount (default 1)
+ float g_flDetailBlendFactor < Default( 1.0 ); Range( 0.0, 1.0 ); UiGroup( "Detail,10/30" ); >;
+ // $detailblendmode - blend mode 0-9 (default 0 = mod2x)
+ int g_nDetailBlendMode < Default( 0 ); Range( 0, 9 ); UiGroup( "Detail,10/40" ); >;
+ // $detailtint - detail color tint (default [1 1 1])
+ float3 g_vDetailTint < UiType( Color ); Default3( 1.0, 1.0, 1.0 ); UiGroup( "Detail,10/50" ); >;
+ #endif
+
+ #if S_ENVMAP
+ // $envmapmask
+ CreateInputTexture2D( TextureEnvMapMask, Linear, 8, "", "_envmapmask", "Environment Map,10/10", Default( 1.0 ) );
+ // $envmap
+ CreateInputTextureCube( TextureEnvMap, Srgb, 8, "", "_envmap", "Environment Map,10/15", Default3( 0.0, 0.0, 0.0 ) );
+ Texture2D g_tEnvMapMask < Channel( R, Box( TextureEnvMapMask ), Linear ); OutputFormat( BC7 ); SrgbRead( false ); >;
+ TextureCube g_tEnvMap < Channel( RGBA, Box( TextureEnvMap ), Srgb ); OutputFormat( BC6H ); SrgbRead( true ); >;
+
+ // $envmaptint - envmap color tint (default [1 1 1])
+ float3 g_vEnvMapTint < UiType( Color ); Default3( 1.0, 1.0, 1.0 ); UiGroup( "Environment Map,10/20" ); >;
+ // $envmapcontrast - 0=normal, 1=color*color (default 0)
+ float g_flEnvMapContrast < Default( 0.0 ); Range( 0.0, 1.0 ); UiGroup( "Environment Map,10/30" ); >;
+ // $envmapsaturation - 0=greyscale, 1=normal (default 1)
+ float g_flEnvMapSaturation < Default( 1.0 ); Range( 0.0, 1.0 ); UiGroup( "Environment Map,10/40" ); >;
+ // $envmapfresnel - fresnel for envmap (default 0)
+ float g_flEnvMapFresnel < Default( 0.0 ); Range( 0.0, 1.0 ); UiGroup( "Environment Map,10/50" ); >;
+ // $basealphaenvmapmask - use base alpha as envmap mask (default 0)
+ int g_nBaseAlphaEnvMapMask < Default( 0 ); Range( 0, 1 ); UiGroup( "Environment Map,10/60" ); >;
+ // $normalmapalphaenvmapmask - use normalmap alpha as envmap mask (default 0)
+ int g_nNormalMapAlphaEnvMapMask < Default( 0 ); Range( 0, 1 ); UiGroup( "Environment Map,10/70" ); >;
+ #endif
+
+ float3 ApplyDetailTexture( float3 vBase, float3 vDetail, int nBlendMode, float flBlendFactor )
+ {
+ switch ( nBlendMode )
+ {
+ case 0: // Mod2x
+ default:
+ return vBase * lerp( float3( 1, 1, 1 ), vDetail * 2.0, flBlendFactor );
+ case 1: // Additive
+ case 5: // Unlit additive
+ case 6: // Unlit additive threshold fade
+ return saturate( vBase + vDetail * flBlendFactor );
+ case 2: // Translucent detail
+ case 3: // Blend factor fade
+ return lerp( vBase, vDetail, flBlendFactor );
+ case 4: // Translucent base
+ case 9: // Base over detail
+ return lerp( vDetail, vBase, flBlendFactor );
+ case 7: // Two-pattern decal modulate
+ return vBase * lerp( float3( 1, 1, 1 ), vDetail * 2.0, flBlendFactor );
+ case 8: // Multiply
+ return vBase * lerp( float3( 1, 1, 1 ), vDetail, flBlendFactor );
+ }
+ }
+
+ float4 MainPs( PixelInput i ) : SV_Target0
+ {
+ float2 vUV = i.vTextureCoords.xy;
+
+ float4 vBaseTexture = g_tColor.Sample( g_sAniso, vUV );
+ float3 vAlbedo = vBaseTexture.rgb * g_vColorTint;
+ float flBaseAlpha = vBaseTexture.a;
+ float flAlpha = flBaseAlpha * g_flAlpha;
+
+ #if S_ALPHA_TEST
+ if ( flAlpha < g_flAlphaTestReference )
+ discard;
+ #endif
+
+ float3 vNormalTs = float3( 0.0, 0.0, 1.0 );
+ float flNormalAlpha = 1.0;
+ #if S_BUMPMAP
+ float4 vNormalSample = g_tNormal.Sample( g_sAniso, vUV );
+ vNormalTs = DecodeNormal( vNormalSample.rgb );
+ vNormalTs = normalize( vNormalTs );
+ flNormalAlpha = vNormalSample.a;
+ #endif
+
+ float3 vNormalWs = TransformNormal( vNormalTs, i.vNormalWs, i.vTangentUWs, i.vTangentVWs );
+
+ #if S_DETAIL
+ {
+ float2 vDetailUV = vUV * g_flDetailScale;
+ float3 vDetail = g_tDetail.Sample( g_sAniso, vDetailUV ).rgb * g_vDetailTint;
+ vAlbedo = ApplyDetailTexture( vAlbedo, vDetail, g_nDetailBlendMode, g_flDetailBlendFactor );
+ }
+ #endif
+
+ #if S_MODE_DEPTH
+ return float4( 0, 0, 0, flAlpha );
+ #endif
+
+ float3 vPositionWs = i.vPositionWithOffsetWs.xyz + g_vCameraPositionWs;
+ float3 vViewWs = normalize( g_vCameraPositionWs - vPositionWs );
+ float flNdotV = saturate( dot( vNormalWs, vViewWs ) );
+
+ float3 vEmission = float3( 0, 0, 0 );
+
+ float flPhongMask = 1.0;
+ float flPhongExponent = 20.0;
+ float flPhongBoost = 1.0;
+ float3 vPhongTint = float3( 1, 1, 1 );
+ float3 vPhongFresnelRanges = float3( 0, 0.5, 1 );
+ float flPhongAlbedoTint = 0.0;
+
+ #if S_PHONG
+ {
+ float4 vExpMapSample = float4( 1.0, 1.0, 0.0, 1.0 );
+ bool bNeedExpTexture = ( g_flPhongExponent < 0.0 );
+
+ if ( bNeedExpTexture )
+ {
+ vExpMapSample = g_tPhongExponent.Sample( g_sAniso, vUV );
+ }
+
+ if ( g_nBaseMapAlphaPhongMask != 0 )
+ {
+ flPhongMask = flBaseAlpha;
+ }
+ else
+ {
+ #if S_BUMPMAP
+ flPhongMask = flNormalAlpha;
+ #else
+ flPhongMask = 1.0;
+ #endif
+ }
+
+ if ( g_nInvertPhongMask != 0 )
+ flPhongMask = 1.0 - flPhongMask;
+
+ if ( g_flPhongExponent >= 0.0 )
+ {
+ flPhongExponent = g_flPhongExponent;
+ }
+ else
+ {
+ flPhongExponent = 1.0 + 149.0 * vExpMapSample.r;
+ }
+
+ flPhongBoost = g_flPhongBoost;
+ vPhongTint = g_vPhongTint;
+ vPhongFresnelRanges = g_vPhongFresnelRanges;
+ flPhongAlbedoTint = g_flPhongAlbedoTint * ( bNeedExpTexture ? vExpMapSample.g : 1.0 );
+ }
+ #endif
+
+ float flRimMask = 1.0;
+ float flRimExponent = 4.0;
+ float flRimBoost = 1.0;
+
+ #if S_RIMLIGHT
+ {
+ flRimExponent = g_flRimLightExponent;
+ flRimBoost = g_flRimLightBoost;
+
+ #if S_PHONG
+ if ( g_nRimMask != 0 )
+ {
+ flRimMask = g_tPhongExponent.Sample( g_sAniso, vUV ).a;
+ }
+ #endif
+ }
+ #endif
+
+ #if S_ENVMAP
+ {
+ float flEnvMapMask = 1.0;
+
+ if ( g_nBaseAlphaEnvMapMask != 0 )
+ {
+ flEnvMapMask = flBaseAlpha;
+ }
+ else if ( g_nNormalMapAlphaEnvMapMask != 0 )
+ {
+ #if S_BUMPMAP
+ flEnvMapMask = flNormalAlpha;
+ #endif
+ }
+ else
+ {
+ flEnvMapMask = g_tEnvMapMask.Sample( g_sAniso, vUV ).r;
+ }
+
+ float flEnvFresnel = 1.0;
+ if ( g_flEnvMapFresnel > 0.0 )
+ {
+ flEnvFresnel = SourceFresnel4( vNormalWs, vViewWs );
+ flEnvFresnel = lerp( 1.0, flEnvFresnel, g_flEnvMapFresnel );
+ }
+
+ float3 vReflectWs = reflect( -vViewWs, vNormalWs );
+
+ float flRoughness = 0.5;
+ #if S_PHONG
+ flRoughness = sqrt( 2.0 / ( flPhongExponent + 2.0 ) );
+ #endif
+
+ float3 vEnvColor = g_tEnvMap.SampleLevel( g_sAniso, vReflectWs, flRoughness * 6.0 ).rgb;
+
+ vEnvColor = lerp( vEnvColor, vEnvColor * vEnvColor, g_flEnvMapContrast );
+
+ float flLuminance = dot( vEnvColor, float3( 0.299, 0.587, 0.114 ) );
+ vEnvColor = lerp( float3( flLuminance, flLuminance, flLuminance ), vEnvColor, g_flEnvMapSaturation );
+
+ vEmission += vEnvColor * g_vEnvMapTint * flEnvMapMask * flEnvFresnel;
+ }
+ #endif
+
+ #if S_SELFILLUM
+ {
+ float flSelfIllumMask = flBaseAlpha;
+ if ( g_flSelfIllumMaskControl > 0.0 )
+ {
+ flSelfIllumMask = g_tSelfIllumMask.Sample( g_sAniso, vUV ).r;
+ }
+
+ vEmission += vAlbedo * flSelfIllumMask * g_vSelfIllumTint;
+ }
+ #endif
+
+ return ShadingModelSource::Shade(
+ i,
+ vAlbedo,
+ vNormalWs,
+ flAlpha,
+ vEmission,
+ #if S_HALFLAMBERT || S_PHONG
+ true,
+ #else
+ false,
+ #endif
+ #if S_PHONG
+ true,
+ #else
+ false,
+ #endif
+ flPhongExponent,
+ flPhongBoost,
+ vPhongTint,
+ vPhongFresnelRanges,
+ flPhongAlbedoTint,
+ flPhongMask,
+ #if S_RIMLIGHT && S_PHONG
+ true,
+ #else
+ false,
+ #endif
+ flRimExponent,
+ flRimBoost,
+ flRimMask,
+ #if S_LIGHTWARP
+ true,
+ #else
+ false,
+ #endif
+ #if S_PHONGWARP && S_PHONG
+ true,
+ #else
+ false,
+ #endif
+ vUV
+ );
+ }
+}