diff --git a/cspell.json b/cspell.json index 5315fcdc4..1b67c599d 100644 --- a/cspell.json +++ b/cspell.json @@ -8,7 +8,8 @@ "blitzkit", "webp", "hocs", - "cooldown" + "cooldown", + "Lods" ], "userWords": [ "WOTB", diff --git a/dotnet-tools.json b/dotnet-tools.json new file mode 100644 index 000000000..97f37dcc4 --- /dev/null +++ b/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "csharpier": { + "version": "1.2.6", + "commands": [ + "csharpier" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/packages/game/JSExport/GameInterface.cs b/packages/game/JSExport/GameInterface.cs index 607c21555..5cbfb6452 100644 --- a/packages/game/JSExport/GameInterface.cs +++ b/packages/game/JSExport/GameInterface.cs @@ -1,150 +1,175 @@ using System.Text.RegularExpressions; using BlitzKit.Game.Models; +using CUE4Parse_Conversion; +using CUE4Parse_Conversion.Meshes; using CUE4Parse_Conversion.Textures; -using CUE4Parse.Compression; +using CUE4Parse_Conversion.UEFormat.Enums; using CUE4Parse.UE4.Assets.Exports; +using CUE4Parse.UE4.Assets.Exports.Engine; +using CUE4Parse.UE4.Assets.Exports.StaticMesh; using CUE4Parse.UE4.Assets.Exports.Texture; -using CUE4Parse.UE4.Assets.Objects.Properties; -using CUE4Parse.UE4.IO.Objects; +using CUE4Parse.UE4.Assets.Objects; using CUE4Parse.UE4.Objects.Engine; using CUE4Parse.UE4.Objects.UObject; -using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.JavaScript.NodeApi; using SkiaSharp; -using CUE4Parse.UE4.AssetRegistry.Objects; -using CUE4Parse.UE4.Readers; -using CUE4Parse.UE4.AssetRegistry.Readers; -using CUE4Parse.UE4.AssetRegistry; -using CUE4Parse.UE4.Assets.Exports.Engine; -using CUE4Parse.UE4.Assets.Objects; -using CUE4Parse.UE4.Assets.Exports.StaticMesh; -using CUE4Parse_Conversion.Meshes; -using CUE4Parse_Conversion; namespace BlitzKit.Game.JSExport; [JSExport] public partial class GameInterface { - private readonly BlitzFileProvider provider; + private readonly BlitzFileProvider provider; + private readonly BlitzExporter exporter; - public HashSet files; + public HashSet files; - public GameInterface(string directory, string map, string temp) - { - provider = new(directory, map, temp); - files = [.. provider.Files.Keys]; - } - - public HashSet Files => files; - - public byte[] TankBigIcon(string tag) - { - var pda = provider.Discovered(tag); - var icon = pda.Get("BigIcon"); - return provider.Image(icon); - } - - [GeneratedRegex(@"T_UI_Flag_(\w+)_S")] - private static partial Regex FlagNameRegex(); - - public byte[] Flag(string nation) - { - var flagsBase = "Blitz/Content/UI/Textures/Flag/TankCard/"; - - foreach (var path in provider.Files.Keys) + public GameInterface(string directory, string map, string temp) { - if (!path.StartsWith(flagsBase)) - { - continue; - } + provider = new(directory, map, temp); + exporter = new(); - var fileName = Path.GetFileNameWithoutExtension(path); - var match = FlagNameRegex().Match(fileName); + files = [.. provider.Files.Keys]; + } - if ( - match.Success - && match.Groups[1].Value.Equals(nation, StringComparison.CurrentCultureIgnoreCase) - ) - { - if (provider.TryLoadPackageObject($"{path}.{fileName}", out var uTexture)) + public HashSet Files => files; + + public byte[] TankBigIcon(string tag) + { + var pda = provider.Discovered(tag); + var icon = pda.Get("BigIcon"); + return provider.Image(icon); + } + + [GeneratedRegex(@"T_UI_Flag_(\w+)_S")] + private static partial Regex FlagNameRegex(); + + public byte[] Flag(string nation) + { + var flagsBase = "Blitz/Content/UI/Textures/Flag/TankCard/"; + + foreach (var path in provider.Files.Keys) { - CTexture? cTexture = uTexture.Decode(ETexturePlatform.DesktopMobile); + if (!path.StartsWith(flagsBase)) + { + continue; + } - if (cTexture is null) - { - Console.WriteLine($"No flag found for {nation}"); - return []; - } + var fileName = Path.GetFileNameWithoutExtension(path); + var match = FlagNameRegex().Match(fileName); - SKBitmap bitmap = cTexture.ToSkBitmap(); - SKData data = bitmap.Encode(SKEncodedImageFormat.Webp, 80); + if ( + match.Success + && match.Groups[1].Value.Equals(nation, StringComparison.CurrentCultureIgnoreCase) + ) + { + if ( + provider.TryLoadPackageObject( + $"{path}.{fileName}", + out var uTexture + ) + ) + { + CTexture? cTexture = uTexture.Decode(ETexturePlatform.DesktopMobile); - return data.ToArray(); + if (cTexture is null) + { + Console.WriteLine($"No flag found for {nation}"); + return []; + } + + SKBitmap bitmap = cTexture.ToSkBitmap(); + SKData data = bitmap.Encode(SKEncodedImageFormat.Webp, 80); + + return data.ToArray(); + } + + Console.WriteLine($"No flag found for {nation}"); + return []; + } } Console.WriteLine($"No flag found for {nation}"); return []; - } } - Console.WriteLine($"No flag found for {nation}"); - return []; - } - - public byte[] ConsumableIcon(string tag) - { - var pda = provider.Discovered(tag); - var icon = pda.Get("Icon"); - return provider.Image(icon); - } - - public byte[] EquipmentIcon(string tag) - { - var pda = provider.Discovered(tag); - var icon = pda.Get("Icon"); - return provider.Image(icon); - } - - - readonly Dictionary tankPartPrefixes = new() - { - {"chassis", "DT_Chassis"}, - {"hull", "DT_Hulls"}, - {"turret", "DT_Turrets"}, - {"gun", "DT_Guns"}, - }; - public byte[] TankPart(string tag, string part) - { - var pda = provider.Discovered(tag); - UDataTable? dataTable = null; - - foreach (var (prefix, name) in tankPartPrefixes) + public byte[] ConsumableIcon(string tag) { - if (!part.StartsWith(prefix)) { continue; } - - dataTable = pda.Get(name); - break; + var pda = provider.Discovered(tag); + var icon = pda.Get("Icon"); + return provider.Image(icon); } - if (dataTable == null) + public byte[] EquipmentIcon(string tag) { - throw new ArgumentException($"Unknown tank part: {part}"); + var pda = provider.Discovered(tag); + var icon = pda.Get("Icon"); + return provider.Image(icon); } - dataTable.TryGetDataTableRow(part, StringComparison.Ordinal, out var row); - - if (row == null) + readonly Dictionary tankPartPrefixes = new() { - throw new ArgumentException($"Unknown tank part: {part}"); + { "chassis", "DT_Chassis" }, + { "hull", "DT_Hulls" }, + { "turret", "DT_Turrets" }, + { "gun", "DT_Guns" }, + }; + + public byte[] TankPart(string tag, string part) + { + var pda = provider.Discovered(tag); + UDataTable? dataTable = null; + + foreach (var (prefix, name) in tankPartPrefixes) + { + if (!part.StartsWith(prefix)) + { + continue; + } + + dataTable = pda.Get(name); + break; + } + + if (dataTable == null) + { + throw new ArgumentException($"Unknown tank part: {part}"); + } + + dataTable.TryGetDataTableRow(part, StringComparison.Ordinal, out var row); + + if (row == null) + { + throw new ArgumentException($"Unknown tank part: {part}"); + } + + var visualData = row.Get("VisualData"); + var meshSettings = visualData.Get("MeshSettings"); + // var collisionMesh = meshSettings.Get("CollisionMesh"); + var mesh = meshSettings.Get("GunMesh"); + var gltf = new MonoGltf(mesh); + + exporter.glb(mesh); + + // var options = new ExporterOptions() { + // LodFormat = ELodFormat.FirstLod, + // MeshFormat = EMeshFormat.Gltf2, + // TextureFormat = ETextureFormat.Jpeg, + // }; + // var exporter = new MeshExporter(mesh, options); + // var lod0 = exporter.MeshLods[0]; + + // exporter.TryWriteToDir(new("../../temp/model"), out var _, out var _); + + // Console.WriteLine(lod0.FileName); + // Console.WriteLine(lod0.FileData.Length); + + // foreach (var material in lod0.Materials) + // { + // material.ma + // // material.TryWriteToDir + // } + + return []; } - - var visualData = row.Get("VisualData"); - var meshSettings = visualData.Get("MeshSettings"); - // var collisionMesh = meshSettings.Get("CollisionMesh"); - var mesh = meshSettings.Get("GunMesh"); - var gltf = new MonoGltf(mesh); - - return [.. gltf.Write()]; - } } diff --git a/packages/game/Models/MonoGltf.cs b/packages/game/Models/MonoGltf.cs index 42470d149..e72f5fb30 100644 --- a/packages/game/Models/MonoGltf.cs +++ b/packages/game/Models/MonoGltf.cs @@ -18,247 +18,269 @@ using SharpGLTF.Scenes; namespace BlitzKit.Game.Models; using ConfiguredMeshBuilder = MeshBuilder< - VertexPositionNormalTangent, - VertexColorXTextureX, - VertexEmpty + VertexPositionNormalTangent, + VertexColorXTextureX, + VertexEmpty >; public class MonoGltf { - readonly SceneBuilder Scene; + readonly SceneBuilder Scene; - public MonoGltf(UStaticMesh staticMesh) - { - Scene = new(staticMesh.Name); - ExporterOptions options = new() { TextureFormat = ETextureFormat.Jpeg }; - staticMesh.TryConvert(out var convertedMesh); - var lod = - convertedMesh.LODs.Find(lod => !lod.SkipLod) ?? throw new Exception("Failed to find lod0"); // first non-skippable lod is the highest quality - - MaterialBuilder emptyMaterial = new("empty_material"); - Dictionary namedMaterials = []; - - int sectionIndex = 0; - foreach (var section in lod.Sections.Value) + public MonoGltf(UStaticMesh staticMesh) { - ConfiguredMeshBuilder meshBuilder = new($"{staticMesh.Name}_section_{sectionIndex}"); - MaterialBuilder materialBuilder; + Scene = new(staticMesh.Name); + ExporterOptions options = new() { TextureFormat = ETextureFormat.Jpeg }; + staticMesh.TryConvert(out var convertedMesh); + var lod = + convertedMesh.LODs.Find(lod => !lod.SkipLod) + ?? throw new Exception("Failed to find lod0"); - if (section.Material == null || section.MaterialName == null) - { - materialBuilder = emptyMaterial; - } - else if (namedMaterials.TryGetValue(section.MaterialName, out MaterialBuilder? value)) - { - materialBuilder = value; - } - else - { - var materialInterface = section.Material.Load()!; - MaterialData materialData = new() { Textures = [], Parameters = new() }; - materialInterface.GetParams(materialData.Parameters, options.MaterialFormat); - materialBuilder = new(section.MaterialName); + MaterialBuilder emptyMaterial = new("empty_material"); + Dictionary namedMaterials = []; - foreach (var parameterTexture in materialData.Parameters.Textures) + int sectionIndex = 0; + foreach (var section in lod.Sections.Value) { - var texture = parameterTexture.Value; + ConfiguredMeshBuilder meshBuilder = new($"{staticMesh.Name}_section_{sectionIndex}"); + MaterialBuilder materialBuilder; - if ( - texture is not UTexture2D texture2d - || texture2d.Decode(options.Platform) is not { } bitmap - ) - return; - - try - { - switch (parameterTexture.Key) + if (section.Material == null || section.MaterialName == null) { - case "BaseColor": - { - var encoded = bitmap.Encode(options.TextureFormat, 100); - var bytes = encoded.ToArray(); - materialBuilder.WithBaseColor(bytes); + materialBuilder = emptyMaterial; + } + else if (namedMaterials.TryGetValue(section.MaterialName, out MaterialBuilder? value)) + { + materialBuilder = value; + } + else + { + var materialInterface = section.Material.Load()!; + MaterialData materialData = new() { Textures = [], Parameters = new() }; + materialInterface.GetParams(materialData.Parameters, options.MaterialFormat); + materialBuilder = new(section.MaterialName); - break; - } - - case "RM": - { - var encoded = bitmap.Encode(options.TextureFormat, 100); - - for (int y = 0; y < bitmap.Height; y++) + foreach (var parameterTexture in materialData.Parameters.Textures) { - for (int x = 0; x < bitmap.Width; x++) - { - var index = y * bitmap.Width + x; - var color = bitmap.Pixels[index]; - bitmap.Pixels[index] = new(color.Blue, color.Red, color.Green, color.Alpha); - } + var texture = parameterTexture.Value; + + if ( + texture is not UTexture2D texture2d + || texture2d.Decode(options.Platform) is not { } bitmap + ) + return; + + try + { + switch (parameterTexture.Key) + { + case "BaseColor": + { + var encoded = bitmap.Encode( + options.TextureFormat, + false, + out var _ + ); + var bytes = encoded.ToArray(); + materialBuilder.WithBaseColor(bytes); + + break; + } + + case "RM": + { + var encoded = bitmap.Encode( + options.TextureFormat, + false, + out var _ + ); + + // for (int y = 0; y < bitmap.Height; y++) + // { + // for (int x = 0; x < bitmap.Width; x++) + // { + // var index = y * bitmap.Width + x; + // var color = bitmap.Pixels[index]; + // bitmap.Pixels[index] = new( + // color.Blue, + // color.Red, + // color.Green, + // color.Alpha + // ); + // } + // } + + var bytes = encoded.ToArray(); + materialBuilder.WithBaseColor(bytes); + + break; + } + + case "PM_SpecularMasks": + // materialBuilder.WithSpecularGlossiness(textureBytes); + break; + + case "PM_Diffuse": + // materialBuilder.WithDiffuse(textureBytes); + break; + + case "T_R90_IS_4_MISC": + case "T_R90_IS_4_MASK": + case "Misc": + case "Mask": + break; + + default: + throw new Exception( + $"Unsupported texture type: {parameterTexture.Key}" + ); + } + } + catch + { + Console.WriteLine($"Failed to load texture: {parameterTexture.Key}"); + } + } + } + + var primitive = meshBuilder.UsePrimitive(materialBuilder); + + for (int faceIndex = 0; faceIndex < section.NumFaces; faceIndex++) + { + var wedgeIndex = new int[3]; + for (var k = 0; k < wedgeIndex.Length; k++) + { + wedgeIndex[k] = (int)lod.Indices.Value[section.FirstIndex + faceIndex * 3 + k]; } - var bytes = encoded.ToArray(); - materialBuilder.WithBaseColor(bytes); + var vertex1 = lod.Verts[wedgeIndex[0]]; + var vertex2 = lod.Verts[wedgeIndex[1]]; + var vertex3 = lod.Verts[wedgeIndex[2]]; - break; - } + var (v1, v2, v3) = PrepareTris(vertex1, vertex2, vertex3); + var (c1, c2, c3) = PrepareUVsAndTexCoords( + lod, + vertex1, + vertex2, + vertex3, + wedgeIndex + ); - case "PM_SpecularMasks": - // materialBuilder.WithSpecularGlossiness(textureBytes); - break; - - case "PM_Diffuse": - // materialBuilder.WithDiffuse(textureBytes); - break; - - case "T_R90_IS_4_MISC": - case "T_R90_IS_4_MASK": - case "Misc": - case "Mask": - break; - - default: - throw new Exception($"Unsupported texture type: {parameterTexture.Key}"); + primitive.AddTriangle((v1, c1), (v2, c2), (v3, c3)); } - } - catch - { - PrettyLog.Warn($"Failed to load texture: {parameterTexture.Key}"); - } + + Scene.AddRigidMesh(meshBuilder, Matrix4x4.Identity); } - } - - var primitive = meshBuilder.UsePrimitive(materialBuilder); - - for (int faceIndex = 0; faceIndex < section.NumFaces; faceIndex++) - { - var wedgeIndex = new int[3]; - for (var k = 0; k < wedgeIndex.Length; k++) - { - wedgeIndex[k] = lod.Indices.Value[section.FirstIndex + faceIndex * 3 + k]; - } - - var vertex1 = lod.Verts[wedgeIndex[0]]; - var vertex2 = lod.Verts[wedgeIndex[1]]; - var vertex3 = lod.Verts[wedgeIndex[2]]; - - var (v1, v2, v3) = PrepareTris(vertex1, vertex2, vertex3); - var (c1, c2, c3) = PrepareUVsAndTexCoords(lod, vertex1, vertex2, vertex3, wedgeIndex); - - primitive.AddTriangle((v1, c1), (v2, c2), (v3, c3)); - } - - Scene.AddRigidMesh(meshBuilder, Matrix4x4.Identity); } - } - public ArraySegment Write() - { - return Scene.ToGltf2().WriteGLB(); - } - - private static ( - VertexPositionNormalTangent, - VertexPositionNormalTangent, - VertexPositionNormalTangent - ) PrepareTris(CMeshVertex vert1, CMeshVertex vert2, CMeshVertex vert3) - { - VertexPositionNormalTangent v1 = new( - SwapYZ(vert1.Position * 0.01f), - SwapYZAndNormalize((FVector)vert1.Normal), - SwapYZAndNormalize((Vector4)vert1.Tangent) - ); - VertexPositionNormalTangent v2 = new( - SwapYZ(vert2.Position * 0.01f), - SwapYZAndNormalize((FVector)vert2.Normal), - SwapYZAndNormalize((Vector4)vert2.Tangent) - ); - VertexPositionNormalTangent v3 = new( - SwapYZ(vert3.Position * 0.01f), - SwapYZAndNormalize((FVector)vert3.Normal), - SwapYZAndNormalize((Vector4)vert3.Tangent) - ); - - return (v1, v2, v3); - } - - public static FVector SwapYZ(FVector vec) - { - var res = new FVector(vec.X, vec.Z, vec.Y); - return res; - } - - public static FVector SwapYZAndNormalize(FVector vec) - { - var res = SwapYZ(vec); - res.Normalize(); - return res; - } - - public static Vector4 SwapYZAndNormalize(Vector4 vec) - { - return Vector4.Normalize(new Vector4(vec.X, vec.Z, vec.Y, vec.W)); - } - - public static ( - VertexColorXTextureX, - VertexColorXTextureX, - VertexColorXTextureX - ) PrepareUVsAndTexCoords( - CBaseMeshLod lod, - CMeshVertex vert1, - CMeshVertex vert2, - CMeshVertex vert3, - int[] indices - ) - { - return PrepareUVsAndTexCoords( - lod.VertexColors ?? new FColor[lod.NumVerts], - vert1, - vert2, - vert3, - lod.ExtraUV.Value, - indices - ); - } - - public static ( - VertexColorXTextureX, - VertexColorXTextureX, - VertexColorXTextureX - ) PrepareUVsAndTexCoords( - FColor[] colors, - CMeshVertex vert1, - CMeshVertex vert2, - CMeshVertex vert3, - FMeshUVFloat[][] uvs, - int[] indices - ) - { - var (uvs1, uvs2, uvs3) = PrepareUVs(vert1, vert2, vert3, uvs, indices); - var c1 = new VertexColorXTextureX((Vector4)colors[indices[0]] / 255, uvs1); - var c2 = new VertexColorXTextureX((Vector4)colors[indices[1]] / 255, uvs2); - var c3 = new VertexColorXTextureX((Vector4)colors[indices[2]] / 255, uvs3); - return (c1, c2, c3); - } - - private static (List, List, List) PrepareUVs( - CMeshVertex vert1, - CMeshVertex vert2, - CMeshVertex vert3, - FMeshUVFloat[][] uvs, - int[] indices - ) - { - var uvs1 = new List() { (Vector2)vert1.UV }; - var uvs2 = new List() { (Vector2)vert2.UV }; - var uvs3 = new List() { (Vector2)vert3.UV }; - foreach (var uv in uvs) + public ArraySegment Write() { - uvs1.Add((Vector2)uv[indices[0]]); - uvs2.Add((Vector2)uv[indices[1]]); - uvs3.Add((Vector2)uv[indices[2]]); + return Scene.ToGltf2().WriteGLB(); } - return (uvs1, uvs2, uvs3); - } + private static ( + VertexPositionNormalTangent, + VertexPositionNormalTangent, + VertexPositionNormalTangent + ) PrepareTris(CMeshVertex vert1, CMeshVertex vert2, CMeshVertex vert3) + { + VertexPositionNormalTangent v1 = new( + SwapYZ(vert1.Position * 0.01f), + SwapYZAndNormalize((FVector)vert1.Normal), + SwapYZAndNormalize((Vector4)vert1.Tangent) + ); + VertexPositionNormalTangent v2 = new( + SwapYZ(vert2.Position * 0.01f), + SwapYZAndNormalize((FVector)vert2.Normal), + SwapYZAndNormalize((Vector4)vert2.Tangent) + ); + VertexPositionNormalTangent v3 = new( + SwapYZ(vert3.Position * 0.01f), + SwapYZAndNormalize((FVector)vert3.Normal), + SwapYZAndNormalize((Vector4)vert3.Tangent) + ); + + return (v1, v2, v3); + } + + public static FVector SwapYZ(FVector vec) + { + var res = new FVector(vec.X, vec.Z, vec.Y); + return res; + } + + public static FVector SwapYZAndNormalize(FVector vec) + { + var res = SwapYZ(vec); + res.Normalize(); + return res; + } + + public static Vector4 SwapYZAndNormalize(Vector4 vec) + { + return Vector4.Normalize(new Vector4(vec.X, vec.Z, vec.Y, vec.W)); + } + + public static ( + VertexColorXTextureX, + VertexColorXTextureX, + VertexColorXTextureX + ) PrepareUVsAndTexCoords( + CBaseMeshLod lod, + CMeshVertex vert1, + CMeshVertex vert2, + CMeshVertex vert3, + int[] indices + ) + { + return PrepareUVsAndTexCoords( + lod.VertexColors ?? new FColor[lod.NumVerts], + vert1, + vert2, + vert3, + lod.ExtraUV.Value, + indices + ); + } + + public static ( + VertexColorXTextureX, + VertexColorXTextureX, + VertexColorXTextureX + ) PrepareUVsAndTexCoords( + FColor[] colors, + CMeshVertex vert1, + CMeshVertex vert2, + CMeshVertex vert3, + FMeshUVFloat[][] uvs, + int[] indices + ) + { + var (uvs1, uvs2, uvs3) = PrepareUVs(vert1, vert2, vert3, uvs, indices); + var c1 = new VertexColorXTextureX((Vector4)colors[indices[0]] / 255, uvs1); + var c2 = new VertexColorXTextureX((Vector4)colors[indices[1]] / 255, uvs2); + var c3 = new VertexColorXTextureX((Vector4)colors[indices[2]] / 255, uvs3); + return (c1, c2, c3); + } + + private static (List, List, List) PrepareUVs( + CMeshVertex vert1, + CMeshVertex vert2, + CMeshVertex vert3, + FMeshUVFloat[][] uvs, + int[] indices + ) + { + var uvs1 = new List() { (Vector2)vert1.UV }; + var uvs2 = new List() { (Vector2)vert2.UV }; + var uvs3 = new List() { (Vector2)vert3.UV }; + foreach (var uv in uvs) + { + uvs1.Add((Vector2)uv[indices[0]]); + uvs2.Add((Vector2)uv[indices[1]]); + uvs3.Add((Vector2)uv[indices[2]]); + } + + return (uvs1, uvs2, uvs3); + } } diff --git a/packages/website-ue/src/pages/[models]/tanks/[id]/[part].glb.ts b/packages/website-ue/src/pages/[models]/tanks/[id]/[part].glb.ts index a19dd53a0..78c9a2892 100644 --- a/packages/website-ue/src/pages/[models]/tanks/[id]/[part].glb.ts +++ b/packages/website-ue/src/pages/[models]/tanks/[id]/[part].glb.ts @@ -36,6 +36,7 @@ export async function GET({ const assetDiscovery = item.BlitzStaticAssetsDiscovery(); const tag = assertDiscoveryTag(assetDiscovery); const part = game!.tankPart(tag, params.part); + const array = new Uint8Array(part); - return Response.json(part.length); + return new Response(array); }