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 + ); + } +}