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; } /// Per-cell portal_view slots, in the same order retail setup_view(cell, i) /// selects them inside PView::DrawCells. public required IReadOnlyDictionary CellClipSlots { get; init; } public required int OutdoorSlot { get; init; } public required bool OutdoorVisible { 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 interior render flood, matching retail PView::DrawCells @ 0x005a4840: /// after the caller handles outside_view terrain + the depth-only clear, DrawCells /// walks cell_draw_list from the end back to zero in separate stages: cell shells, /// then each cell's object_list. The transparent shell pass is split out because /// the modern renderer batches opaque/transparent surfaces separately. /// 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) { // Retail Loop 2: DrawEnvCell for each drawable cell, farthest-to-nearest // (cell_draw_list[cell_draw_num - 1] down to 0). for (int i = ctx.OrderedVisibleCells.Count - 1; i >= 0; i--) { uint cellId = ctx.OrderedVisibleCells[i]; if (!TryBeginCell(ctx, cellId, out _)) continue; _oneCell.Clear(); _oneCell.Add(cellId); ApplyMembershipOnlyRouting(); _envCells.Render(WbRenderPass.Opaque, _oneCell); } // Retail Loop 3: Render::PortalList = cell->portal_view; DrawObjCellForDummies(cell). for (int i = ctx.OrderedVisibleCells.Count - 1; i >= 0; i--) { uint cellId = ctx.OrderedVisibleCells[i]; if (!TryBeginCell(ctx, cellId, out _)) continue; _oneCell.Clear(); _oneCell.Add(cellId); if (ctx.Partition.ByCell.TryGetValue(cellId, out var cellEntities) && cellEntities.Count > 0) { ApplyMembershipOnlyRouting(); DrawEntityBucket(ctx, cellEntities, visibleCellIds: _oneCell); } } // Modern split of DrawEnvCell's transparent/additive batches, same reverse cell order. for (int i = ctx.OrderedVisibleCells.Count - 1; i >= 0; i--) { uint cellId = ctx.OrderedVisibleCells[i]; if (!TryBeginCell(ctx, cellId, out _)) continue; _oneCell.Clear(); _oneCell.Add(cellId); ApplyMembershipOnlyRouting(); _envCells.Render(WbRenderPass.Transparent, _oneCell); } } private bool TryBeginCell(InteriorRenderContext ctx, uint cellId, out int[] slots) { if (ctx.DrawableCells.Contains(cellId)) { ctx.CellClipSlots.TryGetValue(cellId, out slots!); slots ??= System.Array.Empty(); return true; } slots = System.Array.Empty(); return false; } private void ApplyMembershipOnlyRouting() { // PView membership controls which cell shell/object bucket is visited. // Do not turn the 2D portal view into gl_ClipDistance for indoor meshes: // that slices avatars and shell triangles at stairs/doorways instead of // matching retail's DrawMesh view-check-then-draw behavior. _envCells.SetClipRouting(null); _entities.ClearClipRouting(); } // 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 objects; null only for fallback/outdoor buckets where clip-slot routing owns cull). // 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) => DrawEntityBucket( ctx.Camera, ctx.Frustum, ctx.PlayerLandblockId, ctx.AnimatedEntityIds, bucket, visibleCellIds); public void DrawEntityBucket( ICamera camera, FrustumPlanes? frustum, uint? playerLandblockId, HashSet? animatedEntityIds, 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 = playerLandblockId ?? 0u; var entry = (lbId, Vector3.Zero, Vector3.Zero, (IReadOnlyList)bucket, (IReadOnlyDictionary?)null); _entities.Draw( camera, new[] { entry }, frustum, neverCullLandblockId: playerLandblockId, visibleCellIds: visibleCellIds, animatedEntityIds: animatedEntityIds); } }