T3 (BR-5): viewconeCheck port - per-view sphere culling for statics/dynamics/particles, weather player-gate, unattached outdoor emitters

Ports Render::viewconeCheck (Ghidra 0x0054c250): meshes are sphere-CULLED
per portal view, never hard-clipped. NEW ViewconeCuller lifts each
slice's <=8 clip-space half-planes to world-space eye-edge planes (the
view_vertex.plane analog, acclient.h:32483 - one matrix fold: L = VP
rows . P) and tests bounding spheres from the entity's cached AABB (the
dispatcher's own cull bounds source).

Gating now matches retail's shape end to end:
- Per-cell STATICS: sphere vs THEIR CELL's views - the statics-through-
  walls fix (the cottage phantom staircase's actual draw path: a static
  outside every view of its cell no longer paints through the wall).
- DYNAMICS (last pass): sphere vs their cell's views; outdoor/unresolved
  vs the outside views (pass-all under the outdoor root). A dynamic in a
  non-flooded room culls HERE - retail never reaches an object whose cell
  is not in the draw list; the partition still routes it so the CULL is
  what drops it, retail's shape exactly.
- OutdoorStatic (landscape pass): pre-filtered per outside slice; the
  per-slice entity gl_ClipDistance routing is DELETED (entities draw
  outside the clip bracket; terrain/sky keep their plane clip).
- PARTICLES: the scissor-AABB gate is DELETED; emitters gate through
  their cone-surviving owners (candle-flames-through-walls fix).
- WEATHER: gated on the PLAYER being outside (retail is_player_outside -
  an indoor player gets no rain even looking out a doorway). Closes
  weather-gate-player-vs-viewer.
- UNATTACHED emitters (campfires) get their missing outdoor-root pass
  (closes unattached-particles-dropped-outdoors).

Suites: App 226 green (flood gates included), Core baseline unchanged
(1398 + 4 pre-existing #99-era).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-11 12:56:48 +02:00
parent 88f3ce1fa0
commit a6aec8c32f
3 changed files with 268 additions and 24 deletions

View file

@ -7646,6 +7646,7 @@ public sealed class GameWindow : IDisposable
playerLb,
animatedIds,
renderSky,
renderWeather: playerSeenOutside,
kf,
environOverrideActive),
// T1: retail's depth discipline (PView::DrawCells, Ghidra 0x005a4840).
@ -7877,6 +7878,25 @@ public sealed class GameWindow : IDisposable
AcDream.Core.Vfx.ParticleRenderPass.Scene);
}
}
else if (clipRoot is { IsOutdoorNode: true }
&& _particleSystem is not null && _particleRenderer is not null)
{
// T3 (BR-5): unattached emitters (campfires, ground effects —
// AttachedObjectId == 0) under the OUTDOOR root. The unified
// path's attached emitters draw via the landscape slice + the
// per-cell callbacks; unattached ones had NO pass on
// outdoor-node frames (the unattached-particles-dropped-
// outdoors divergence, adjusted-confirmed). The outdoor root's
// outside view is full-screen (cone pass-all); depth test
// composites them against the world.
sigSceneParticles = sigSceneParticles == "none" ? "unattached" : sigSceneParticles + "+unattached";
_particleRenderer.Draw(
_particleSystem,
camera,
camPos,
AcDream.Core.Vfx.ParticleRenderPass.Scene,
emitter => emitter.AttachedObjectId == 0);
}
// Bug A fix (post-#26 worktree, 2026-04-26): weather sky
// Outdoor LScape post-scene weather. Indoor weather through an exit portal is
@ -9480,6 +9500,7 @@ public sealed class GameWindow : IDisposable
uint? playerLb,
HashSet<uint>? animatedIds,
bool renderSky,
bool renderWeather,
AcDream.Core.World.SkyKeyframe kf,
bool environOverrideActive)
{
@ -9510,6 +9531,11 @@ public sealed class GameWindow : IDisposable
_terrainCpuSampleCursor = (_terrainCpuSampleCursor + 1) % _terrainCpuSamples.Length;
MaybeFlushTerrainDiag();
// T3 (BR-5): entities draw OUTSIDE the clip bracket — retail meshes
// are viewcone-CHECKED, never hard-clipped (Ghidra 0x0054c250); the
// sphere pre-filter already ran in RetailPViewRenderer (OutdoorEntities
// is the per-slice survivor set).
DisableClipDistances();
if (sliceCtx.OutdoorEntities.Count > 0)
{
var sceneryEntry = (playerLb ?? 0u, System.Numerics.Vector3.Zero, System.Numerics.Vector3.Zero,
@ -9539,8 +9565,13 @@ public sealed class GameWindow : IDisposable
&& _outdoorSceneParticleEntityIds.Contains(emitter.AttachedObjectId));
}
// T3 (BR-5): weather gates on the PLAYER being outside, not the viewer
// root — retail draws weather only when is_player_outside (the rain
// cylinder rides the player; an indoor player gets NO rain even while
// looking out a doorway). Closes the rain-through-doorways divergence
// (weather-gate-player-vs-viewer, adjusted-confirmed).
EnableClipDistances();
if (renderSky)
if (renderSky && renderWeather)
{
_skyRenderer?.RenderWeather(camera, camPos, (float)WorldTime.DayFraction,
_activeDayGroup, kf, environOverrideActive);
@ -9620,8 +9651,12 @@ public sealed class GameWindow : IDisposable
if (_visibleSceneParticleEntityIds.Count == 0)
return;
// T3 (BR-5): the scissor-AABB gate is DELETED — retail gates particles
// like meshes (viewcone on the owner; depth does the pixels). The
// CellEntities set is already the cone-surviving owner list, so the
// id-predicate below IS the cone gate; the punch/seal depth discipline
// composites the pixels.
DisableClipDistances();
bool scissor = BeginDoorwayScissor(true, sliceCtx.Slice.NdcAabb);
_particleRenderer.Draw(
_particleSystem,
camera,
@ -9629,8 +9664,6 @@ public sealed class GameWindow : IDisposable
AcDream.Core.Vfx.ParticleRenderPass.Scene,
emitter => emitter.AttachedObjectId != 0
&& _visibleSceneParticleEntityIds.Contains(emitter.AttachedObjectId));
if (scissor)
_gl!.Disable(EnableCap.ScissorTest);
DisableClipDistances();
}