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>
110 lines
5.4 KiB
C#
110 lines
5.4 KiB
C#
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using AcDream.App.Rendering.Wb;
|
|
using AcDream.Core.World;
|
|
|
|
namespace AcDream.App.Rendering;
|
|
|
|
/// <summary>Per-frame inputs for one <see cref="InteriorRenderer.DrawInside"/> flood.</summary>
|
|
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>
|
|
public required InteriorEntityPartition.Result Partition { get; init; }
|
|
|
|
public required ICamera Camera { get; init; }
|
|
public required FrustumPlanes? Frustum { get; init; }
|
|
|
|
/// <summary>The full FFFF-suffixed landblock id of the player. Used as BOTH the synthetic
|
|
/// per-cell entry id AND neverCullLandblockId so the degenerate (zero) synthetic AABB is never
|
|
/// landblock-culled — per-entity frustum culling inside Draw still applies.</summary>
|
|
public required uint? PlayerLandblockId { get; init; }
|
|
|
|
public required HashSet<uint>? AnimatedEntityIds { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The per-cell interior render flood — a faithful port of retail PView::DrawCells' per-cell loops
|
|
/// (decomp 0x5a4840). Iterates the visible cells closest-first; per cell draws the closed shell +
|
|
/// that cell's static objects (portal-clipped via the clip routing the caller installed), then the
|
|
/// live-dynamics unclipped, then the transparent shells. The landscape-through-door (sky/terrain/
|
|
/// scenery) + the conditional Z-clear are the caller's responsibility, run BEFORE this. GL state is
|
|
/// self-contained inside each renderer (EnvCellRenderer / WbDrawDispatcher set their own).
|
|
/// </summary>
|
|
public sealed class InteriorRenderer
|
|
{
|
|
private readonly EnvCellRenderer _envCells;
|
|
private readonly WbDrawDispatcher _entities;
|
|
|
|
// Reused single-cell filter set — cleared + repopulated per cell to avoid per-frame allocs.
|
|
private readonly HashSet<uint> _oneCell = new(1);
|
|
|
|
public InteriorRenderer(EnvCellRenderer envCells, WbDrawDispatcher entities)
|
|
{
|
|
_envCells = envCells;
|
|
_entities = entities;
|
|
}
|
|
|
|
public void DrawInside(InteriorRenderContext ctx)
|
|
{
|
|
// 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);
|
|
|
|
if (ctx.Partition.ByCell.TryGetValue(cellId, out var cellEntities) && cellEntities.Count > 0)
|
|
DrawEntityBucket(ctx, cellEntities, visibleCellIds: _oneCell);
|
|
}
|
|
|
|
// Live-dynamics (player / NPCs): unclipped (serverGuid != 0 → clip slot 0), depth-tested.
|
|
// Drawn AFTER opaque shells so wall depth occludes them correctly.
|
|
if (ctx.Partition.LiveDynamic.Count > 0)
|
|
DrawEntityBucket(ctx, ctx.Partition.LiveDynamic, visibleCellIds: null);
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
// Draws one bucket of entities via the existing dispatcher, scoped to a synthetic single-entry
|
|
// landblock list. visibleCellIds gates which entities pass the cell-membership walk (a single-cell
|
|
// set for per-cell statics; null for live-dynamics — they pass the gate and resolve to slot 0).
|
|
// The clip slot per entity comes from the SetClipRouting the caller installed (cellIdToSlot +
|
|
// outdoorSlot + outdoorVisible) via ResolveEntitySlot.
|
|
private void DrawEntityBucket(
|
|
InteriorRenderContext ctx, IReadOnlyList<WorldEntity> bucket, HashSet<uint>? visibleCellIds)
|
|
{
|
|
// LandblockId == neverCullLandblockId (PlayerLandblockId) ⇒ the degenerate (zero) AABB is
|
|
// never landblock-frustum-culled; per-entity AABB culling inside Draw still applies.
|
|
uint lbId = ctx.PlayerLandblockId ?? 0u;
|
|
var entry = (lbId, Vector3.Zero, Vector3.Zero,
|
|
(IReadOnlyList<WorldEntity>)bucket,
|
|
(IReadOnlyDictionary<uint, WorldEntity>?)null);
|
|
|
|
_entities.Draw(
|
|
ctx.Camera,
|
|
new[] { entry },
|
|
ctx.Frustum,
|
|
neverCullLandblockId: ctx.PlayerLandblockId,
|
|
visibleCellIds: visibleCellIds,
|
|
animatedEntityIds: ctx.AnimatedEntityIds);
|
|
}
|
|
}
|