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 cells the assembler mapped a clip slot for (ClipFrameAssembly.CellIdToSlot.Keys = /// the GameWindow envCellShellFilter). A cell may appear in but /// reduce to IsNothingVisible in the assembler (no slot) — those are skipped. This is the /// membership filter; supplies the draw ORDER. public required IReadOnlySet DrawableCells { 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) { 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 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); } }