From 5baf3f324ff6cf49e83697d0f50f80a478667733 Mon Sep 17 00:00:00 2001 From: bird_egop Date: Sat, 6 Jun 2026 21:26:56 +0300 Subject: [PATCH] material initial and pose fix --- MaterialLib/MaterialFile.cs | 6 +- MaterialLib/MaterialParser.cs | 2 +- NResUI/ImGuiUI/NResExplorerPanel.cs | 2 +- NResUI/Rendering/Viewport/Meshes/GpuMesh.cs | 6 +- .../Viewport/Meshes/PrimitiveMeshes.cs | 268 ++++------------ .../Viewport/Msh/MshRestPoseBuilder.cs | 5 +- .../Viewport/Msh/MshViewportLoader.cs | 86 +++-- .../Viewport/Msh/WeaMaterialLibrary.cs | 293 ++++++++++++++++++ .../Viewport/OpenGL/ShaderProgram.cs | 5 + .../Viewport/OpenGL/ViewportTextureLoader.cs | 35 +++ NResUI/Rendering/Viewport/ViewportMaterial.cs | 16 + NResUI/Rendering/Viewport/ViewportPiece.cs | 21 +- NResUI/Rendering/Viewport/ViewportRenderer.cs | 39 ++- 13 files changed, 536 insertions(+), 248 deletions(-) create mode 100644 NResUI/Rendering/Viewport/Msh/WeaMaterialLibrary.cs create mode 100644 NResUI/Rendering/Viewport/OpenGL/ViewportTextureLoader.cs create mode 100644 NResUI/Rendering/Viewport/ViewportMaterial.cs diff --git a/MaterialLib/MaterialFile.cs b/MaterialLib/MaterialFile.cs index 04f167a..98adf83 100644 --- a/MaterialLib/MaterialFile.cs +++ b/MaterialLib/MaterialFile.cs @@ -15,11 +15,11 @@ namespace MaterialLib; /// Анимации материала в порядке чтения из файла. public record MaterialFile( string FileName, - int Version, + uint Version, int Magic1, - int MaterialRenderingType, + uint MaterialRenderingType, bool SupportsBumpMapping, - int IsParticleEffect, + uint IsParticleEffect, BlendMode SourceBlendMode, BlendMode DestBlendMode, float GlobalAlphaMultiplier, diff --git a/MaterialLib/MaterialParser.cs b/MaterialLib/MaterialParser.cs index bd1a126..449fb9e 100644 --- a/MaterialLib/MaterialParser.cs +++ b/MaterialLib/MaterialParser.cs @@ -5,7 +5,7 @@ namespace MaterialLib; public static class MaterialParser { - public static MaterialFile ReadFromStream(Stream fs, string fileName, int elementCount, int magic1) + public static MaterialFile ReadFromStream(Stream fs, string fileName, uint elementCount, int magic1) { var materialRenderingType = elementCount >> 2 & 0xf; var supportsBumpMapping = (elementCount & 2) != 0; diff --git a/NResUI/ImGuiUI/NResExplorerPanel.cs b/NResUI/ImGuiUI/NResExplorerPanel.cs index d2629bb..eefb1e3 100644 --- a/NResUI/ImGuiUI/NResExplorerPanel.cs +++ b/NResUI/ImGuiUI/NResExplorerPanel.cs @@ -268,7 +268,7 @@ public class NResExplorerPanel : IImGuiPanel try { - var parseResult = MaterialLib.MaterialParser.ReadFromStream(ms, file.FileName, (int)file.ElementCount, file.Magic1); + var parseResult = MaterialLib.MaterialParser.ReadFromStream(ms, file.FileName, file.ElementCount, file.Magic1); _materialViewModel.SetParseResult(parseResult, Path.Combine(_viewModel.Path!, file.FileName)); Console.WriteLine("Read Material from context menu"); } diff --git a/NResUI/Rendering/Viewport/Meshes/GpuMesh.cs b/NResUI/Rendering/Viewport/Meshes/GpuMesh.cs index be68e25..eeda762 100644 --- a/NResUI/Rendering/Viewport/Meshes/GpuMesh.cs +++ b/NResUI/Rendering/Viewport/Meshes/GpuMesh.cs @@ -1,4 +1,5 @@ using Silk.NET.OpenGL; +using NResUI.Rendering.Viewport; namespace NResUI.Rendering.Viewport.Meshes; @@ -11,6 +12,7 @@ public sealed class GpuMesh public uint ElementBufferObject { get; } public uint IndexCount { get; } public PrimitiveType PrimitiveType { get; } + public ViewportMaterial Material { get; } public GpuMesh( GL gl, @@ -18,7 +20,8 @@ public sealed class GpuMesh uint vertexBufferObject, uint elementBufferObject, uint indexCount, - PrimitiveType primitiveType = PrimitiveType.Triangles) + PrimitiveType primitiveType = PrimitiveType.Triangles, + ViewportMaterial? material = null) { _gl = gl; VertexArrayObject = vertexArrayObject; @@ -26,6 +29,7 @@ public sealed class GpuMesh ElementBufferObject = elementBufferObject; IndexCount = indexCount; PrimitiveType = primitiveType; + Material = material ?? ViewportMaterial.Untextured; } public unsafe void Draw() diff --git a/NResUI/Rendering/Viewport/Meshes/PrimitiveMeshes.cs b/NResUI/Rendering/Viewport/Meshes/PrimitiveMeshes.cs index e5e529b..7f562f8 100644 --- a/NResUI/Rendering/Viewport/Meshes/PrimitiveMeshes.cs +++ b/NResUI/Rendering/Viewport/Meshes/PrimitiveMeshes.cs @@ -1,100 +1,42 @@ using System.Numerics; using Silk.NET.OpenGL; +using NResUI.Rendering.Viewport; namespace NResUI.Rendering.Viewport.Meshes; public static unsafe class PrimitiveMeshes { + public const int FloatsPerVertex = 11; + public static GpuMesh CreateCube(GL gl) { var vertices = new List(); var indices = new List(); - AddCubeFace( - vertices, indices, - new Vector3(-1, -1, 1), - new Vector3( 1, -1, 1), - new Vector3( 1, 1, 1), - new Vector3(-1, 1, 1), - new Vector3(0, 0, 1), - new Vector3(1, 0, 0)); - - AddCubeFace( - vertices, indices, - new Vector3( 1, -1, -1), - new Vector3(-1, -1, -1), - new Vector3(-1, 1, -1), - new Vector3( 1, 1, -1), - new Vector3(0, 0, -1), - new Vector3(0, 1, 0)); - - AddCubeFace( - vertices, indices, - new Vector3(-1, -1, -1), - new Vector3(-1, -1, 1), - new Vector3(-1, 1, 1), - new Vector3(-1, 1, -1), - new Vector3(-1, 0, 0), - new Vector3(0, 0, 1)); - - AddCubeFace( - vertices, indices, - new Vector3(1, -1, 1), - new Vector3(1, -1, -1), - new Vector3(1, 1, -1), - new Vector3(1, 1, 1), - new Vector3(1, 0, 0), - new Vector3(1, 1, 0)); - - AddCubeFace( - vertices, indices, - new Vector3(-1, 1, 1), - new Vector3( 1, 1, 1), - new Vector3( 1, 1, -1), - new Vector3(-1, 1, -1), - new Vector3(0, 1, 0), - new Vector3(1, 0, 1)); - - AddCubeFace( - vertices, indices, - new Vector3(-1, -1, -1), - new Vector3( 1, -1, -1), - new Vector3( 1, -1, 1), - new Vector3(-1, -1, 1), - new Vector3(0, -1, 0), - new Vector3(0, 1, 1)); + AddCubeFace(vertices, indices, new(-1, -1, 1), new( 1, -1, 1), new( 1, 1, 1), new(-1, 1, 1), new(0, 0, 1), new(1, 0, 0)); + AddCubeFace(vertices, indices, new( 1, -1, -1), new(-1, -1, -1), new(-1, 1, -1), new( 1, 1, -1), new(0, 0, -1), new(0, 1, 0)); + AddCubeFace(vertices, indices, new(-1, -1, -1), new(-1, -1, 1), new(-1, 1, 1), new(-1, 1, -1), new(-1, 0, 0), new(0, 0, 1)); + AddCubeFace(vertices, indices, new( 1, -1, 1), new( 1, -1, -1), new( 1, 1, -1), new( 1, 1, 1), new(1, 0, 0), new(1, 1, 0)); + AddCubeFace(vertices, indices, new(-1, 1, 1), new( 1, 1, 1), new( 1, 1, -1), new(-1, 1, -1), new(0, 1, 0), new(1, 0, 1)); + AddCubeFace(vertices, indices, new(-1, -1, -1), new( 1, -1, -1), new( 1, -1, 1), new(-1, -1, 1), new(0, -1, 0), new(0, 1, 1)); return CreateColoredIndexedMesh(gl, vertices, indices, PrimitiveType.Triangles); } - public static GpuMesh CreateWorldGrid( - GL gl, - int halfExtent = 20, - int minorStep = 1, - int majorStep = 5) + public static GpuMesh CreateWorldGrid(GL gl, int halfExtent = 20, int minorStep = 1, int majorStep = 5) { - if (halfExtent <= 0) - throw new ArgumentOutOfRangeException(nameof(halfExtent)); - - if (minorStep <= 0) - throw new ArgumentOutOfRangeException(nameof(minorStep)); - - if (majorStep <= 0) - throw new ArgumentOutOfRangeException(nameof(majorStep)); + if (halfExtent <= 0) throw new ArgumentOutOfRangeException(nameof(halfExtent)); + if (minorStep <= 0) throw new ArgumentOutOfRangeException(nameof(minorStep)); + if (majorStep <= 0) throw new ArgumentOutOfRangeException(nameof(majorStep)); var vertices = new List(); var indices = new List(); - void AddLine( - float x0, float y0, float z0, - float x1, float y1, float z1, - float r, float g, float b) + void AddLine(float x0, float y0, float z0, float x1, float y1, float z1, float r, float g, float b) { - var startIndex = (uint)(vertices.Count / 9); - - AddVertex(vertices, x0, y0, z0, r, g, b, 0, 1, 0); - AddVertex(vertices, x1, y1, z1, r, g, b, 0, 1, 0); - + var startIndex = (uint)(vertices.Count / FloatsPerVertex); + AddVertex(vertices, x0, y0, z0, r, g, b); + AddVertex(vertices, x1, y1, z1, r, g, b); indices.Add(startIndex); indices.Add(startIndex + 1); } @@ -105,135 +47,54 @@ public static unsafe class PrimitiveMeshes { var isAxis = i == 0; var isMajor = i % majorStep == 0; - var brightness = isMajor ? 0.38f : 0.24f; var r = brightness; var g = brightness; var b = brightness; - // Lines parallel to X. The Z axis itself is blue. - if (isAxis) - { - AddLine(-halfExtent, y, i, halfExtent, y, i, 0.25f, 0.45f, 1.0f); - } - else - { - AddLine(-halfExtent, y, i, halfExtent, y, i, r, g, b); - } - - // Lines parallel to Z. The X axis itself is red. - if (isAxis) - { - AddLine(i, y, -halfExtent, i, y, halfExtent, 1.0f, 0.25f, 0.25f); - } - else - { - AddLine(i, y, -halfExtent, i, y, halfExtent, r, g, b); - } + AddLine(-halfExtent, y, i, halfExtent, y, i, isAxis ? 0.25f : r, isAxis ? 0.45f : g, isAxis ? 1.0f : b); + AddLine(i, y, -halfExtent, i, y, halfExtent, isAxis ? 1.0f : r, isAxis ? 0.25f : g, isAxis ? 0.25f : b); } return CreateIndexedMesh(gl, vertices.ToArray(), indices.ToArray(), PrimitiveType.Lines); } - private static void AddCubeFace( - List vertices, - List indices, - Vector3 a, - Vector3 b, - Vector3 c, - Vector3 d, - Vector3 normal, - Vector3 color) - { - var start = (uint)(vertices.Count / 9); - - AddVertex(vertices, a.X, a.Y, a.Z, color.X, color.Y, color.Z, normal.X, normal.Y, normal.Z); - AddVertex(vertices, b.X, b.Y, b.Z, color.X, color.Y, color.Z, normal.X, normal.Y, normal.Z); - AddVertex(vertices, c.X, c.Y, c.Z, color.X, color.Y, color.Z, normal.X, normal.Y, normal.Z); - AddVertex(vertices, d.X, d.Y, d.Z, color.X, color.Y, color.Z, normal.X, normal.Y, normal.Z); - - indices.Add(start + 0); - indices.Add(start + 1); - indices.Add(start + 2); - - indices.Add(start + 2); - indices.Add(start + 3); - indices.Add(start + 0); - } - - public static GpuMesh CreateUnitWireBox(GL gl) { var vertices = new List(); - var indices = new List(); - var color = new Vector3(1.0f, 0.82f, 0.15f); Vector3[] corners = { - new(-1, -1, -1), - new( 1, -1, -1), - new( 1, 1, -1), - new(-1, 1, -1), - new(-1, -1, 1), - new( 1, -1, 1), - new( 1, 1, 1), - new(-1, 1, 1), + new(-1, -1, -1), new( 1, -1, -1), new( 1, 1, -1), new(-1, 1, -1), + new(-1, -1, 1), new( 1, -1, 1), new( 1, 1, 1), new(-1, 1, 1), }; foreach (var corner in corners) - { AddVertex(vertices, corner.X, corner.Y, corner.Z, color.X, color.Y, color.Z); - } - uint[] lineIndices = + uint[] indices = { 0, 1, 1, 2, 2, 3, 3, 0, 4, 5, 5, 6, 6, 7, 7, 4, 0, 4, 1, 5, 2, 6, 3, 7, }; - indices.AddRange(lineIndices); - - return CreateIndexedMesh(gl, vertices.ToArray(), indices.ToArray(), PrimitiveType.Lines); - } - - private static void AddVertex( - List vertices, - float x, float y, float z, - float r, float g, float b, - float nx = 0.0f, - float ny = 1.0f, - float nz = 0.0f) - { - vertices.Add(x); - vertices.Add(y); - vertices.Add(z); - - vertices.Add(r); - vertices.Add(g); - vertices.Add(b); - - vertices.Add(nx); - vertices.Add(ny); - vertices.Add(nz); + return CreateIndexedMesh(gl, vertices.ToArray(), indices, PrimitiveType.Lines); } public static GpuMesh CreateAxes(GL gl, float length = 1.0f) { - if (length <= 0.0f) - throw new ArgumentOutOfRangeException(nameof(length)); + if (length <= 0.0f) throw new ArgumentOutOfRangeException(nameof(length)); var vertices = new List(); var indices = new List(); void AddLine(Vector3 a, Vector3 b, Vector3 color) { - var start = (uint)(vertices.Count / 9); - + var start = (uint)(vertices.Count / FloatsPerVertex); AddVertex(vertices, a.X, a.Y, a.Z, color.X, color.Y, color.Z); AddVertex(vertices, b.X, b.Y, b.Z, color.X, color.Y, color.Z); - indices.Add(start); indices.Add(start + 1); } @@ -245,20 +106,31 @@ public static unsafe class PrimitiveMeshes return CreateIndexedMesh(gl, vertices.ToArray(), indices.ToArray(), PrimitiveType.Lines); } - public static GpuMesh CreateColoredIndexedMesh( - GL gl, - IReadOnlyList vertices, - IReadOnlyList indices, - PrimitiveType primitiveType = PrimitiveType.Triangles) + public static GpuMesh CreateColoredIndexedMesh(GL gl, IReadOnlyList vertices, IReadOnlyList indices, PrimitiveType primitiveType = PrimitiveType.Triangles, ViewportMaterial? material = null) { - return CreateIndexedMesh(gl, vertices.ToArray(), indices.ToArray(), primitiveType); + return CreateIndexedMesh(gl, vertices.ToArray(), indices.ToArray(), primitiveType, material); } - private static GpuMesh CreateIndexedMesh( - GL gl, - float[] vertices, - uint[] indices, - PrimitiveType primitiveType) + private static void AddCubeFace(List vertices, List indices, Vector3 a, Vector3 b, Vector3 c, Vector3 d, Vector3 normal, Vector3 color) + { + var start = (uint)(vertices.Count / FloatsPerVertex); + AddVertex(vertices, a.X, a.Y, a.Z, color.X, color.Y, color.Z, normal.X, normal.Y, normal.Z); + AddVertex(vertices, b.X, b.Y, b.Z, color.X, color.Y, color.Z, normal.X, normal.Y, normal.Z); + AddVertex(vertices, c.X, c.Y, c.Z, color.X, color.Y, color.Z, normal.X, normal.Y, normal.Z); + AddVertex(vertices, d.X, d.Y, d.Z, color.X, color.Y, color.Z, normal.X, normal.Y, normal.Z); + indices.Add(start + 0); indices.Add(start + 1); indices.Add(start + 2); + indices.Add(start + 2); indices.Add(start + 3); indices.Add(start + 0); + } + + private static void AddVertex(List vertices, float x, float y, float z, float r, float g, float b, float nx = 0.0f, float ny = 1.0f, float nz = 0.0f, float u = 0.0f, float v = 0.0f) + { + vertices.Add(x); vertices.Add(y); vertices.Add(z); + vertices.Add(r); vertices.Add(g); vertices.Add(b); + vertices.Add(nx); vertices.Add(ny); vertices.Add(nz); + vertices.Add(u); vertices.Add(v); + } + + private static GpuMesh CreateIndexedMesh(GL gl, float[] vertices, uint[] indices, PrimitiveType primitiveType, ViewportMaterial? material = null) { var vao = gl.GenVertexArray(); var vbo = gl.GenBuffer(); @@ -269,55 +141,31 @@ public static unsafe class PrimitiveMeshes gl.BindBuffer(BufferTargetARB.ArrayBuffer, vbo); fixed (float* vertexPtr = vertices) { - gl.BufferData( - BufferTargetARB.ArrayBuffer, - (nuint)(vertices.Length * sizeof(float)), - vertexPtr, - BufferUsageARB.StaticDraw); + gl.BufferData(BufferTargetARB.ArrayBuffer, (nuint)(vertices.Length * sizeof(float)), vertexPtr, BufferUsageARB.StaticDraw); } gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, ebo); fixed (uint* indexPtr = indices) { - gl.BufferData( - BufferTargetARB.ElementArrayBuffer, - (nuint)(indices.Length * sizeof(uint)), - indexPtr, - BufferUsageARB.StaticDraw); + gl.BufferData(BufferTargetARB.ElementArrayBuffer, (nuint)(indices.Length * sizeof(uint)), indexPtr, BufferUsageARB.StaticDraw); } - const uint stride = 9 * sizeof(float); + const uint stride = FloatsPerVertex * sizeof(float); gl.EnableVertexAttribArray(0); - gl.VertexAttribPointer( - 0, - 3, - VertexAttribPointerType.Float, - false, - stride, - null); + gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, stride, null); gl.EnableVertexAttribArray(1); - gl.VertexAttribPointer( - 1, - 3, - VertexAttribPointerType.Float, - false, - stride, - (void*)(3 * sizeof(float))); - + gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float))); + gl.EnableVertexAttribArray(2); - gl.VertexAttribPointer( - 2, - 3, - VertexAttribPointerType.Float, - false, - stride, - (void*)(6 * sizeof(float)) - ); + gl.VertexAttribPointer(2, 3, VertexAttribPointerType.Float, false, stride, (void*)(6 * sizeof(float))); + + gl.EnableVertexAttribArray(3); + gl.VertexAttribPointer(3, 2, VertexAttribPointerType.Float, false, stride, (void*)(9 * sizeof(float))); gl.BindVertexArray(0); - return new GpuMesh(gl, vao, vbo, ebo, (uint)indices.Length, primitiveType); + return new GpuMesh(gl, vao, vbo, ebo, (uint)indices.Length, primitiveType, material); } } diff --git a/NResUI/Rendering/Viewport/Msh/MshRestPoseBuilder.cs b/NResUI/Rendering/Viewport/Msh/MshRestPoseBuilder.cs index 27e0ed5..8b97065 100644 --- a/NResUI/Rendering/Viewport/Msh/MshRestPoseBuilder.cs +++ b/NResUI/Rendering/Viewport/Msh/MshRestPoseBuilder.cs @@ -143,7 +143,10 @@ public static class MshRestPoseBuilder if (q.LengthSquared() < 1e-8f) return Quaternion.Identity; - return Quaternion.Normalize(q); + // MSH/game convention is opposite to our System.Numerics/OpenGL transform convention. + // Conjugating converts the stored piece rotation into the viewport convention. + // The MSH keyframe rotation is stored as mesh-to-parent instead of parent-to-mesh, so the viewer needs the inverse. + return Quaternion.Conjugate(q); } } diff --git a/NResUI/Rendering/Viewport/Msh/MshViewportLoader.cs b/NResUI/Rendering/Viewport/Msh/MshViewportLoader.cs index 332bd1d..701bdc3 100644 --- a/NResUI/Rendering/Viewport/Msh/MshViewportLoader.cs +++ b/NResUI/Rendering/Viewport/Msh/MshViewportLoader.cs @@ -30,7 +30,7 @@ public static class MshViewportLoader } fs.Seek(0, SeekOrigin.Begin); - var pieces = LoadModelPieces(gl, fs, archive); + var pieces = LoadModelPieces(gl, fs, archive, path); if (pieces.Count == 0) return MshViewportLoadResult.Failure("MSH was parsed, but no renderable geometry pieces were found.", path); @@ -42,17 +42,19 @@ public static class MshViewportLoader } } - private static List LoadModelPieces(GL gl, FileStream fs, NResArchive archive) + private static List LoadModelPieces(GL gl, FileStream fs, NResArchive archive, string path) { var nodes = Msh0x01.ReadComponent(fs, archive); var geometry = Msh0x02.ReadComponent(fs, archive); var positions = Msh0x03.ReadComponent(fs, archive); + var uvs = TryReadUvs(fs, archive); var indices = Msh0x06.ReadComponent(fs, archive); var batches = Msh0x0D.ReadComponent(fs, archive); var animationDescriptors = Msh0x08.ReadComponent(fs, archive); var restPoses = MshRestPoseBuilder.BuildRestPose(nodes, animationDescriptors); var mshToViewportTransform = Matrix4x4.CreateRotationX(-MathF.PI * 0.5f); var names = TryReadNames(fs, archive); + var materialLibrary = WeaMaterialLibrary.TryLoadForMsh(gl, path); var pieces = new List(); @@ -71,7 +73,7 @@ public static class MshViewportLoader if (!slot.HasBatches) continue; - var meshBuildResult = BuildPieceMesh(gl, nodeIndex, slot, positions, indices, batches); + var meshBuildResult = BuildPieceMeshes(gl, nodeIndex, slot, positions, uvs, indices, batches, materialLibrary); if (meshBuildResult == null) continue; @@ -79,7 +81,7 @@ public static class MshViewportLoader pieces.Add(new ViewportPiece( id: nodeIndex, name: name, - mesh: meshBuildResult.Mesh, + meshes: meshBuildResult.Meshes, localTransform: restPose.MeshSpaceTransform * mshToViewportTransform, boundsMin: meshBuildResult.BoundsMin, boundsMax: meshBuildResult.BoundsMax, @@ -100,17 +102,18 @@ public static class MshViewportLoader return pieces; } - private static PieceMeshBuildResult? BuildPieceMesh( + private static PieceMeshBuildResult? BuildPieceMeshes( GL gl, int nodeIndex, Msh0x02.GeometrySlot slot, IReadOnlyList positions, + IReadOnlyList uvs, IReadOnlyList indices, - IReadOnlyList batches) + IReadOnlyList batches, + WeaMaterialLibrary materialLibrary) { - var vertices = new List(); - var outIndices = new List(); - var color = PickDebugColor(nodeIndex); + var meshes = new List(); + var debugColor = PickDebugColor(nodeIndex); var batchCount = 0; var triangleCount = 0; @@ -127,7 +130,12 @@ public static class MshViewportLoader if (batch.IndexStart0x06 + batch.IndexCount0x06 > indices.Count) continue; - batchCount++; + var vertices = new List(); + var outIndices = new List(); + var batchTriangleCount = 0; + + var material = materialLibrary.FindMaterial(batch.MaterialIndex) ?? ViewportMaterial.Untextured; + var vertexColor = material.HasTexture ? Vector3.One : debugColor; for (var i = 0; i + 2 < batch.IndexCount0x06; i += 3) { @@ -149,20 +157,35 @@ public static class MshViewportLoader else normal = Vector3.Normalize(normal); - AddTriangleVertex(p0, color, normal, vertices, outIndices, ref boundsMin, ref boundsMax); - AddTriangleVertex(p1, color, normal, vertices, outIndices, ref boundsMin, ref boundsMax); - AddTriangleVertex(p2, color, normal, vertices, outIndices, ref boundsMin, ref boundsMax); - triangleCount++; + AddTriangleVertex(p0, vertexColor, normal, GetUv(uvs, vertexIndex0), vertices, outIndices, ref boundsMin, ref boundsMax); + AddTriangleVertex(p1, vertexColor, normal, GetUv(uvs, vertexIndex1), vertices, outIndices, ref boundsMin, ref boundsMax); + AddTriangleVertex(p2, vertexColor, normal, GetUv(uvs, vertexIndex2), vertices, outIndices, ref boundsMin, ref boundsMax); + batchTriangleCount++; } + + if (batchTriangleCount == 0) + continue; + + batchCount++; + triangleCount += batchTriangleCount; + meshes.Add(PrimitiveMeshes.CreateColoredIndexedMesh(gl, vertices, outIndices, PrimitiveType.Triangles, material)); } - if (triangleCount == 0) + if (triangleCount == 0 || meshes.Count == 0) return null; - var mesh = PrimitiveMeshes.CreateColoredIndexedMesh(gl, vertices, outIndices, PrimitiveType.Triangles); - return new PieceMeshBuildResult(mesh, boundsMin, boundsMax, batchCount, triangleCount); + return new PieceMeshBuildResult(meshes, boundsMin, boundsMax, batchCount, triangleCount); } - + + private static Vector2 GetUv(IReadOnlyList uvs, int vertexIndex) + { + if (vertexIndex < 0 || vertexIndex >= uvs.Count) + return Vector2.Zero; + + var uv = uvs[vertexIndex]; + return new Vector2(uv.U / 1024.0f, 1.0f - uv.V / 1024.0f); + } + private static Vector3 ToNumericsVector3(Common.Vector3 position) { return new Vector3(position.X, position.Y, position.Z); @@ -172,13 +195,13 @@ public static class MshViewportLoader Vector3 position, Vector3 color, Vector3 normal, + Vector2 uv, List vertices, List indices, ref Vector3 boundsMin, - ref Vector3 boundsMax - ) + ref Vector3 boundsMax) { - var vertexIndex = (uint)(vertices.Count / 9); + var vertexIndex = (uint)(vertices.Count / PrimitiveMeshes.FloatsPerVertex); vertices.Add(position.X); vertices.Add(position.Y); @@ -189,12 +212,13 @@ public static class MshViewportLoader vertices.Add(normal.X); vertices.Add(normal.Y); vertices.Add(normal.Z); + vertices.Add(uv.X); + vertices.Add(uv.Y); indices.Add(vertexIndex); - var p = new Vector3(position.X, position.Y, position.Z); - boundsMin = Vector3.Min(boundsMin, p); - boundsMax = Vector3.Max(boundsMax, p); + boundsMin = Vector3.Min(boundsMin, position); + boundsMax = Vector3.Max(boundsMax, position); } private static bool IsValidTriangle(int vertexCount, int vertexIndex0, int vertexIndex1, int vertexIndex2) @@ -221,6 +245,18 @@ public static class MshViewportLoader } } + private static List TryReadUvs(FileStream fs, NResArchive archive) + { + try + { + return Msh0x05.ReadComponent(fs, archive); + } + catch + { + return new List(); + } + } + private static string ResolvePieceName(IReadOnlyList names, int nodeIndex) { if (nodeIndex >= 0 && nodeIndex < names.Count && !string.IsNullOrWhiteSpace(names[nodeIndex])) @@ -247,7 +283,7 @@ public static class MshViewportLoader } private sealed record PieceMeshBuildResult( - GpuMesh Mesh, + IReadOnlyList Meshes, Vector3 BoundsMin, Vector3 BoundsMax, int BatchCount, diff --git a/NResUI/Rendering/Viewport/Msh/WeaMaterialLibrary.cs b/NResUI/Rendering/Viewport/Msh/WeaMaterialLibrary.cs new file mode 100644 index 0000000..84053e5 --- /dev/null +++ b/NResUI/Rendering/Viewport/Msh/WeaMaterialLibrary.cs @@ -0,0 +1,293 @@ +using MaterialLib; +using NResLib; +using NResUI.Rendering.Viewport.OpenGL; +using Silk.NET.OpenGL; +using TexmLib; + +namespace NResUI.Rendering.Viewport.Msh; + +public sealed class WeaMaterialLibrary +{ + private readonly Dictionary _materialsById; + + private WeaMaterialLibrary(Dictionary materialsById) + { + _materialsById = materialsById; + } + + public static WeaMaterialLibrary Empty { get; } = new(new Dictionary()); + + public ViewportMaterial? FindMaterial(int id) + { + return _materialsById.TryGetValue(id, out var material) + ? material + : null; + } + + public static WeaMaterialLibrary TryLoadForMsh(GL gl, string mshPath) + { + var weaPath = FindMatchingWeaPath(mshPath); + if (!File.Exists(weaPath)) + return Empty; + + var materialRefs = ParseMaterialRefs(weaPath); + if (materialRefs.Count == 0) + return Empty; + + // TODO: Hardcoded for now + var materialLibFs = "C:\\IronStrategy\\Material.lib"; + + using var materialFs = new FileStream(materialLibFs, FileMode.Open, FileAccess.Read, FileShare.Read); + var parseResult = NResParser.ReadFile(materialFs); + if (parseResult.Archive == null) + return Empty; + + var result = new Dictionary(); + + foreach (var materialRef in materialRefs) + { + var texture = TryLoadMaterialTexture(gl, materialFs, parseResult.Archive, materialRef.Name); + result[materialRef.Id] = texture != null + ? new ViewportMaterial(materialRef.Name, texture.Value) + : new ViewportMaterial(materialRef.Name); + } + + return new WeaMaterialLibrary(result); + } + + private static string? FindMatchingWeaPath(string mshPath) + { + var directory = Path.GetDirectoryName(mshPath); + if (string.IsNullOrWhiteSpace(directory) || !Directory.Exists(directory)) + return null; + + var mshResourceName = GetExportedResourceNameWithoutExtension(mshPath); + var mshObjectStem = ExtractObjectStem(mshResourceName); + + var candidates = Directory.EnumerateFiles(directory, "*.wea", SearchOption.TopDirectoryOnly) + .Select(path => new + { + Path = path, + ResourceName = GetExportedResourceNameWithoutExtension(path), + }) + .Select(x => new + { + x.Path, + x.ResourceName, + ObjectStem = ExtractObjectStem(x.ResourceName), + }) + .ToList(); + + // Best case: exact resource stem match after type prefix normalization. + var exact = candidates.FirstOrDefault(x => string.Equals(x.ObjectStem, mshObjectStem, + StringComparison.OrdinalIgnoreCase)); + + if (exact != null) + return exact.Path; + + // Fallback: choose a WEA whose resource name shares the longest token prefix. + var best = candidates + .Select(x => new + { + x.Path, + Score = CommonPrefixTokenCount( + SplitResourceTokens(mshObjectStem), + SplitResourceTokens(x.ObjectStem)) + }) + .OrderByDescending(x => x.Score) + .FirstOrDefault(); + + return best?.Score > 0 ? best.Path : null; + } + + private static string GetExportedResourceNameWithoutExtension(string path) + { + var name = Path.GetFileNameWithoutExtension(path); + + // Exported files are usually like: + // 58_MESH_o_tur_ba_06 + // 81_WEAR_o_tur_ba_02 + // + // Strip numeric export prefix. + var firstUnderscore = name.IndexOf('_'); + if (firstUnderscore > 0 && + name[..firstUnderscore].All(char.IsDigit)) + { + name = name[(firstUnderscore + 1)..]; + } + + return name; + } + + private static string ExtractObjectStem(string resourceName) + { + // Normalize known resource kind prefixes. + // MESH_o_tur_ba_06 -> o_tur_ba + // WEAR_o_tur_ba_02 -> o_tur_ba + var normalized = resourceName; + + foreach (var prefix in new[] { "MESH_", "WEAR_", "TEXT_", "ANIM_" }) + { + if (normalized.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + normalized = normalized[prefix.Length..]; + break; + } + } + + var tokens = normalized.Split('_', StringSplitOptions.RemoveEmptyEntries); + + return string.Join('_', tokens); + } + + private static string[] SplitResourceTokens(string value) + { + return value.Split('_', StringSplitOptions.RemoveEmptyEntries); + } + + private static int CommonPrefixTokenCount(string[] a, string[] b) + { + var count = Math.Min(a.Length, b.Length); + var result = 0; + + for (var i = 0; i < count; i++) + { + if (!string.Equals(a[i], b[i], StringComparison.OrdinalIgnoreCase)) + break; + + result++; + } + + return result; + } + + private static IReadOnlyList ParseMaterialRefs(string weaPath) + { + var lines = File.ReadAllLines(weaPath) + .Select(x => x.Trim()) + .Where(x => x.Length != 0) + .ToList(); + + if (lines.Count == 0 || !int.TryParse(lines[0], out var materialCount) || materialCount <= 0) + return []; + + var result = new List(); + + for (var i = 0; i < materialCount && i + 1 < lines.Count; i++) + { + var line = lines[i + 1]; + if (line.Equals("LIGHTMAPS", StringComparison.OrdinalIgnoreCase)) + break; + + var parts = line.Split((char[]?)null, 2, + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (parts.Length < 2) + continue; + + if (!int.TryParse(parts[0], out var id)) + continue; + + result.Add(new WeaMaterialRef(id, parts[1])); + } + + return result; + } + + private static uint? TryLoadMaterialTexture( + GL gl, FileStream materialFs, NResArchive materialArchive, string materialName + ) + { + var entry = FindMaterialEntry(materialArchive, materialName); + if (entry == null) + return null; + + materialFs.Seek(entry.OffsetInFile, SeekOrigin.Begin); + var materialData = new byte[entry.FileLength]; + materialFs.ReadExactly(materialData, 0, materialData.Length); + + using var ms = new MemoryStream(materialData, writable: false); + + var materialFile = MaterialParser.ReadFromStream(ms, materialName, entry.ElementCount, entry.Magic1); + + var texture = materialFile.Stages[0].TextureName; + + var textureLibFs = "C:\\IronStrategy\\Textures.lib"; + + using var textureFs = new FileStream(textureLibFs, FileMode.Open, FileAccess.Read, FileShare.Read); + var textureResult = NResParser.ReadFile(textureFs); + if (textureResult.Archive == null) + return 0; + + var textureEntry = FindTextureEntry(textureResult.Archive, texture); + if (textureEntry is null) + { + Console.WriteLine($"Didnt find texture {texture}"); + return 0; + } + + textureFs.Seek(textureEntry.OffsetInFile, SeekOrigin.Begin); + var textureData = new byte[textureEntry.FileLength]; + textureFs.ReadExactly(textureData, 0, textureData.Length); + + using var textureMs = new MemoryStream(textureData, writable: false); + + var texmResult = TexmParser.ReadFromStream(textureMs, entry.FileName); + if (texmResult.TexmFile == null) + return null; + + var rgba = texmResult.TexmFile.GetRgba32BytesFromMipmap(0, out var width, out var height); + return ViewportTextureLoader.CreateRgbaTexture(gl, rgba, width, height); + } + + private static ListMetadataItem? FindMaterialEntry(NResArchive materialArchive, string materialName) + { + static string Normalize(string value) + { + return value.Trim().ToLowerInvariant(); + } + + var normalized = Normalize(materialName); + + return materialArchive.Files.FirstOrDefault(x => + string.Equals(x.FileName, materialName, StringComparison.OrdinalIgnoreCase) || + Normalize(x.FileName) == normalized); + } + + private static ListMetadataItem? FindTextureEntry(NResArchive archive, string name) + { + static string Normalize(string value) + { + return value.Trim().ToLowerInvariant(); + } + + var normalized = Normalize(name); + + return archive.Files.FirstOrDefault(x => + string.Equals(x.FileName, name, StringComparison.OrdinalIgnoreCase) || + Normalize(x.FileName) == normalized); + } + + private static string? FindTexturesLib(string? startDirectory) + { + if (string.IsNullOrWhiteSpace(startDirectory)) + return null; + + var directory = new DirectoryInfo(startDirectory); + while (directory != null) + { + var candidate = Path.Combine(directory.FullName, "Textures.lib"); + if (Directory.Exists(candidate)) + return candidate; + + candidate = Path.Combine(directory.FullName, "textures.lib"); + if (Directory.Exists(candidate)) + return candidate; + + directory = directory.Parent; + } + + return null; + } + + private readonly record struct WeaMaterialRef(int Id, string Name); +} diff --git a/NResUI/Rendering/Viewport/OpenGL/ShaderProgram.cs b/NResUI/Rendering/Viewport/OpenGL/ShaderProgram.cs index 04ba547..94fad68 100644 --- a/NResUI/Rendering/Viewport/OpenGL/ShaderProgram.cs +++ b/NResUI/Rendering/Viewport/OpenGL/ShaderProgram.cs @@ -54,6 +54,11 @@ public sealed unsafe class ShaderProgram _gl.Uniform3(location, value.X, value.Y, value.Z); } + public void SetInt(int location, int value) + { + _gl.Uniform1(location, value); + } + public void SetVector4(int location, Vector4 value) { _gl.Uniform4(location, value.X, value.Y, value.Z, value.W); diff --git a/NResUI/Rendering/Viewport/OpenGL/ViewportTextureLoader.cs b/NResUI/Rendering/Viewport/OpenGL/ViewportTextureLoader.cs new file mode 100644 index 0000000..3b14f88 --- /dev/null +++ b/NResUI/Rendering/Viewport/OpenGL/ViewportTextureLoader.cs @@ -0,0 +1,35 @@ +using Silk.NET.OpenGL; + +namespace NResUI.Rendering.Viewport.OpenGL; + +public static unsafe class ViewportTextureLoader +{ + public static uint CreateRgbaTexture(GL gl, byte[] rgba, int width, int height) + { + var texture = gl.GenTexture(); + gl.BindTexture(TextureTarget.Texture2D, texture); + + fixed (byte* data = rgba) + { + gl.TexImage2D( + TextureTarget.Texture2D, + 0, + InternalFormat.Rgba8, + (uint)width, + (uint)height, + 0, + PixelFormat.Rgba, + PixelType.UnsignedByte, + data); + } + + gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.LinearMipmapLinear); + gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); + gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat); + gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat); + gl.GenerateMipmap(TextureTarget.Texture2D); + + gl.BindTexture(TextureTarget.Texture2D, 0); + return texture; + } +} diff --git a/NResUI/Rendering/Viewport/ViewportMaterial.cs b/NResUI/Rendering/Viewport/ViewportMaterial.cs new file mode 100644 index 0000000..4ab9546 --- /dev/null +++ b/NResUI/Rendering/Viewport/ViewportMaterial.cs @@ -0,0 +1,16 @@ +namespace NResUI.Rendering.Viewport; + +public sealed class ViewportMaterial +{ + public string Name { get; } + public uint TextureHandle { get; } + public bool HasTexture => TextureHandle != 0; + + public ViewportMaterial(string name, uint textureHandle = 0) + { + Name = name; + TextureHandle = textureHandle; + } + + public static ViewportMaterial Untextured { get; } = new("Untextured"); +} diff --git a/NResUI/Rendering/Viewport/ViewportPiece.cs b/NResUI/Rendering/Viewport/ViewportPiece.cs index f499f80..6b06f70 100644 --- a/NResUI/Rendering/Viewport/ViewportPiece.cs +++ b/NResUI/Rendering/Viewport/ViewportPiece.cs @@ -7,7 +7,9 @@ public sealed class ViewportPiece { public int Id { get; } public string Name { get; } - public GpuMesh Mesh { get; } + public IReadOnlyList Meshes { get; } + + public GpuMesh Mesh => Meshes[0]; public Matrix4x4 LocalTransform { get; set; } @@ -24,10 +26,25 @@ public sealed class ViewportPiece Vector3 boundsMin, Vector3 boundsMax, ViewportPieceDebugInfo? debugInfo = null) + : this(id, name, new[] { mesh }, localTransform, boundsMin, boundsMax, debugInfo) { + } + + public ViewportPiece( + int id, + string name, + IReadOnlyList meshes, + Matrix4x4 localTransform, + Vector3 boundsMin, + Vector3 boundsMax, + ViewportPieceDebugInfo? debugInfo = null) + { + if (meshes.Count == 0) + throw new ArgumentException("A viewport piece must contain at least one mesh.", nameof(meshes)); + Id = id; Name = name; - Mesh = mesh; + Meshes = meshes; LocalTransform = localTransform; BoundsMin = boundsMin; BoundsMax = boundsMax; diff --git a/NResUI/Rendering/Viewport/ViewportRenderer.cs b/NResUI/Rendering/Viewport/ViewportRenderer.cs index facd1a6..5a4f015 100644 --- a/NResUI/Rendering/Viewport/ViewportRenderer.cs +++ b/NResUI/Rendering/Viewport/ViewportRenderer.cs @@ -23,6 +23,8 @@ public sealed class ViewportRenderer private int _modelLocation; private int _mvpLocation; private int _lightDirectionLocation; + private int _useTextureLocation; + private int _texture0Location; private int _outlineMvpLocation; private int _outlineColorLocation; @@ -93,11 +95,13 @@ public sealed class ViewportRenderer if (grid == null || !grid.IsVisible) return; + var model = grid.LocalTransform * sceneRotation; + _gl.Disable(EnableCap.StencilTest); _gl.PolygonMode(TriangleFace.FrontAndBack, PolygonMode.Fill); - var model = grid.LocalTransform * sceneRotation; DrawMesh(grid.Mesh, model, view, projection); + } private void DrawScene( @@ -205,7 +209,8 @@ public sealed class ViewportRenderer Matrix4x4 projection) { var model = piece.LocalTransform * sceneRotation; - DrawMesh(piece.Mesh, model, view, projection); + foreach (var mesh in piece.Meshes) + DrawMesh(mesh, model, view, projection); } private void DrawPieceOutline( @@ -226,7 +231,8 @@ public sealed class ViewportRenderer _outlineShader.SetMatrix4(_outlineMvpLocation, mvp); _outlineShader.SetVector4(_outlineColorLocation, new Vector4(1.0f, 0.82f, 0.15f, 1.0f)); - piece.Mesh.Draw(); + foreach (var mesh in piece.Meshes) + mesh.Draw(); } private void DrawMesh( @@ -247,6 +253,19 @@ public sealed class ViewportRenderer _meshShader.SetMatrix4(_modelLocation, model); _meshShader.SetVector3(_lightDirectionLocation, lightDirection); + if (mesh.Material.HasTexture) + { + _gl.ActiveTexture(TextureUnit.Texture0); + _gl.BindTexture(TextureTarget.Texture2D, mesh.Material.TextureHandle); + _meshShader.SetInt(_texture0Location, 0); + _meshShader.SetInt(_useTextureLocation, 1); + } + else + { + _gl.BindTexture(TextureTarget.Texture2D, 0); + _meshShader.SetInt(_useTextureLocation, 0); + } + mesh.Draw(); } @@ -277,17 +296,20 @@ public sealed class ViewportRenderer layout (location = 0) in vec3 aPosition; layout (location = 1) in vec3 aColor; layout (location = 2) in vec3 aNormal; + layout (location = 3) in vec2 aTexCoord; uniform mat4 uModel; uniform mat4 uMvp; out vec3 vColor; out vec3 vNormalWorld; + out vec2 vTexCoord; void main() { vColor = aColor; vNormalWorld = mat3(transpose(inverse(uModel))) * aNormal; + vTexCoord = aTexCoord; gl_Position = uMvp * vec4(aPosition, 1.0); } """; @@ -297,8 +319,11 @@ public sealed class ViewportRenderer in vec3 vColor; in vec3 vNormalWorld; + in vec2 vTexCoord; uniform vec3 uLightDirectionWorld; + uniform bool uUseTexture; + uniform sampler2D uTexture0; out vec4 FragColor; @@ -315,7 +340,11 @@ public sealed class ViewportRenderer float diffuse = max(dot(normal, lightDir), 0.0); float lighting = 0.35 + diffuse * 0.65; - FragColor = vec4(vColor * lighting, 1.0); + vec4 texel = uUseTexture ? texture(uTexture0, vTexCoord) : vec4(1.0); + if (texel.a < 0.05) + discard; + + FragColor = vec4(vColor * texel.rgb * lighting, texel.a); } """; @@ -351,6 +380,8 @@ public sealed class ViewportRenderer _modelLocation = _meshShader.GetUniformLocation("uModel"); _mvpLocation = _meshShader.GetUniformLocation("uMvp"); _lightDirectionLocation = _meshShader.GetUniformLocation("uLightDirectionWorld"); + _useTextureLocation = _meshShader.GetUniformLocation("uUseTexture"); + _texture0Location = _meshShader.GetUniformLocation("uTexture0"); _outlineMvpLocation = _outlineShader.GetUniformLocation("uMvp"); _outlineColorLocation = _outlineShader.GetUniformLocation("uColor");