using System.Numerics; namespace AcDream.App.Rendering.Walk; /// /// Retail Vec2Dscreen: homogeneous viewport coordinates as produced /// by PrimD3DRender::xformStart @0x0059b990 — X/Y are viewport-scaled /// but NOT perspective-divided (divide by W to get pixels), Z is raw clip z, /// W is raw clip w. copy_view performs the divide; /// polyClipFinish clips pre-divide homogeneously. /// public struct WalkScreenPoint { public float X, Y, Z, W; public WalkScreenPoint(float x, float y, float z, float w) { X = x; Y = y; Z = z; W = w; } } /// /// Campaign FW1 — the screen-space projection/clip chain, ported from the /// flood-read appendix (docs/research/2026-08-30-fw-flood-pseudocode-appendix.md, /// report 3; Ghidra-arbitrated — BN's literal rendering inverts the edge /// inside test and the w-clip plane). /// public static class WalkScreenClip { /// The w-clip plane constant cdstW (= retail F_EPSILON). public const float MinW = WalkVisibilityMath.Epsilon; /// /// PrimD3DRender::xformStart @0x0059b990 (toScreen path): object /// space → homogeneous viewport coordinates. x=(bw/2)(x_clip+w), /// y=(bh/2)(w−y_clip) — y flipped, origin top-left — z/w raw clip. /// is the concatenated object→clip /// matrix (row-vector convention, v * M). /// public static WalkScreenPoint TransformToScreen( Vector3 point, in Matrix4x4 objectToClip, float viewportWidth, float viewportHeight) { Vector4 clip = Vector4.Transform(new Vector4(point, 1f), objectToClip); return new WalkScreenPoint( clip.X * viewportWidth * 0.5f + clip.W * viewportWidth * 0.5f, clip.W * viewportHeight * 0.5f - clip.Y * viewportHeight * 0.5f, clip.Z, clip.W); } /// /// ACRender::polyClipFinish @0x006b6d00: Sutherland-Hodgman clip /// of a homogeneous screen polygon against the active view — first the /// w ≥ plane (only when some w is below it), then /// every view edge, iterated LAST-to-FIRST as vertex pairs /// (v[0], v[n−1]), (v[n−1], v[n−2]), …, (v[1], v[0]). Edge INSIDE is /// side ≤ 0 with the homogeneous 2D cross /// side(p) = (p.x − a.x·p.w)·ey − (p.y − a.y·p.w)·ex. Each pass scans /// its input in REVERSE; the output keeps the ORIGINAL winding (retail's /// pass-parity bookkeeping collapses to reversing per pass and /// un-reversing at the end — this port appends reversed per pass and /// restores at the end, observably identical). Returns the surviving /// count, or 0 the moment any stage drops below 3 vertices — in which /// case content is unspecified (retail never /// writes the out count on that path; callers pre-zero it). /// public static int ClipAgainstView( ReadOnlySpan input, ReadOnlySpan viewEdgeVertices, Span output) { // Working buffers sized for retail's ≤32-vertex contract plus clip growth. Span bufferA = stackalloc WalkScreenPoint[64]; Span bufferB = stackalloc WalkScreenPoint[64]; Span current = bufferA; int count = input.Length; input.CopyTo(current); // Track how many reversing passes ran so the final copy can restore // the original winding exactly as retail's parity dance does. int reversals = 0; // Pass 0: the w-plane, only when some vertex is below cdstW. bool anyBelow = false; for (int i = 0; i < count; i++) if (current[i].W < MinW) { anyBelow = true; break; } if (anyBelow) { count = ClipPassW(current[..count], bufferB); if (count < 3) return 0; Span swap = current; current = bufferB; bufferB = swap; reversals++; } // Edge passes: pairs (a, b) = (v[0], v[n-1]), (v[n-1], v[n-2]) … (v[1], v[0]). int n = viewEdgeVertices.Length; for (int e = n - 1; e >= 0; e--) { Vector2 a = viewEdgeVertices[e == n - 1 ? 0 : e + 1]; Vector2 b = viewEdgeVertices[e]; count = ClipPassEdge(current[..count], a, b, bufferB); if (count < 3) return 0; Span swap = current; current = bufferB; bufferB = swap; reversals++; } // Restore original winding: each pass reversed the order once. if ((reversals & 1) != 0) { for (int i = 0; i < count; i++) output[i] = current[count - 1 - i]; } else { current[..count].CopyTo(output); } return count; } private static int ClipPassW(ReadOnlySpan pts, Span outPts) { int outCount = 0; // Reverse traversal starting from the wrap pair (pts[0], pts[n-1]). WalkScreenPoint prev = pts[0]; float sPrev = prev.W - MinW; bool inPrev = sPrev >= 0f; for (int i = pts.Length - 1; i >= 0; i--) { WalkScreenPoint cur = pts[i]; float s = cur.W - MinW; bool inCur = s >= 0f; if (inPrev != inCur) outPts[outCount++] = Lerp(prev, cur, sPrev / (sPrev - s)); if (inCur) outPts[outCount++] = cur; prev = cur; sPrev = s; inPrev = inCur; } return outCount; } private static int ClipPassEdge( ReadOnlySpan pts, Vector2 a, Vector2 b, Span outPts) { float ex = b.X - a.X; float ey = b.Y - a.Y; float Side(in WalkScreenPoint p) => (p.X - a.X * p.W) * ey - (p.Y - a.Y * p.W) * ex; int outCount = 0; WalkScreenPoint prev = pts[0]; float s0 = Side(prev); float sPrev = s0; bool inPrev = s0 <= 0f; // INSIDE = side <= 0 (Ghidra-verified) for (int i = pts.Length - 1; i >= 0; i--) { WalkScreenPoint cur = pts[i]; float s = i != 0 ? Side(cur) : s0; // final pair reuses point 0's side bool inCur = s <= 0f; if (inPrev != inCur) outPts[outCount++] = Lerp(prev, cur, sPrev / (sPrev - s)); if (inCur) outPts[outCount++] = cur; prev = cur; sPrev = s; inPrev = inCur; } return outCount; } private static WalkScreenPoint Lerp(in WalkScreenPoint p, in WalkScreenPoint q, float t) => new( p.X + (q.X - p.X) * t, p.Y + (q.Y - p.Y) * t, p.Z + (q.Z - p.Z) * t, p.W + (q.W - p.W) * t); }