feat(render): Phase W Stage 4 — sky/weather portal-clip seal (LScape through the doorway)

The sky + weather (rain cylinder) are retail's LScape — 'the outside seen through the exit portal.' Retail PView::DrawCells (pseudo_c:432709) draws LScape clipped to the OutsideView when outside_view.view_count>0, then does a conditional Z-buffer-ONLY clear (432731) before the indoor cells. acdream now does the same:

- sky.vert writes gl_ClipDistance against the SAME binding=2 TerrainClip UBO the terrain reads. The OutsideView planes are screen-space (NDC) half-spaces encoded as clip-space planes (nx,ny,0,dw); the test dot(plane,gl_Position)>=0 reduces after perspective divide to nx*ndcX+ny*ndcY+dw>=0 — projection-INDEPENDENT — so the same plane set clips the sky EXACTLY despite its separate dome projection. count==0 (outdoor) → all distances +1 → full-screen, bit-identical. Lighting/fog math untouched.

- GameWindow: relocated the sky pre-scene + weather post-scene draws to their retail LScape positions, each in a local 8-plane clip bracket so sky.vert confines them to the doorway indoors / full-screen outdoors. Added the conditional doorway depth-ONLY Z-clear (no color → no blue hole), scissored to the OutsideView AABB. drawSkyThisFrame = seen_outside policy AND (outdoor OR exit-portal-in-view) — a sealed interior with no exit portal in view draws no sky (kills the full-screen-sky interim regression). Sky pre/post particle passes (particle.vert has no gl_ClipDistance) scissored to the doorway bbox.

- ClipFrameAssembly gains HasOutsideView + OutsideViewNdcAabb (the doorway NDC AABB, computed for BOTH Planes and Scissor terrain modes — unlike TerrainScissorNdcAabb which is Scissor-only).

- The pre-login goto SkipWorldGeometry moved BELOW the sky draw so the live sky still renders during the EnterWorld handshake (clipAssembly is null/no-clip pre-login → full-screen).

Build green; App tests 160/160. Stage 4 tests + verify-annotations follow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-02 16:15:08 +02:00
parent 55e1b30553
commit ce2edad66a
3 changed files with 194 additions and 30 deletions

View file

@ -95,6 +95,22 @@ public sealed class ClipFrameAssembly
/// <see cref="TerrainMode"/> is <see cref="TerrainClipMode.Scissor"/>. Unused otherwise.</summary>
public required Vector4 TerrainScissorNdcAabb { get; init; }
/// <summary>True ⇒ the OutsideView (the exit-portal screen region) is meaningfully visible this
/// frame — the camera can see outdoors through a portal chain (<see cref="TerrainMode"/> is
/// <see cref="TerrainClipMode.Planes"/> or <see cref="TerrainClipMode.Scissor"/>). False ⇒ a
/// sealed interior with no exit portal in view (<see cref="TerrainClipMode.Skip"/>). Drives the
/// Stage 4 sky/weather draw + the conditional doorway Z-clear. Always false on the outdoor root
/// (the caller does not invoke <see cref="ClipFrameAssembler.Assemble"/> there).</summary>
public required bool HasOutsideView { get; init; }
/// <summary>NDC AABB (minX,minY,maxX,maxY) of the OutsideView screen region — the doorway
/// opening's bounding box. Computed whenever <see cref="HasOutsideView"/> is true, for BOTH the
/// Planes and Scissor terrain modes (unlike <see cref="TerrainScissorNdcAabb"/>, which is valid
/// only in Scissor mode). Stage 4 scissors the conditional doorway depth-only Z-clear (retail
/// PView::DrawCells:432731) and the sky/weather particle passes to this region. Degenerate
/// (<see cref="Vector4.Zero"/>) when <see cref="HasOutsideView"/> is false.</summary>
public required Vector4 OutsideViewNdcAabb { get; init; }
// ---- Probe data (ACDREAM_PROBE_VIS / RenderingDiagnostics.EmitVis) --------
/// <summary>Plane count the OutsideView reduced to (0 ⇒ scissor or empty).</summary>
@ -196,6 +212,17 @@ public static class ClipFrameAssembler
scissorFallbacks++;
}
// Stage 4: the doorway screen-space AABB (the OutsideView union bounds), available for
// BOTH Planes and Scissor modes — the sky/weather particle scissor + the conditional
// doorway Z-clear need it regardless of how the OutsideView reduced to a gate.
// TerrainScissorNdcAabb above is only valid in Scissor mode; the OutsideView CellView
// always tracks its Min/Max as polygons accumulate, so it is the single source here.
bool hasOutsideView = terrainMode != TerrainClipMode.Skip;
Vector4 outsideViewNdcAabb = (hasOutsideView && !pvFrame.OutsideView.IsEmpty)
? new Vector4(pvFrame.OutsideView.MinX, pvFrame.OutsideView.MinY,
pvFrame.OutsideView.MaxX, pvFrame.OutsideView.MaxY)
: Vector4.Zero;
return new ClipFrameAssembly
{
Frame = frame,
@ -204,6 +231,8 @@ public static class ClipFrameAssembler
OutdoorVisible = outdoorVisible,
TerrainMode = terrainMode,
TerrainScissorNdcAabb = terrainScissor,
HasOutsideView = hasOutsideView,
OutsideViewNdcAabb = outsideViewNdcAabb,
OutsidePlaneCount = ov.Count,
PerCellPlaneCounts = perCellPlaneCounts,
ScissorFallbacks = scissorFallbacks,