using System; using System.Numerics; using Xunit; namespace AcDream.App.Tests.Rendering; /// /// #108-residual orientation pin: TerrainModernRenderer culls terrain back /// faces with FrontFace(Ccw) — the GL port of retail's single-sided terrain /// (ACRender::landPolysDraw 0x006b7040: a land triangle draws ONLY when the /// camera is on the POSITIVE side of its plane via Plane::which_side2). /// /// The FrontFace choice rests on one mapping fact: under the production /// camera convention (Matrix4x4.CreateLookAt with up = world +Z, Numerics /// CreatePerspectiveFieldOfView — RetailChaseCamera.cs:203 / :52), an /// UP-FACING terrain triangle that LandblockMesh emits CCW in world XY /// rasterizes /// · CCW in NDC/window space when the eye is ABOVE its plane (kept), and /// · CW when the eye is BELOW (culled — retail draws nothing there: from /// a below-grade cellar eye the door aperture shows sky, never grass). /// This test pins that mapping in pure CPU math so a projection-convention /// change (handedness, Y-flip) can't silently invert the cull and either /// resurrect the #108 grass window or cull terrain from above. /// public class TerrainCullOrientationTests { // An up-facing triangle, CCW in world XY viewed from above — the exact // emission convention pinned by LandblockMeshTests (crossZ > 0). private static readonly Vector3[] Triangle = { new(-1f, 10f, 94f), new( 1f, 10f, 94f), new( 1f, 12f, 94f), }; private static float NdcSignedArea2(Vector3 eye, Vector3 forward) { // The production camera shape: look-at with world-Z up // (RetailChaseCamera.cs:203), Numerics perspective with the retail // znear 0.1 (RetailChaseCamera.cs:52). var view = Matrix4x4.CreateLookAt(eye, eye + forward, Vector3.UnitZ); var proj = Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 3f, 16f / 9f, 0.1f, 5000f); var viewProj = view * proj; Span ndc = stackalloc Vector2[3]; for (int i = 0; i < 3; i++) { var c = Vector4.Transform(new Vector4(Triangle[i], 1f), viewProj); Assert.True(c.W > 1e-3f, "test triangle must be in front of the eye"); ndc[i] = new Vector2(c.X / c.W, c.Y / c.W); } // Twice the signed area: > 0 = CCW in NDC (GL window space keeps the // orientation — NDC y up maps to window y up, no flip). return (ndc[1].X - ndc[0].X) * (ndc[2].Y - ndc[0].Y) - (ndc[1].Y - ndc[0].Y) * (ndc[2].X - ndc[0].X); } [Fact] public void EyeAboveTerrainPlane_WindsCcw_FrontFaceKept() { // Eye above grade looking forward-down at the triangle (the normal // outdoor view). Retail: which_side2 = POSITIVE → drawn. float area = NdcSignedArea2(new Vector3(0f, 5f, 96.5f), new Vector3(0f, 1f, -0.3f)); Assert.True(area > 0f, $"above-plane eye must see the terrain triangle CCW (area2={area}) — " + "FrontFace(Ccw)+Cull(Back) would otherwise cull terrain from above"); } [Fact] public void EyeBelowTerrainPlane_WindsCw_BackfaceCulled() { // Eye below grade (the cellar-stairwell window) looking up-forward at // the underside. Retail: which_side2 = NEGATIVE → not drawn at all — // the #108 grass that covered the exit door was exactly this // underside rasterizing when culling was left disabled. float area = NdcSignedArea2(new Vector3(0f, 5f, 92.5f), new Vector3(0f, 1f, 0.2f)); Assert.True(area < 0f, $"below-plane eye must see the terrain triangle CW (area2={area}) — " + "it must backface-cull like retail's which_side2 eye-side gate"); } }