diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 36b10937..4de6e2ca 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -4635,15 +4635,27 @@ the landscape stage is now TWO phases per frame — EARLY per slice: sky, terrain, outdoor static meshes (the look-in punches need their depth, the #117 lesson); then the #124 look-ins; then LATE per slice: outside-stage dynamics' meshes + ALL attached scene particles + weather + the -unattached pass. Outdoor roots keep their existing order (no look-ins in -the stage; net order unchanged). Residual (documented, AP-34): under -OUTDOOR roots slice particles still draw before merged building -interiors (the outdoor sibling of this bug, unreported); building -exteriors' own translucent batches draw early. +unattached pass. (This FIXED #132 indoors but not the portal.) + +**ROOT CAUSE (fix 3 — the real one, pinned by the teleport capture):** +walking into the portal flipped `pCell` to **0xA9B4017A — the hall's +porch EnvCell**. The portal's swirl emitter is owned by a STATIC inside +ANOTHER BUILDING'S CELL, not an outdoor entity at all. Outdoors the +hall's cells merge into the main frame and the per-cell object pass +runs `DrawCellParticles` → swirl visible. Under an interior root the +#124 look-in sub-pass drew the far cells' shells + statics but had NO +cell-particles call — retail's nested DrawCells draws objects WITH +their emitters (`DrawObjCellForDummies`). Every earlier suspect +(unattached pass, owner-cone verdicts, alpha ordering) was real-but- +adjacent; the cone math was correct all along (the 0xC0A9B462 flips +were a porch torch, geometrically defensible). + +**Fix 3:** `DrawBuildingLookIns` pass 2 invokes `DrawCellParticles` per +look-in cell with its static bucket (same callback as the main per-cell +pass; no-clip slice when the cell has no slot). **Gate:** stand inside, look out the doorway at the town portal — the -swirl renders through the door; the candle flame (#132) stays visible -with the through-opening behind it. +swirl renders through the door. --- diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs index f23d419c..5a305809 100644 --- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs +++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs @@ -308,7 +308,10 @@ public sealed class RetailPViewRenderer // repainted by the root's own shells after the depth clear, so over-draw // here is color-safe; statics draw whole (the main viewcone has no entry // for look-in cells; over-include is the safe direction). - private void DrawBuildingLookIns(RetailPViewDrawContext ctx, InteriorEntityPartition.Result partition) + private void DrawBuildingLookIns( + RetailPViewDrawContext ctx, + ClipFrameAssembly clipAssembly, + InteriorEntityPartition.Result partition) { if (_lookInFrames.Count == 0) return; @@ -355,6 +358,17 @@ public sealed class RetailPViewRenderer _cellStaticScratch.Clear(); _cellStaticScratch.AddRange(bucket); DrawEntityBucket(ctx, _cellStaticScratch, _oneCell); + + // #131: the cell-particles pass for look-in cells — retail's + // nested DrawCells draws objects WITH their emitters + // (DrawObjCellForDummies, pc:432878+). Without this, an + // emitter owned by a far building's cell static (the + // Holtburg hall-porch portal swirl, cell 0x017A) drew ONLY + // when the viewer was outdoors (the merge path runs the + // main per-cell pass) — invisible from inside any cottage. + foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId)) + ctx.DrawCellParticles?.Invoke(new RetailPViewCellSliceContext( + cellId, slice, _cellStaticScratch)); } } } @@ -410,7 +424,7 @@ public sealed class RetailPViewRenderer // stage (their punches mark against the terrain/exterior depth just // drawn), strictly BEFORE the depth clear + seals below, matching // retail's LScape::draw placement (DrawCells pc:432719 vs 432732/432785). - DrawBuildingLookIns(ctx, partition); + DrawBuildingLookIns(ctx, clipAssembly, partition); // LATE phase (per slice): outside-stage dynamics' meshes (#118 — drawn // pre-clear so the seal protects their aperture pixels; AFTER the @@ -429,8 +443,20 @@ public sealed class RetailPViewRenderer foreach (var e in partition.OutdoorStatic) { EntitySphere(e, out var c, out float r); - if (viewcone.SphereVisibleInOutsideSlice(probeSliceIndex, c, r)) + bool ownerPass = viewcone.SphereVisibleInOutsideSlice(probeSliceIndex, c, r); + if (ownerPass) _lateParticleOwnerScratch.Add(e); + // #131 owner watchlist (throwaway): ACDREAM_DUMP_ENTITY ids + // double as an ENTITY-id watchlist here — one line per watched + // outdoor-static owner per CHANGE of its cone verdict. + if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeOutStageEnabled + && AcDream.Core.Rendering.RenderingDiagnostics.DumpEntitySourceIds.Contains(e.Id) + && (!_outStageOwnerVerdicts.TryGetValue(e.Id, out bool prev) || prev != ownerPass)) + { + _outStageOwnerVerdicts[e.Id] = ownerPass; + Console.WriteLine(System.FormattableString.Invariant( + $"[outstage-own] id=0x{e.Id:X8} src=0x{e.SourceGfxObjOrSetupId:X8} pos=({e.Position.X:F1},{e.Position.Y:F1},{e.Position.Z:F1}) c=({c.X:F1},{c.Y:F1},{c.Z:F1}) r={r:F1} slice={probeSliceIndex} {(ownerPass ? "PASS" : "CULL")}")); + } } foreach (var e in _outsideStageDynamics) { @@ -475,6 +501,7 @@ public sealed class RetailPViewRenderer // which outdoor dynamics were routed to the outside stage and which // survived the slice viewcone. Strip with the probe when #131 closes. private string? _lastOutStageSig; + private readonly Dictionary _outStageOwnerVerdicts = new(); private void EmitOutStageProbe(int sliceIndex, ViewconeCuller viewcone) {