feat(render): R1 — binary render decision, indoor = per-cell DrawInside only
GameWindow.OnRender: when clipRoot != null, run only InteriorRenderer.DrawInside (per-cell shells + per-cell objects + live-dynamics); the global entity pass + global shell pass are no longer issued indoors. Outdoor scenery drawn clipped to the doorway (after terrain, before the Z-clear). Outdoor root path unchanged. pvFrame hoisted so the splice reads OrderedVisibleCells; per-frame 3-bucket partition built on the indoor root. Retail RenderNormalMode @ 0x453aa0. InteriorRenderer amended with a DrawableCells membership filter (an IsNothingVisible cell can be in OrderedVisibleCells but absent from CellIdToSlot — iterate for ORDER, filter for membership; matches the old envCellShellFilter set exactly). Build green, 174/174 App tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4b75c68ea3
commit
c4fd71149a
2 changed files with 72 additions and 26 deletions
|
|
@ -166,6 +166,12 @@ public sealed class GameWindow : IDisposable
|
|||
private AcDream.App.Rendering.Wb.EnvCellRenderer? _envCellRenderer;
|
||||
private AcDream.App.Rendering.Wb.WbFrustum? _envCellFrustum;
|
||||
|
||||
// R1 (render redesign): the per-cell DrawInside flood + its per-frame entity partition.
|
||||
// _interiorRenderer is constructed once both renderers exist; _interiorPartition is rebuilt
|
||||
// each frame on an indoor root (null on the outdoor root).
|
||||
private AcDream.App.Rendering.InteriorRenderer? _interiorRenderer;
|
||||
private AcDream.App.Rendering.InteriorEntityPartition.Result? _interiorPartition;
|
||||
|
||||
// Phase U.3: the shared per-frame clip data (binding=2 mesh SSBO + terrain
|
||||
// UBO). In U.3 a single ClipFrame.NoClip() instance is created lazily (??=) and
|
||||
// REUSED across frames — its GL buffers persist; only the cheap CPU-side no-clip
|
||||
|
|
@ -1796,6 +1802,9 @@ public sealed class GameWindow : IDisposable
|
|||
_envCellRenderer = new AcDream.App.Rendering.Wb.EnvCellRenderer(
|
||||
_gl, _wbMeshAdapter!.MeshManager!, _envCellFrustum);
|
||||
_envCellRenderer.Initialize(_meshShader!);
|
||||
|
||||
// R1: the per-cell DrawInside flood. Both renderers exist here (just constructed).
|
||||
_interiorRenderer = new AcDream.App.Rendering.InteriorRenderer(_envCellRenderer!, _wbDrawDispatcher!);
|
||||
}
|
||||
|
||||
// Phase G.1 sky renderer — its own shader (sky.vert / sky.frag)
|
||||
|
|
@ -7305,6 +7314,7 @@ public sealed class GameWindow : IDisposable
|
|||
_clipFrame ??= ClipFrame.NoClip();
|
||||
var clipRoot = visibility?.CameraCell;
|
||||
ClipFrameAssembly? clipAssembly = null;
|
||||
PortalVisibilityFrame? pvFrame = null; // R1: hoisted so the binary decision below reads OrderedVisibleCells
|
||||
var terrainClipMode = TerrainClipMode.Planes; // overwritten below for indoor root
|
||||
System.Numerics.Vector4 terrainScissorNdc = default;
|
||||
HashSet<uint>? envCellShellFilter = null; // drawable visible cells (cellIdToSlot keys)
|
||||
|
|
@ -7313,7 +7323,7 @@ public sealed class GameWindow : IDisposable
|
|||
// Phase U.4c: side test + distance ordering use the PLAYER position (visRootPos,
|
||||
// stable inside the cell); projection uses the eye's envCellViewProj (the screen
|
||||
// view). See the visRootPos rationale at the ComputeVisibility call above.
|
||||
var pvFrame = PortalVisibilityBuilder.Build(
|
||||
pvFrame = PortalVisibilityBuilder.Build(
|
||||
clipRoot,
|
||||
visRootPos,
|
||||
id => _cellVisibility.TryGetCell(id, out var c) ? c : null,
|
||||
|
|
@ -7332,6 +7342,12 @@ public sealed class GameWindow : IDisposable
|
|||
// map's keys; IsNothingVisible cells were excluded by the assembler).
|
||||
envCellShellFilter = new HashSet<uint>(clipAssembly.CellIdToSlot.Keys);
|
||||
|
||||
// R1: partition this frame's entities into per-cell / outdoor / live-dynamic buckets
|
||||
// for the DrawInside flood + the outdoor-scenery-through-door draw. Keyed by the SAME
|
||||
// visible-cell set the shells use (cellIdToSlot.Keys).
|
||||
_interiorPartition = AcDream.App.Rendering.InteriorEntityPartition.Partition(
|
||||
envCellShellFilter, _worldState.LandblockEntries);
|
||||
|
||||
// [vis] probe (ACDREAM_PROBE_VIS=1) — the real PortalVisibilityFrame
|
||||
// numbers, replacing the old camera-state-only spike. Cell-change
|
||||
// throttled inside EmitVis so launch.log stays readable under motion.
|
||||
|
|
@ -7368,6 +7384,7 @@ public sealed class GameWindow : IDisposable
|
|||
_clipFrame.Reset();
|
||||
_wbDrawDispatcher?.ClearClipRouting();
|
||||
_envCellRenderer?.SetClipRouting(null);
|
||||
_interiorPartition = null; // R1: no indoor flood on the outdoor root
|
||||
}
|
||||
|
||||
_clipFrame.UploadShared(_gl);
|
||||
|
|
@ -7512,6 +7529,22 @@ public sealed class GameWindow : IDisposable
|
|||
animatedIds.Add(k);
|
||||
}
|
||||
|
||||
// R1: outdoor scenery (ParentCellId == null) is part of the landscape seen through the
|
||||
// doorway (retail LScape::draw draws the exterior, clipped to OutsideView). Drawn here —
|
||||
// after terrain, BEFORE the Z-clear — only on an indoor root, scoped to the outdoor bucket.
|
||||
// ResolveEntitySlot routes these (ParentCellId == null) to OutdoorSlot when OutdoorVisible,
|
||||
// else CULLs them, via the SetClipRouting installed above. visibleCellIds: null ⇒ they pass
|
||||
// the membership gate (no cell filter) and are gated purely by the clip slot.
|
||||
if (clipAssembly is not null && _interiorPartition is not null
|
||||
&& _interiorPartition.Outdoor.Count > 0 && clipAssembly.OutdoorVisible)
|
||||
{
|
||||
var sceneryEntry = (playerLb ?? 0u, System.Numerics.Vector3.Zero, System.Numerics.Vector3.Zero,
|
||||
(IReadOnlyList<AcDream.Core.World.WorldEntity>)_interiorPartition.Outdoor,
|
||||
(IReadOnlyDictionary<uint, AcDream.Core.World.WorldEntity>?)null);
|
||||
_wbDrawDispatcher!.Draw(camera, new[] { sceneryEntry }, frustum,
|
||||
neverCullLandblockId: playerLb, visibleCellIds: null, animatedEntityIds: animatedIds);
|
||||
}
|
||||
|
||||
// ── [Stage 4] conditional doorway Z-clear ───────────────────────────────────
|
||||
// Retail PView::DrawCells @ pseudo_c:432731: after the landscape (sky + terrain) is drawn
|
||||
// through the exit portal, RenderDevice->Clear(flag 4 = Z-BUFFER ONLY, NOT color) resets
|
||||
|
|
@ -7527,31 +7560,36 @@ public sealed class GameWindow : IDisposable
|
|||
if (_zc) _gl.Disable(EnableCap.ScissorTest);
|
||||
}
|
||||
|
||||
// Phase U.4: render the indoor cell SHELLS (walls / floors / ceilings)
|
||||
// — previously DORMANT (EnvCellRenderer.Render was never called in the
|
||||
// live loop). Inside the clip bracket so each cell's instances are gated
|
||||
// to its CellClip slot via the binding=3 map we installed above. Opaque
|
||||
// pass BEFORE the entity dispatcher (front-to-back, depth writes on);
|
||||
// Transparent pass AFTER. Filter = the drawable visible cells. Only when
|
||||
// there's an indoor root (clipAssembly != null) — outdoor frames draw no
|
||||
// shells. PrepareRenderBatches already ran earlier this frame.
|
||||
if (clipAssembly is not null && envCellShellFilter is not null)
|
||||
_envCellRenderer?.Render(AcDream.App.Rendering.Wb.WbRenderPass.Opaque, envCellShellFilter);
|
||||
|
||||
// Scene entity draw. N.5: WbDrawDispatcher is always non-null
|
||||
// (modern path mandatory). Default EntitySet.All — every entity
|
||||
// walked, gated only by the ParentCellId ∈ visibleCellIds filter.
|
||||
// Phase U.4: per-instance clip slots come from SetClipRouting above
|
||||
// (indoor root) or ClearClipRouting (outdoor root → every instance slot 0).
|
||||
_wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum,
|
||||
neverCullLandblockId: playerLb,
|
||||
visibleCellIds: visibility?.VisibleCellIds,
|
||||
animatedEntityIds: animatedIds);
|
||||
|
||||
// Phase U.4: cell shells transparent pass (additive / alpha-blend cell
|
||||
// surfaces, e.g. stained glass). Still inside the clip bracket.
|
||||
if (clipAssembly is not null && envCellShellFilter is not null)
|
||||
_envCellRenderer?.Render(AcDream.App.Rendering.Wb.WbRenderPass.Transparent, envCellShellFilter);
|
||||
// R1 — the binary render decision (retail RenderNormalMode @ 0x453aa0):
|
||||
// INDOOR root (clipRoot != null): run ONLY the per-cell DrawInside flood. The global
|
||||
// entity pass + global shell pass are NOT issued — visibility IS the cull, so the
|
||||
// outdoor world cannot bleed (it is never iterated; outdoor scenery entered above,
|
||||
// clipped to the doorway). DrawInside draws per-cell shells (opaque + transparent) +
|
||||
// per-cell objects + live-dynamics, closest-first over the drawable visible cells.
|
||||
// OUTDOOR root: the existing global entity pass (no shells, no DrawInside).
|
||||
if (clipRoot is not null && _interiorRenderer is not null
|
||||
&& _interiorPartition is not null && envCellShellFilter is not null)
|
||||
{
|
||||
var interiorCtx = new AcDream.App.Rendering.InteriorRenderContext
|
||||
{
|
||||
OrderedVisibleCells = pvFrame!.OrderedVisibleCells,
|
||||
DrawableCells = envCellShellFilter,
|
||||
Partition = _interiorPartition,
|
||||
Camera = camera,
|
||||
Frustum = frustum,
|
||||
PlayerLandblockId = playerLb,
|
||||
AnimatedEntityIds = animatedIds,
|
||||
};
|
||||
_interiorRenderer.DrawInside(interiorCtx);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Outdoor root: the global entity pass (unchanged).
|
||||
_wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum,
|
||||
neverCullLandblockId: playerLb,
|
||||
visibleCellIds: visibility?.VisibleCellIds,
|
||||
animatedEntityIds: animatedIds);
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -11,6 +11,12 @@ public sealed class InteriorRenderContext
|
|||
/// <summary>Visible cells, closest-first (retail cell_draw_list). From PortalVisibilityFrame.</summary>
|
||||
public required IReadOnlyList<uint> OrderedVisibleCells { get; init; }
|
||||
|
||||
/// <summary>The cells the assembler mapped a clip slot for (ClipFrameAssembly.CellIdToSlot.Keys =
|
||||
/// the GameWindow envCellShellFilter). A cell may appear in <see cref="OrderedVisibleCells"/> but
|
||||
/// reduce to IsNothingVisible in the assembler (no slot) — those are skipped. This is the
|
||||
/// membership filter; <see cref="OrderedVisibleCells"/> supplies the draw ORDER.</summary>
|
||||
public required IReadOnlySet<uint> DrawableCells { get; init; }
|
||||
|
||||
/// <summary>The 3-bucket entity split (<see cref="InteriorEntityPartition"/>). Only ByCell +
|
||||
/// LiveDynamic are used here; Outdoor scenery is drawn by the caller's landscape-through-door
|
||||
/// step (clipped to OutsideView).</summary>
|
||||
|
|
@ -54,6 +60,7 @@ public sealed class InteriorRenderer
|
|||
// Loop A — per-cell OPAQUE shell + that cell's static objects (closest-first).
|
||||
foreach (uint cellId in ctx.OrderedVisibleCells)
|
||||
{
|
||||
if (!ctx.DrawableCells.Contains(cellId)) continue; // no clip slot ⇒ assembler culled it
|
||||
_oneCell.Clear();
|
||||
_oneCell.Add(cellId);
|
||||
_envCells.Render(WbRenderPass.Opaque, _oneCell);
|
||||
|
|
@ -70,6 +77,7 @@ public sealed class InteriorRenderer
|
|||
// Loop B — per-cell TRANSPARENT shells (stained glass / additive cell surfaces).
|
||||
foreach (uint cellId in ctx.OrderedVisibleCells)
|
||||
{
|
||||
if (!ctx.DrawableCells.Contains(cellId)) continue;
|
||||
_oneCell.Clear();
|
||||
_oneCell.Add(cellId);
|
||||
_envCells.Render(WbRenderPass.Transparent, _oneCell);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue