material initial and pose fix

This commit is contained in:
bird_egop
2026-06-06 21:26:56 +03:00
parent 776bb9fcb3
commit 5baf3f324f
13 changed files with 536 additions and 248 deletions
+1 -1
View File
@@ -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");
}
+5 -1
View File
@@ -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()
@@ -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<float>();
var indices = new List<uint>();
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<float>();
var indices = new List<uint>();
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<float> vertices,
List<uint> 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<float>();
var indices = new List<uint>();
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<float> 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<float>();
var indices = new List<uint>();
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<float> vertices,
IReadOnlyList<uint> indices,
PrimitiveType primitiveType = PrimitiveType.Triangles)
public static GpuMesh CreateColoredIndexedMesh(GL gl, IReadOnlyList<float> vertices, IReadOnlyList<uint> 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<float> vertices, List<uint> 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<float> 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);
}
}
@@ -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);
}
}
@@ -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<ViewportPiece> LoadModelPieces(GL gl, FileStream fs, NResArchive archive)
private static List<ViewportPiece> 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<ViewportPiece>();
@@ -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<Common.Vector3> positions,
IReadOnlyList<Msh05Uv> uvs,
IReadOnlyList<ushort> indices,
IReadOnlyList<Msh0x0D.Batch> batches)
IReadOnlyList<Msh0x0D.Batch> batches,
WeaMaterialLibrary materialLibrary)
{
var vertices = new List<float>();
var outIndices = new List<uint>();
var color = PickDebugColor(nodeIndex);
var meshes = new List<GpuMesh>();
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<float>();
var outIndices = new List<uint>();
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<Msh05Uv> 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<float> vertices,
List<uint> 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<Msh05Uv> TryReadUvs(FileStream fs, NResArchive archive)
{
try
{
return Msh0x05.ReadComponent(fs, archive);
}
catch
{
return new List<Msh05Uv>();
}
}
private static string ResolvePieceName(IReadOnlyList<string> 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<GpuMesh> Meshes,
Vector3 BoundsMin,
Vector3 BoundsMax,
int BatchCount,
@@ -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<int, ViewportMaterial> _materialsById;
private WeaMaterialLibrary(Dictionary<int, ViewportMaterial> materialsById)
{
_materialsById = materialsById;
}
public static WeaMaterialLibrary Empty { get; } = new(new Dictionary<int, ViewportMaterial>());
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<int, ViewportMaterial>();
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<WeaMaterialRef> 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<WeaMaterialRef>();
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);
}
@@ -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);
@@ -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;
}
}
@@ -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");
}
+19 -2
View File
@@ -7,7 +7,9 @@ public sealed class ViewportPiece
{
public int Id { get; }
public string Name { get; }
public GpuMesh Mesh { get; }
public IReadOnlyList<GpuMesh> 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<GpuMesh> 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;
+35 -4
View File
@@ -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");