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");