using System; using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering.Wb; using AcDream.Core.World; using Silk.NET.OpenGL; namespace AcDream.App.Rendering; /// /// App-layer port of the retail indoor render orchestration: /// SmartBox::RenderNormalMode -> RenderDeviceD3D::DrawInside -> /// PView::DrawInside -> ConstructView -> DrawCells. /// public sealed class RetailPViewRenderer { private readonly GL _gl; private readonly ClipFrame _clipFrame; private readonly EnvCellRenderer _envCells; private readonly WbDrawDispatcher _entities; private static readonly ClipViewSlice NoClipSlice = new(0, new Vector4(-1f, -1f, 1f, 1f), Array.Empty()); private readonly HashSet _oneCell = new(1); private readonly Dictionary _oneCellSlot = new(1); public RetailPViewRenderer( GL gl, ClipFrame clipFrame, EnvCellRenderer envCells, WbDrawDispatcher entities) { _gl = gl; _clipFrame = clipFrame; _envCells = envCells; _entities = entities; } public RetailPViewFrameResult DrawInside(RetailPViewDrawContext ctx) { ArgumentNullException.ThrowIfNull(ctx); var pvFrame = PortalVisibilityBuilder.Build( ctx.RootCell, ctx.ViewerEyePos, ctx.CellLookup, ctx.ViewProjection); var clipAssembly = ClipFrameAssembler.Assemble(_clipFrame, pvFrame); UploadClipFrame(ctx.SetTerrainClipUbo); // R1: draw EVERY visible cell (retail cell_draw_list), not only the cells the // assembler handed a clip-slot. This feeds the Prepare filter + entity partition, // so every visible cell's shell has a prepared batch and seals — killing the grey // (the old clipAssembly.CellIdToSlot.Keys filter silently dropped slot-less cells). // Per-slice trim still applies in DrawEnvCellShells (Task 4 makes it self-contained). var drawableCells = new HashSet(pvFrame.OrderedVisibleCells); UseIndoorMembershipOnlyRouting(); _envCells.PrepareRenderBatches( ctx.ViewProjection, ctx.CameraWorldPosition, filter: drawableCells, centerLbX: ctx.RenderCenterLbX, centerLbY: ctx.RenderCenterLbY, renderRadius: ctx.RenderRadius); var partition = InteriorEntityPartition.Partition(drawableCells, ctx.LandblockEntries); var result = new RetailPViewFrameResult { PortalFrame = pvFrame, ClipAssembly = clipAssembly, DrawableCells = drawableCells, Partition = partition, }; ctx.EmitDiagnostics?.Invoke(result); DrawLandscapeThroughOutsideView(ctx, clipAssembly, partition); UseIndoorMembershipOnlyRouting(); DrawExitPortalMasks(ctx, pvFrame, clipAssembly, drawableCells); DrawEnvCellShells(ctx, pvFrame, clipAssembly, drawableCells); DrawCellObjectLists(ctx, pvFrame, clipAssembly, drawableCells, partition); return result; } public RetailPViewFrameResult? DrawPortal(RetailPViewPortalDrawContext ctx) { ArgumentNullException.ThrowIfNull(ctx); var pvFrame = PortalVisibilityBuilder.BuildFromExterior( ctx.CandidateCells, ctx.ViewerEyePos, ctx.CellLookup, ctx.ViewProjection, ctx.MaxSeedDistance); if (pvFrame.OrderedVisibleCells.Count == 0) { RestoreNoClip(ctx.SetTerrainClipUbo); return null; } var clipAssembly = ClipFrameAssembler.Assemble(_clipFrame, pvFrame); UploadClipFrame(ctx.SetTerrainClipUbo); var drawableCells = new HashSet(clipAssembly.CellIdToSlot.Keys); UseIndoorMembershipOnlyRouting(); _envCells.PrepareRenderBatches( ctx.ViewProjection, ctx.CameraWorldPosition, filter: drawableCells, centerLbX: ctx.RenderCenterLbX, centerLbY: ctx.RenderCenterLbY, renderRadius: ctx.RenderRadius); var partition = InteriorEntityPartition.Partition(drawableCells, ctx.LandblockEntries); var result = new RetailPViewFrameResult { PortalFrame = pvFrame, ClipAssembly = clipAssembly, DrawableCells = drawableCells, Partition = partition, }; ctx.EmitDiagnostics?.Invoke(result); DrawExitPortalMasks(ctx, pvFrame, clipAssembly, drawableCells); DrawEnvCellShells(ctx, pvFrame, clipAssembly, drawableCells); DrawCellObjectLists(ctx, pvFrame, clipAssembly, drawableCells, partition); RestoreNoClip(ctx.SetTerrainClipUbo); return result; } private void DrawLandscapeThroughOutsideView( RetailPViewDrawContext ctx, ClipFrameAssembly clipAssembly, InteriorEntityPartition.Result partition) { if (clipAssembly.OutsideViewSlices.Length == 0) return; foreach (var slice in clipAssembly.OutsideViewSlices) { _clipFrame.SetTerrainClip(slice.Planes); UploadClipFrame(ctx.SetTerrainClipUbo); _entities.SetClipRouting(clipAssembly.CellIdToSlot, slice.Slot, outdoorVisible: true); ctx.DrawLandscapeSlice(new RetailPViewLandscapeSliceContext(slice, partition.Outdoor)); } foreach (var slice in clipAssembly.OutsideViewSlices) ctx.ClearDepthSlice?.Invoke(slice); UseIndoorMembershipOnlyRouting(); } private void DrawExitPortalMasks( IRetailPViewCellDrawCallbacks ctx, PortalVisibilityFrame pvFrame, ClipFrameAssembly clipAssembly, HashSet drawableCells) { if (ctx.DrawExitPortalMasks is null) return; for (int i = pvFrame.OrderedVisibleCells.Count - 1; i >= 0; i--) { uint cellId = pvFrame.OrderedVisibleCells[i]; if (!drawableCells.Contains(cellId)) continue; foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId)) ctx.DrawExitPortalMasks(new RetailPViewCellSliceContext(cellId, slice, Array.Empty())); } } private void DrawEnvCellShells( IRetailPViewCellDrawCallbacks ctx, PortalVisibilityFrame pvFrame, ClipFrameAssembly clipAssembly, HashSet drawableCells) // param kept this task; removed in Task 4 { // Retail DrawCells Loop 2: every visible cell's shell, reverse cell_draw_list // (far→near), per portal_view slice. No drawableCells filter — a cell without a // clip-slot falls through GetCellSlicesOrNoClip to NoClipSlice and draws unclipped // (sealed; per-slice trim returns in Task 4). foreach (var entry in IndoorDrawPlan.ShellPass(pvFrame)) { uint cellId = entry.CellId; _oneCell.Clear(); _oneCell.Add(cellId); foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId)) { UseShellClipRouting(cellId, slice); _envCells.Render(WbRenderPass.Opaque, _oneCell); _envCells.Render(WbRenderPass.Transparent, _oneCell); } } } private void DrawCellObjectLists( IRetailPViewCellDrawContext ctx, PortalVisibilityFrame pvFrame, ClipFrameAssembly clipAssembly, HashSet drawableCells, InteriorEntityPartition.Result partition) { for (int i = pvFrame.OrderedVisibleCells.Count - 1; i >= 0; i--) { uint cellId = pvFrame.OrderedVisibleCells[i]; if (!drawableCells.Contains(cellId)) continue; if (!partition.ByCell.TryGetValue(cellId, out var bucket) || bucket.Count == 0) continue; _oneCell.Clear(); _oneCell.Add(cellId); UseIndoorMembershipOnlyRouting(); DrawEntityBucket(ctx, bucket, _oneCell); foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId)) ctx.DrawCellParticles?.Invoke(new RetailPViewCellSliceContext(cellId, slice, bucket)); } } private static ClipViewSlice[] GetCellSlicesOrNoClip( ClipFrameAssembly clipAssembly, uint cellId) { if (clipAssembly.CellIdToViewSlices.TryGetValue(cellId, out var slices) && slices.Length > 0) return slices; return new[] { NoClipSlice }; } private void UseIndoorMembershipOnlyRouting() { // Retail's PView portal views decide which cells/objects are eligible, // but DrawMesh only performs portal-view visibility checks before drawing // the mesh. Feeding those 2D views into gl_ClipDistance slices characters // and cell shells at stair/door boundaries, which retail does not do. _envCells.SetClipRouting(null); _entities.ClearClipRouting(); } private void UseShellClipRouting(uint cellId, ClipViewSlice slice) { _oneCellSlot.Clear(); _oneCellSlot[cellId] = slice.Slot; _envCells.SetClipRouting(_oneCellSlot); _entities.ClearClipRouting(); } private void DrawEntityBucket( IRetailPViewCellDrawContext ctx, IReadOnlyList bucket, HashSet? visibleCellIds) { 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); } private void RestoreNoClip(Action setTerrainClipUbo) { _clipFrame.Reset(); UploadClipFrame(setTerrainClipUbo); UseIndoorMembershipOnlyRouting(); } private void UploadClipFrame(Action setTerrainClipUbo) { _clipFrame.UploadShared(_gl); _entities.SetClipRegionSsbo(_clipFrame.RegionSsbo); _envCells.SetClipRegionSsbo(_clipFrame.RegionSsbo); setTerrainClipUbo(_clipFrame.TerrainUbo); } } public interface IRetailPViewCellDrawCallbacks { public Action? DrawExitPortalMasks { get; } public Action? DrawCellParticles { get; } } public interface IRetailPViewCellDrawContext : IRetailPViewCellDrawCallbacks { public ICamera Camera { get; } public FrustumPlanes? Frustum { get; } public uint? PlayerLandblockId { get; } public HashSet? AnimatedEntityIds { get; } } public sealed class RetailPViewDrawContext : IRetailPViewCellDrawContext { public required LoadedCell RootCell { get; init; } public required Vector3 ViewerEyePos { get; init; } public required Matrix4x4 ViewProjection { get; init; } public required Func CellLookup { get; init; } public required ICamera Camera { get; init; } public required Vector3 CameraWorldPosition { get; init; } public required FrustumPlanes? Frustum { get; init; } public required uint? PlayerLandblockId { get; init; } public required HashSet? AnimatedEntityIds { get; init; } public required int RenderCenterLbX { get; init; } public required int RenderCenterLbY { get; init; } public required int RenderRadius { get; init; } public required IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax, IReadOnlyList Entities, IReadOnlyDictionary? AnimatedById)> LandblockEntries { get; init; } public required Action SetTerrainClipUbo { get; init; } public required Action DrawLandscapeSlice { get; init; } public Action? ClearDepthSlice { get; init; } public Action? DrawExitPortalMasks { get; init; } public Action? DrawCellParticles { get; init; } public Action? EmitDiagnostics { get; init; } } public sealed class RetailPViewPortalDrawContext : IRetailPViewCellDrawContext { public required IEnumerable CandidateCells { get; init; } public required Vector3 ViewerEyePos { get; init; } public required Matrix4x4 ViewProjection { get; init; } public required Func CellLookup { get; init; } public required ICamera Camera { get; init; } public required Vector3 CameraWorldPosition { get; init; } public required FrustumPlanes? Frustum { get; init; } public required uint? PlayerLandblockId { get; init; } public required HashSet? AnimatedEntityIds { get; init; } public required int RenderCenterLbX { get; init; } public required int RenderCenterLbY { get; init; } public required int RenderRadius { get; init; } public required float MaxSeedDistance { get; init; } public required IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax, IReadOnlyList Entities, IReadOnlyDictionary? AnimatedById)> LandblockEntries { get; init; } public required Action SetTerrainClipUbo { get; init; } public Action? DrawExitPortalMasks { get; init; } public Action? DrawCellParticles { get; init; } public Action? EmitDiagnostics { get; init; } } public sealed class RetailPViewFrameResult { public required PortalVisibilityFrame PortalFrame { get; init; } public required ClipFrameAssembly ClipAssembly { get; init; } public required HashSet DrawableCells { get; init; } public required InteriorEntityPartition.Result Partition { get; init; } } public readonly record struct RetailPViewLandscapeSliceContext( ClipViewSlice Slice, IReadOnlyList OutdoorEntities); public readonly record struct RetailPViewCellSliceContext( uint CellId, ClipViewSlice Slice, IReadOnlyList CellEntities);