diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index d59a9ac..8ea9f6a 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -7391,11 +7391,21 @@ public sealed class GameWindow : IDisposable System.Numerics.Vector4 skyDoorwayNdc = clipAssembly?.OutsideViewNdcAabb ?? default; if (drawSkyThisFrame) { - // Sky MESH: enable the 8 clip planes around RenderSky so sky.vert clips it to the - // OutsideView (binding=2 UBO: count>0 indoor → confined to the doorway; count==0 - // outdoor → all distances +1 → full-screen, bit-identical to pre-Stage-4). Re-bind - // binding=2 (UBO namespace) defensively — SkyRenderer does not own it, and we must not - // inherit whatever was last bound (memory: render-self-contained-gl-state). + // Scissor the WHOLE sky pre-scene block (mesh + particles) to the doorway AABB when + // indoors. The sky MESH is precisely clipped by sky.vert's gl_ClipDistance in PLANES + // mode (the scissor is then a harmless over-include — the planes are tighter); but in + // SCISSOR mode the OutsideView exceeded the convex-plane budget so the assembler left + // the binding=2 UBO at count 0 (no planes) — there the scissor is the ONLY confinement, + // exactly mirroring the terrain Scissor path. Without this, a multi-exit interior would + // bleed full-screen sky/rain (sky.vert with count 0 writes all +1 = no clip). The + // SkyPreScene particles (particle.vert, no gl_ClipDistance) rely on the scissor in BOTH + // modes. Outdoors (skyDoorwayClip=false) → no scissor → full-screen, bit-identical. + bool skySc = BeginDoorwayScissor(skyDoorwayClip, skyDoorwayNdc); + + // Sky MESH: re-bind binding=2 (the OutsideView UBO) defensively — SkyRenderer does not + // own it and we must not inherit whatever was last bound (memory: + // render-self-contained-gl-state) — then enable the 8 clip planes so sky.vert clips + // precisely in Planes mode (count>0). count==0 (outdoor / Scissor-mode) → all +1. _gl.BindBufferBase(BufferTargetARB.UniformBuffer, ClipFrame.TerrainClipUboBinding, _clipFrame.TerrainUbo); for (int _cp = 0; _cp < ClipFrame.MaxPlanes; _cp++) @@ -7405,15 +7415,12 @@ public sealed class GameWindow : IDisposable for (int _cp = 0; _cp < ClipFrame.MaxPlanes; _cp++) _gl.Disable(EnableCap.ClipDistance0 + _cp); - // SkyPreScene particles (particle.vert, no gl_ClipDistance) → scissor to the doorway - // bbox indoors. Outdoors (skyDoorwayClip=false) draws full-screen. + // SkyPreScene particles (particle.vert, no gl_ClipDistance) — confined by the scissor. if (_particleSystem is not null && _particleRenderer is not null) - { - bool sc = BeginDoorwayScissor(skyDoorwayClip, skyDoorwayNdc); _particleRenderer.Draw(_particleSystem, camera, camPos, AcDream.Core.Vfx.ParticleRenderPass.SkyPreScene); - if (sc) _gl.Disable(EnableCap.ScissorTest); - } + + if (skySc) _gl.Disable(EnableCap.ScissorTest); } // K-fix1 (2026-04-26): suppress terrain + entity rendering while live mode is configured @@ -7546,10 +7553,11 @@ public sealed class GameWindow : IDisposable if (clipAssembly is not null && envCellShellFilter is not null) _envCellRenderer?.Render(AcDream.App.Rendering.Wb.WbRenderPass.Transparent, envCellShellFilter); - // Phase U.3: close the world-geometry clip bracket opened above. From - // here down (particles, weather, debug lines, UI) the vertex shaders do - // NOT write gl_ClipDistance, so the planes must be OFF to avoid the - // undefined-behavior clip. + // Phase U.3: close the world-geometry clip bracket opened above. From here down the + // scene particles, debug lines, and UI use shaders that do NOT write gl_ClipDistance, so + // the planes must be OFF to avoid the undefined-behavior clip. (The weather/rain pass + // below DOES use sky.vert — it re-enables the planes in its OWN local bracket; the sky + // pre-scene pass above already did the same. Both are scissored to the doorway too.) for (int _cp = 0; _cp < ClipFrame.MaxPlanes; _cp++) _gl.Disable(EnableCap.ClipDistance0 + _cp); @@ -7574,6 +7582,13 @@ public sealed class GameWindow : IDisposable // outdoors). Suppressed in sealed dungeons / interiors with no exit portal in view. if (drawSkyThisFrame) { + // Scissor the WHOLE weather post-scene block (rain mesh + particles) to the doorway + // AABB when indoors — symmetric with the sky pre-scene block. The rain cylinder MESH + // is precisely clipped by sky.vert in Planes mode (scissor a harmless over-include); + // in Scissor mode (UBO count 0, no planes) the scissor is the ONLY confinement — else + // the 815m rain cylinder bleeds full-screen indoors. Outdoors → no scissor → unchanged. + bool wxSc = BeginDoorwayScissor(skyDoorwayClip, skyDoorwayNdc); + // Weather MESH (rain cylinder): re-bind binding=2 (the OutsideView UBO) defensively, // enable the 8 clip planes around RenderWeather, disable after. count==0 outdoors ⇒ // full-screen rain, unchanged. @@ -7586,15 +7601,12 @@ public sealed class GameWindow : IDisposable for (int _cp = 0; _cp < ClipFrame.MaxPlanes; _cp++) _gl.Disable(EnableCap.ClipDistance0 + _cp); - // SkyPostScene particles (particle.vert, no gl_ClipDistance) → scissor to the doorway - // bbox indoors, full-screen outdoors. + // SkyPostScene particles (particle.vert, no gl_ClipDistance) — confined by the scissor. if (_particleSystem is not null && _particleRenderer is not null) - { - bool sc = BeginDoorwayScissor(skyDoorwayClip, skyDoorwayNdc); _particleRenderer.Draw(_particleSystem, camera, camPos, AcDream.Core.Vfx.ParticleRenderPass.SkyPostScene); - if (sc) _gl.Disable(EnableCap.ScissorTest); - } + + if (wxSc) _gl.Disable(EnableCap.ScissorTest); } // Debug: draw collision shapes as wireframe cylinders around the