From 4b75c68ea36e4f341d7afbb0f3b39e7c69705f34 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 2 Jun 2026 19:54:42 +0200 Subject: [PATCH] =?UTF-8?q?feat(render):=20R1=20=E2=80=94=20InteriorRender?= =?UTF-8?q?er=20per-cell=20DrawInside=20loop=20(retail=20PView::DrawCells)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-cell flood: closest-first over OrderedVisibleCells, per cell draws the closed shell (EnvCellRenderer.Render(pass,{cellId})) + that cell's objects, then live- dynamics unclipped, then transparent shells. Reuses the existing dispatcher Draw per cell (safe to call N x/frame; only diagnostic GPU-timing miscounts). Caller owns the landscape-through-door + Z-clear. Not yet wired (Task 3). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AcDream.App/Rendering/InteriorRenderer.cs | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 src/AcDream.App/Rendering/InteriorRenderer.cs diff --git a/src/AcDream.App/Rendering/InteriorRenderer.cs b/src/AcDream.App/Rendering/InteriorRenderer.cs new file mode 100644 index 0000000..ec0dd26 --- /dev/null +++ b/src/AcDream.App/Rendering/InteriorRenderer.cs @@ -0,0 +1,102 @@ +using System.Collections.Generic; +using System.Numerics; +using AcDream.App.Rendering.Wb; +using AcDream.Core.World; + +namespace AcDream.App.Rendering; + +/// Per-frame inputs for one flood. +public sealed class InteriorRenderContext +{ + /// Visible cells, closest-first (retail cell_draw_list). From PortalVisibilityFrame. + public required IReadOnlyList OrderedVisibleCells { get; init; } + + /// The 3-bucket entity split (). Only ByCell + + /// LiveDynamic are used here; Outdoor scenery is drawn by the caller's landscape-through-door + /// step (clipped to OutsideView). + public required InteriorEntityPartition.Result Partition { get; init; } + + public required ICamera Camera { get; init; } + public required FrustumPlanes? Frustum { get; init; } + + /// 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. + public required uint? PlayerLandblockId { get; init; } + + public required HashSet? AnimatedEntityIds { get; init; } +} + +/// +/// 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). +/// +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 _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) + { + _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) + { + _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 bucket, HashSet? 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)bucket, + (IReadOnlyDictionary?)null); + + _entities.Draw( + ctx.Camera, + new[] { entry }, + ctx.Frustum, + neverCullLandblockId: ctx.PlayerLandblockId, + visibleCellIds: visibleCellIds, + animatedEntityIds: ctx.AnimatedEntityIds); + } +}