diff --git a/.csharpierrc b/.csharpierrc new file mode 100644 index 000000000..c7399784e --- /dev/null +++ b/.csharpierrc @@ -0,0 +1 @@ +indentSize: 2 diff --git a/cspell.json b/cspell.json index 1b67c599d..76588bab0 100644 --- a/cspell.json +++ b/cspell.json @@ -9,7 +9,8 @@ "webp", "hocs", "cooldown", - "Lods" + "Lods", + "Skia" ], "userWords": [ "WOTB", diff --git a/packages/game/JSExport/GameInterface.cs b/packages/game/JSExport/GameInterface.cs index 6ba4df9ba..6186e6b8a 100644 --- a/packages/game/JSExport/GameInterface.cs +++ b/packages/game/JSExport/GameInterface.cs @@ -12,6 +12,7 @@ using CUE4Parse.UE4.Assets.Objects; using CUE4Parse.UE4.Objects.Engine; using CUE4Parse.UE4.Objects.UObject; using Microsoft.JavaScript.NodeApi; +using SharpGLTF.Materials; using SkiaSharp; namespace BlitzKit.Game.JSExport; @@ -19,134 +20,129 @@ namespace BlitzKit.Game.JSExport; [JSExport] public partial class GameInterface { - private readonly BlitzFileProvider provider; + private readonly BlitzFileProvider provider; - public HashSet files; + public HashSet files; - public GameInterface(string directory, string map, string temp) + 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) { - provider = new(directory, map, temp); + if (!path.StartsWith(flagsBase)) + { + continue; + } - files = [.. provider.Files.Keys]; - } + var fileName = Path.GetFileNameWithoutExtension(path); + var match = FlagNameRegex().Match(fileName); - 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) + if ( + match.Success + && match.Groups[1].Value.Equals(nation, StringComparison.CurrentCultureIgnoreCase) + ) + { + if (provider.TryLoadPackageObject($"{path}.{fileName}", out var uTexture)) { - if (!path.StartsWith(flagsBase)) - { - continue; - } + CTexture? cTexture = uTexture.Decode(ETexturePlatform.DesktopMobile); - var fileName = Path.GetFileNameWithoutExtension(path); - var match = FlagNameRegex().Match(fileName); + if (cTexture is null) + { + Console.WriteLine($"No flag found for {nation}"); + return []; + } - 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); + SKBitmap bitmap = cTexture.ToSkBitmap(); + SKData data = bitmap.Encode(SKEncodedImageFormat.Webp, 80); - 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 []; - } + return data.ToArray(); } Console.WriteLine($"No flag found for {nation}"); return []; + } } - public byte[] ConsumableIcon(string tag) + 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 MappedGltf TankPart(string tag, string part) + { + var pda = provider.Discovered(tag); + UDataTable? dataTable = null; + + foreach (var (prefix, name) in tankPartPrefixes) { - var pda = provider.Discovered(tag); - var icon = pda.Get("Icon"); - return provider.Image(icon); + if (!part.StartsWith(prefix)) + { + continue; + } + + dataTable = pda.Get(name); + break; } - public byte[] EquipmentIcon(string tag) + if (dataTable == null) { - var pda = provider.Discovered(tag); - var icon = pda.Get("Icon"); - return provider.Image(icon); + throw new ArgumentException($"Unknown tank part: {part}"); } - readonly Dictionary tankPartPrefixes = new() + dataTable.TryGetDataTableRow(part, StringComparison.Ordinal, out var row); + + if (row == null) { - { "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); - - return [.. gltf.Write()]; + 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("Mesh"); + var gltf = new MonoGltf(mesh); + + return gltf.Write(); + } } diff --git a/packages/game/JSExport/MappedGltf.cs b/packages/game/JSExport/MappedGltf.cs new file mode 100644 index 000000000..fa8d81178 --- /dev/null +++ b/packages/game/JSExport/MappedGltf.cs @@ -0,0 +1,17 @@ +using Microsoft.JavaScript.NodeApi; +using SharpGLTF.Materials; + +namespace BlitzKit.Game.JSExport; + +[JSExport] +public readonly struct MappedGltf +{ + public string Json { get; } + public Dictionary> TextureMap { get; } + + public MappedGltf(string json, Dictionary> textureMap) + { + Json = json; + TextureMap = textureMap; + } +} diff --git a/packages/game/Models/MonoGltf.cs b/packages/game/Models/MonoGltf.cs index b209b3055..5b23032f1 100644 --- a/packages/game/Models/MonoGltf.cs +++ b/packages/game/Models/MonoGltf.cs @@ -1,4 +1,6 @@ using System.Numerics; +using System.Text; +using BlitzKit.Game.JSExport; using CUE4Parse_Conversion; using CUE4Parse_Conversion.Materials; using CUE4Parse_Conversion.Meshes; @@ -13,270 +15,224 @@ using CUE4Parse.UE4.Objects.Meshes; using SharpGLTF.Geometry; using SharpGLTF.Geometry.VertexTypes; using SharpGLTF.Materials; +using SharpGLTF.Memory; using SharpGLTF.Scenes; +using SharpGLTF.Schema2; +using SharpGLTF.Validation; namespace BlitzKit.Game.Models; using ConfiguredMeshBuilder = MeshBuilder< - VertexPositionNormalTangent, - VertexColorXTextureX, - VertexEmpty + VertexPositionNormalTangent, + VertexColorXTextureX, + VertexEmpty >; public class MonoGltf { - readonly SceneBuilder Scene; + readonly SceneBuilder scene; + readonly Dictionary> textureMap = []; - public MonoGltf(UStaticMesh staticMesh) + readonly Dictionary knownChannels = new() + { + { "BaseColor", KnownChannel.BaseColor }, + { "Normal", KnownChannel.Normal }, + { "RMAO", KnownChannel.MetallicRoughness }, + // { "CDE", ?? }, + }; + + public MonoGltf(UStaticMesh staticMesh) + { + scene = new(staticMesh.Name); + var options = new ExporterOptions() { TextureFormat = ETextureFormat.Jpeg }; + + staticMesh.TryConvert(out var convertedMesh); + var lod = + convertedMesh.LODs.Find(lod => !lod.SkipLod) ?? throw new Exception("Failed to find lod0"); + + MaterialBuilder emptyMaterial = new("empty_material"); + Dictionary materialMap = []; + + int sectionIndex = 0; + foreach (var section in lod.Sections.Value) { - 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"); + ConfiguredMeshBuilder meshBuilder = new($"{staticMesh.Name}_section_{sectionIndex}"); + MaterialBuilder materialBuilder; - MaterialBuilder emptyMaterial = new("empty_material"); - Dictionary namedMaterials = []; + if (section.Material == null || section.MaterialName == null) + { + materialBuilder = emptyMaterial; + } + else if (materialMap.TryGetValue(section.MaterialName, out MaterialBuilder? value)) + { + materialBuilder = value; + } + else + { + var materialInterface = section.Material.Load()!; + var materialData = new MaterialData() { Parameters = new() }; - Console.WriteLine(lod.Sections.Value.Length); + materialInterface.GetParams(materialData.Parameters, options.MaterialFormat); + materialBuilder = new(section.MaterialName); - int sectionIndex = 0; - foreach (var section in lod.Sections.Value) + var textures = new Dictionary(); + + materialMap.Add(section.MaterialName, materialBuilder); + textureMap.Add(section.MaterialName, textures); + + foreach (var parameterTexture in materialData.Parameters.Textures) { - Console.WriteLine(section.MaterialName); + if (knownChannels.TryGetValue(parameterTexture.Key, out var channel)) + { + var texture = parameterTexture.Value; + var name = Path.GetFileNameWithoutExtension(texture.GetPathName()); + var path = $"/textures/{name}"; + // var path = texture.GetPathName(); - ConfiguredMeshBuilder meshBuilder = new($"{staticMesh.Name}_section_{sectionIndex}"); - MaterialBuilder materialBuilder; - - 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); - - foreach (var parameterTexture in materialData.Parameters.Textures) - { - var texture = parameterTexture.Value; - - if ( - texture is not UTexture2D texture2d - || texture2d.Decode(options.Platform) is not { } bitmap - ) - { - Console.WriteLine($"Failed to decode texture: {texture?.Name}"); - continue; - } - - switch (parameterTexture.Key) - { - case "BaseColor": - { - var encoded = bitmap.Encode(options.TextureFormat, false, out var _); - var bytes = encoded.ToArray(); - materialBuilder.WithBaseColor(bytes); - - break; - } - - case "Normal": - { - var encoded = bitmap.Encode(options.TextureFormat, false, out var _); - var bytes = encoded.ToArray(); - materialBuilder.WithNormal(bytes); - - break; - } - - case "RMAO": - { - 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 "CDE": - { - // idk wtf this is - break; - } - - default: - { - Console.WriteLine($"Unsupported texture type: {parameterTexture.Key}"); - break; - } - } - } - } - - var primitive = meshBuilder.UsePrimitive(materialBuilder); - - Console.WriteLine($"faces: {section.NumFaces}"); - - 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 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); + textures.Add(channel, path); + } + else + { + // Console.WriteLine($"Unsupported texture type: {parameterTexture.Key}"); + } } - } + } - public ArraySegment Write() - { - return Scene.ToGltf2().WriteGLB(); - } + var primitive = meshBuilder.UsePrimitive(materialBuilder); - 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) + for (int faceIndex = 0; faceIndex < section.NumFaces; faceIndex++) + { + var wedgeIndex = new int[3]; + for (var k = 0; k < wedgeIndex.Length; k++) { - uvs1.Add((Vector2)uv[indices[0]]); - uvs2.Add((Vector2)uv[indices[1]]); - uvs3.Add((Vector2)uv[indices[2]]); + wedgeIndex[k] = (int)lod.Indices.Value[section.FirstIndex + faceIndex * 3 + k]; } - return (uvs1, uvs2, uvs3); + 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 MappedGltf Write() + { + var model = scene.ToGltf2(); + var json = model.GetJSON(false); + + return new MappedGltf(json, textureMap); + } + + 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 78c9a2892..d1df7b7e1 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 @@ -35,8 +35,8 @@ export async function GET({ const item = api.metadata.item(params.id); const assetDiscovery = item.BlitzStaticAssetsDiscovery(); const tag = assertDiscoveryTag(assetDiscovery); - const part = game!.tankPart(tag, params.part); - const array = new Uint8Array(part); + const asd = game!.tankPart(tag, params.part); + // const array = new Uint8Array(part); - return new Response(array); + return Response.json(asd); } diff --git a/stub/stupid.png b/stub/stupid.png new file mode 100644 index 000000000..3b9d7830c Binary files /dev/null and b/stub/stupid.png differ