diff --git a/src/AcDream.Core/Meshing/.gitkeep b/src/AcDream.Core/Meshing/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/AcDream.Core/Meshing/GfxObjMesh.cs b/src/AcDream.Core/Meshing/GfxObjMesh.cs new file mode 100644 index 0000000..31e6309 --- /dev/null +++ b/src/AcDream.Core/Meshing/GfxObjMesh.cs @@ -0,0 +1,88 @@ +using System.Numerics; +using AcDream.Core.Terrain; +using DatReaderWriter.DBObjs; + +namespace AcDream.Core.Meshing; + +public static class GfxObjMesh +{ + /// + /// Walk a GfxObj's polygons and produce one + /// per referenced Surface. Polygons are triangulated as fans. + /// + public static IReadOnlyList Build(GfxObj gfxObj) + { + // Group output vertices and indices per surface index. + var perSurface = new Dictionary Vertices, List Indices, Dictionary<(int pos, int uv), uint> Dedupe)>(); + + foreach (var kvp in gfxObj.Polygons) + { + var poly = kvp.Value; + + if (poly.VertexIds.Count < 3) + continue; // degenerate + + int surfaceIdx = poly.PosSurface; + if (surfaceIdx < 0 || surfaceIdx >= gfxObj.Surfaces.Count) + continue; // out of range surface + + if (!perSurface.TryGetValue(surfaceIdx, out var bucket)) + { + bucket = (new List(), new List(), new Dictionary<(int, int), uint>()); + perSurface[surfaceIdx] = bucket; + } + + // Collect output vertex indices for this polygon. + var polyOut = new List(poly.VertexIds.Count); + bool skipPoly = false; + + for (int i = 0; i < poly.VertexIds.Count; i++) + { + int posIdx = poly.VertexIds[i]; + int uvIdx = i < poly.PosUVIndices.Count ? poly.PosUVIndices[i] : 0; + + if (!gfxObj.VertexArray.Vertices.TryGetValue((ushort)posIdx, out var sw)) + { + skipPoly = true; + break; + } + + var texcoord = uvIdx >= 0 && uvIdx < sw.UVs.Count + ? new Vector2(sw.UVs[uvIdx].U, sw.UVs[uvIdx].V) + : Vector2.Zero; + + var key = (posIdx, uvIdx); + if (!bucket.Dedupe.TryGetValue(key, out var outIdx)) + { + outIdx = (uint)bucket.Vertices.Count; + bucket.Vertices.Add(new Vertex(sw.Origin, sw.Normal, texcoord)); + bucket.Dedupe[key] = outIdx; + } + polyOut.Add(outIdx); + } + + if (skipPoly || polyOut.Count < 3) + continue; + + // Fan triangulation: (v0, v1, v2), (v0, v2, v3), ... + for (int i = 1; i < polyOut.Count - 1; i++) + { + bucket.Indices.Add(polyOut[0]); + bucket.Indices.Add(polyOut[i]); + bucket.Indices.Add(polyOut[i + 1]); + } + } + + // Emit one sub-mesh per surface. + var result = new List(perSurface.Count); + foreach (var kvp in perSurface) + { + var surfaceId = (uint)gfxObj.Surfaces[kvp.Key]; + result.Add(new GfxObjSubMesh( + SurfaceId: surfaceId, + Vertices: kvp.Value.Vertices.ToArray(), + Indices: kvp.Value.Indices.ToArray())); + } + return result; + } +} diff --git a/src/AcDream.Core/Meshing/GfxObjSubMesh.cs b/src/AcDream.Core/Meshing/GfxObjSubMesh.cs new file mode 100644 index 0000000..6c399e5 --- /dev/null +++ b/src/AcDream.Core/Meshing/GfxObjSubMesh.cs @@ -0,0 +1,12 @@ +using AcDream.Core.Terrain; + +namespace AcDream.Core.Meshing; + +/// +/// One sub-mesh of a GfxObj: a vertex+index buffer that uses a single Surface. +/// A GfxObj with multiple surfaces produces multiple sub-meshes. +/// +public sealed record GfxObjSubMesh( + uint SurfaceId, + Vertex[] Vertices, + uint[] Indices); diff --git a/tests/AcDream.Core.Tests/Meshing/GfxObjMeshTests.cs b/tests/AcDream.Core.Tests/Meshing/GfxObjMeshTests.cs new file mode 100644 index 0000000..c80bbad --- /dev/null +++ b/tests/AcDream.Core.Tests/Meshing/GfxObjMeshTests.cs @@ -0,0 +1,250 @@ +using System.Numerics; +using AcDream.Core.Meshing; +using DatReaderWriter.DBObjs; +using DatReaderWriter.Enums; +using DatReaderWriter.Lib; +using DatReaderWriter.Types; + +namespace AcDream.Core.Tests.Meshing; + +public class GfxObjMeshTests +{ + /// + /// Build a minimal GfxObj fixture with a single triangle using surface index 0. + /// Three unique positions, one UV slot each. + /// + private static GfxObj BuildSingleTriangle() + { + var gfx = new GfxObj + { + Surfaces = { 0x08000000u }, // synthetic surface id + VertexArray = new VertexArray + { + VertexType = VertexType.CSWVertexType, + Vertices = + { + [0] = new SWVertex + { + Origin = new Vector3(0, 0, 0), + Normal = new Vector3(0, 0, 1), + UVs = { new Vec2Duv { U = 0, V = 0 } }, + }, + [1] = new SWVertex + { + Origin = new Vector3(1, 0, 0), + Normal = new Vector3(0, 0, 1), + UVs = { new Vec2Duv { U = 1, V = 0 } }, + }, + [2] = new SWVertex + { + Origin = new Vector3(0, 1, 0), + Normal = new Vector3(0, 0, 1), + UVs = { new Vec2Duv { U = 0, V = 1 } }, + }, + }, + }, + Polygons = + { + [0] = new Polygon + { + PosSurface = 0, + NegSurface = -1, + VertexIds = { 0, 1, 2 }, + PosUVIndices = { 0, 0, 0 }, + }, + }, + }; + return gfx; + } + + [Fact] + public void Build_SingleTriangle_ProducesOneSubMeshOneTriangle() + { + var gfx = BuildSingleTriangle(); + + var subs = GfxObjMesh.Build(gfx); + + var sub = Assert.Single(subs); + Assert.Equal(0x08000000u, sub.SurfaceId); + Assert.Equal(3, sub.Vertices.Length); + Assert.Equal(3, sub.Indices.Length); // one triangle, 3 indices + } + + [Fact] + public void Build_SingleTriangle_CopiesPositionsNormalsAndUVs() + { + var gfx = BuildSingleTriangle(); + + var sub = GfxObjMesh.Build(gfx).Single(); + + // Indices point at unique vertices; collect them in order. + var vAtIdx0 = sub.Vertices[sub.Indices[0]]; + var vAtIdx1 = sub.Vertices[sub.Indices[1]]; + var vAtIdx2 = sub.Vertices[sub.Indices[2]]; + + Assert.Equal(new Vector3(0, 0, 0), vAtIdx0.Position); + Assert.Equal(new Vector3(1, 0, 0), vAtIdx1.Position); + Assert.Equal(new Vector3(0, 1, 0), vAtIdx2.Position); + + Assert.Equal(new Vector3(0, 0, 1), vAtIdx0.Normal); + Assert.Equal(new Vector2(0, 0), vAtIdx0.TexCoord); + Assert.Equal(new Vector2(1, 0), vAtIdx1.TexCoord); + Assert.Equal(new Vector2(0, 1), vAtIdx2.TexCoord); + } + + [Fact] + public void Build_Quad_IsTriangulatedAsFan() + { + // Single quad polygon with 4 vertices -> 2 triangles, 6 indices. + var gfx = new GfxObj + { + Surfaces = { 0x08000000u }, + VertexArray = new VertexArray + { + Vertices = + { + [0] = new SWVertex { Origin = new(0, 0, 0), UVs = { new Vec2Duv() } }, + [1] = new SWVertex { Origin = new(1, 0, 0), UVs = { new Vec2Duv() } }, + [2] = new SWVertex { Origin = new(1, 1, 0), UVs = { new Vec2Duv() } }, + [3] = new SWVertex { Origin = new(0, 1, 0), UVs = { new Vec2Duv() } }, + }, + }, + Polygons = + { + [0] = new Polygon + { + PosSurface = 0, + VertexIds = { 0, 1, 2, 3 }, + PosUVIndices = { 0, 0, 0, 0 }, + }, + }, + }; + + var sub = GfxObjMesh.Build(gfx).Single(); + + Assert.Equal(4, sub.Vertices.Length); + Assert.Equal(6, sub.Indices.Length); // 2 triangles + } + + [Fact] + public void Build_SamePositionDifferentUVs_DuplicatesOutputVertices() + { + // One vertex has two different UV slots. Each (posIdx, uvIdx) combo + // becomes a distinct output vertex. + var gfx = new GfxObj + { + Surfaces = { 0x08000000u }, + VertexArray = new VertexArray + { + Vertices = + { + [0] = new SWVertex + { + Origin = new(0, 0, 0), + UVs = + { + new Vec2Duv { U = 0, V = 0 }, + new Vec2Duv { U = 1, V = 1 }, + }, + }, + [1] = new SWVertex { Origin = new(1, 0, 0), UVs = { new Vec2Duv() } }, + [2] = new SWVertex { Origin = new(0, 1, 0), UVs = { new Vec2Duv() } }, + }, + }, + Polygons = + { + [0] = new Polygon + { + PosSurface = 0, + VertexIds = { 0, 1, 2 }, + PosUVIndices = { 0, 0, 0 }, + }, + [1] = new Polygon + { + PosSurface = 0, + VertexIds = { 0, 1, 2 }, + PosUVIndices = { 1, 0, 0 }, // same positions, different UV on vert 0 + }, + }, + }; + + var sub = GfxObjMesh.Build(gfx).Single(); + + // vert 0 has two different UV slots → 2 output vertices for pos 0 + // vert 1 + 2 unique → 2 more output vertices + // total: 4 output vertices + Assert.Equal(4, sub.Vertices.Length); + Assert.Equal(6, sub.Indices.Length); // 2 triangles + } + + [Fact] + public void Build_MultipleSurfaces_ProducesMultipleSubMeshes() + { + // 2 polygons, 2 surfaces → 2 sub-meshes. + var gfx = new GfxObj + { + Surfaces = { 0x08000001u, 0x08000002u }, + VertexArray = new VertexArray + { + Vertices = + { + [0] = new SWVertex { Origin = new(0, 0, 0), UVs = { new Vec2Duv() } }, + [1] = new SWVertex { Origin = new(1, 0, 0), UVs = { new Vec2Duv() } }, + [2] = new SWVertex { Origin = new(0, 1, 0), UVs = { new Vec2Duv() } }, + [3] = new SWVertex { Origin = new(1, 1, 0), UVs = { new Vec2Duv() } }, + }, + }, + Polygons = + { + [0] = new Polygon + { + PosSurface = 0, + VertexIds = { 0, 1, 2 }, + PosUVIndices = { 0, 0, 0 }, + }, + [1] = new Polygon + { + PosSurface = 1, + VertexIds = { 1, 3, 2 }, + PosUVIndices = { 0, 0, 0 }, + }, + }, + }; + + var subs = GfxObjMesh.Build(gfx); + + Assert.Equal(2, subs.Count); + Assert.Contains(subs, s => s.SurfaceId == 0x08000001u); + Assert.Contains(subs, s => s.SurfaceId == 0x08000002u); + } + + [Fact] + public void Build_DegeneratePolygonWithTwoVertices_Skipped() + { + var gfx = new GfxObj + { + Surfaces = { 0x08000000u }, + VertexArray = new VertexArray + { + Vertices = + { + [0] = new SWVertex { Origin = new(0, 0, 0), UVs = { new Vec2Duv() } }, + [1] = new SWVertex { Origin = new(1, 0, 0), UVs = { new Vec2Duv() } }, + }, + }, + Polygons = + { + [0] = new Polygon + { + PosSurface = 0, + VertexIds = { 0, 1 }, + PosUVIndices = { 0, 0 }, + }, + }, + }; + + var subs = GfxObjMesh.Build(gfx); + + Assert.Empty(subs); // no valid polygons → no sub-meshes + } +}