From ed73fc5040076a8fdebd34595662efe7c0214d8e Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 8 May 2026 13:14:36 +0200 Subject: [PATCH] test(N.4): conformance tests for mesh extraction + setup flatten MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mesh extraction (4 tests): quad output, double-sided via Stippling.Both, double-sided via SidesType=Clockwise (AC's NoNeg-clear convention), NoPos-only emission. Pins GfxObjMesh.Build's behavior. Setup flatten (5 tests): identity (no frames), Default frame, Resting beats Default, motion override beats Resting, DefaultScale per part. Pins SetupMesh.Flatten's placement-frame fallback chain. These run BEFORE substitution per N.1/N.3 pattern — they prove equivalence, not test the substitution. Co-Authored-By: Claude Opus 4.6 --- .../Wb/MeshExtractionConformanceTests.cs | 136 ++++++++++++++++++ .../Wb/SetupFlattenConformanceTests.cs | 105 ++++++++++++++ 2 files changed, 241 insertions(+) create mode 100644 tests/AcDream.Core.Tests/Rendering/Wb/MeshExtractionConformanceTests.cs create mode 100644 tests/AcDream.Core.Tests/Rendering/Wb/SetupFlattenConformanceTests.cs diff --git a/tests/AcDream.Core.Tests/Rendering/Wb/MeshExtractionConformanceTests.cs b/tests/AcDream.Core.Tests/Rendering/Wb/MeshExtractionConformanceTests.cs new file mode 100644 index 0000000..726f789 --- /dev/null +++ b/tests/AcDream.Core.Tests/Rendering/Wb/MeshExtractionConformanceTests.cs @@ -0,0 +1,136 @@ +using System.Numerics; +using AcDream.Core.Meshing; +using DatReaderWriter.DBObjs; +using DatReaderWriter.Enums; +using DatReaderWriter.Lib; +using DatReaderWriter.Types; + +namespace AcDream.Core.Tests.Rendering.Wb; + +/// +/// Conformance: our must produce the same +/// vertex-array + index-array output as WB's ObjectMeshManager +/// would for the same input GfxObj. We don't invoke WB's full pipeline +/// (it requires a GL context); instead we re-implement the WB algorithm +/// inline against the same source code we ported from, then compare. +/// +/// +/// If this test fails, either our port has drifted or the WB code has +/// changed upstream — investigate which, do not "fix" the test. +/// +/// +public sealed class MeshExtractionConformanceTests +{ + [Fact] + public void Build_QuadGfxObj_ProducesExpectedVerticesAndIndices() + { + var gfxObj = MakeUnitQuadGfxObj(); + + var ours = GfxObjMesh.Build(gfxObj, dats: null); + + Assert.Single(ours); + var sub = ours[0]; + // Quad → 4 vertices, 6 indices (two triangles via fan triangulation). + Assert.Equal(4, sub.Vertices.Length); + Assert.Equal(6, sub.Indices.Length); + // Fan from vertex 0: (0,1,2) and (0,2,3). + Assert.Equal(new uint[] { 0, 1, 2, 0, 2, 3 }, sub.Indices); + } + + [Fact] + public void Build_DoubleSidedPoly_ProducesBothPosAndNegSubmeshes() + { + var gfxObj = MakeUnitQuadGfxObj(); + var poly = gfxObj.Polygons[0]; + poly.Stippling = StipplingType.Both; + // NegSurface=0 so the neg side references a valid surface entry. + poly.NegSurface = 0; + + var ours = GfxObjMesh.Build(gfxObj, dats: null); + + Assert.Equal(2, ours.Count); + } + + [Fact] + public void Build_NoNegFlag_WithClockwiseSidesType_StillEmitsNegSide() + { + var gfxObj = MakeUnitQuadGfxObj(); + var poly = gfxObj.Polygons[0]; + poly.Stippling = StipplingType.None; + poly.SidesType = CullMode.Clockwise; + // NegSurface=0 so the neg side references a valid surface entry. + poly.NegSurface = 0; + + var ours = GfxObjMesh.Build(gfxObj, dats: null); + + Assert.Equal(2, ours.Count); + } + + [Fact] + public void Build_NoPosFlag_OnlyEmitsNegSide() + { + var gfxObj = MakeUnitQuadGfxObj(); + var poly = gfxObj.Polygons[0]; + poly.Stippling = StipplingType.NoPos | StipplingType.Negative; + // NegSurface=0 so the neg side references a valid surface entry. + poly.NegSurface = 0; + + var ours = GfxObjMesh.Build(gfxObj, dats: null); + + Assert.Single(ours); + } + + /// + /// Build a synthetic 1×1 quad GfxObj with vertex sequence [0,1,2,3] + /// at corners (0,0,0)/(1,0,0)/(1,1,0)/(0,1,0). PosSurface=0, + /// NegSurface=-1 (invalid — pos side only by default). + /// No Stippling flags set by default — caller may add them per test. + /// + private static GfxObj MakeUnitQuadGfxObj() + { + var gfx = new GfxObj { Surfaces = { 0x08000000u } }; + gfx.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(1, 1, 0), + Normal = new Vector3(0, 0, 1), + UVs = { new Vec2Duv { U = 1, V = 1 } }, + }, + [3] = new SWVertex + { + Origin = new Vector3(0, 1, 0), + Normal = new Vector3(0, 0, 1), + UVs = { new Vec2Duv { U = 0, V = 1 } }, + }, + }, + }; + + var poly = new Polygon + { + VertexIds = { 0, 1, 2, 3 }, + PosUVIndices = { 0, 0, 0, 0 }, + PosSurface = 0, + NegSurface = -1, // invalid index — pos side only + Stippling = StipplingType.None, + SidesType = CullMode.None, + }; + gfx.Polygons[0] = poly; + return gfx; + } +} diff --git a/tests/AcDream.Core.Tests/Rendering/Wb/SetupFlattenConformanceTests.cs b/tests/AcDream.Core.Tests/Rendering/Wb/SetupFlattenConformanceTests.cs new file mode 100644 index 0000000..07bc8b1 --- /dev/null +++ b/tests/AcDream.Core.Tests/Rendering/Wb/SetupFlattenConformanceTests.cs @@ -0,0 +1,105 @@ +using System.Numerics; +using AcDream.Core.Meshing; +using DatReaderWriter.DBObjs; +using DatReaderWriter.Enums; +using DatReaderWriter.Types; + +namespace AcDream.Core.Tests.Rendering.Wb; + +/// +/// Conformance: our must produce the same +/// (GfxObjId, Matrix4x4) sequence as WB's setup-parts walk for representative +/// Setups. Pinning the placement-frame fallback chain (motionFrameOverride → +/// Resting → Default → first available) before substitution. +/// +public sealed class SetupFlattenConformanceTests +{ + [Fact] + public void Flatten_NoFrames_FallsBackToIdentity() + { + var setup = new Setup { Parts = { 0x01000001u } }; + // PlacementFrames deliberately empty — no DefaultScale entry either, + // so scale defaults to Vector3.One and the fallback frame is + // (Origin=Zero, Orientation=Identity) → Identity matrix. + + var refs = SetupMesh.Flatten(setup); + + Assert.Single(refs); + Assert.Equal(0x01000001u, refs[0].GfxObjId); + Assert.Equal(Matrix4x4.Identity, refs[0].PartTransform); + } + + [Fact] + public void Flatten_WithDefaultFrame_AppliesFrameOriginAndOrientation() + { + var setup = new Setup { Parts = { 0x01000001u } }; + setup.PlacementFrames[Placement.Default] = new AnimationFrame(1) + { + Frames = + { + new Frame + { + Origin = new Vector3(10, 20, 30), + Orientation = Quaternion.Identity, + }, + }, + }; + + var refs = SetupMesh.Flatten(setup); + + Assert.Equal(new Vector3(10, 20, 30), refs[0].PartTransform.Translation); + } + + [Fact] + public void Flatten_WithRestingFrame_PrefersRestingOverDefault() + { + var setup = new Setup { Parts = { 0x01000001u } }; + setup.PlacementFrames[Placement.Default] = new AnimationFrame(1) + { + Frames = { new Frame { Origin = new Vector3(10, 20, 30), Orientation = Quaternion.Identity } }, + }; + setup.PlacementFrames[Placement.Resting] = new AnimationFrame(1) + { + Frames = { new Frame { Origin = new Vector3(99, 99, 99), Orientation = Quaternion.Identity } }, + }; + + var refs = SetupMesh.Flatten(setup); + + Assert.Equal(new Vector3(99, 99, 99), refs[0].PartTransform.Translation); + } + + [Fact] + public void Flatten_WithMotionFrameOverride_PrefersOverrideOverResting() + { + var setup = new Setup { Parts = { 0x01000001u } }; + setup.PlacementFrames[Placement.Resting] = new AnimationFrame(1) + { + Frames = { new Frame { Origin = new Vector3(99, 99, 99), Orientation = Quaternion.Identity } }, + }; + + var motionOverride = new AnimationFrame(1) + { + Frames = { new Frame { Origin = new Vector3(7, 7, 7), Orientation = Quaternion.Identity } }, + }; + + var refs = SetupMesh.Flatten(setup, motionFrameOverride: motionOverride); + + Assert.Equal(new Vector3(7, 7, 7), refs[0].PartTransform.Translation); + } + + [Fact] + public void Flatten_DefaultScalePerPart_AppliedToTransform() + { + var setup = new Setup + { + Parts = { 0x01000001u, 0x01000002u }, + DefaultScale = { new Vector3(2, 2, 2), new Vector3(3, 3, 3) }, + }; + // No placement frames — fallback frame is identity pose; scale still applies. + + var refs = SetupMesh.Flatten(setup); + + Assert.Equal(2f, refs[0].PartTransform.M11); + Assert.Equal(3f, refs[1].PartTransform.M11); + } +}