fix(render): feed look-in cells to particle and light visibility
Look-in rooms (cells reached through building portals) drew their geometry but never entered the visible-cell set consumed by the particle gate and the point-light candidacy scope, so their emitters stayed frozen and their lanterns dark until the player entered or the camera left the building. Retail has no look-in split: ShouldDrawParticles @0x0050FE60 gates on cell->IsInView(), and a cell installed by DrawBuilding @0x0059F2A0 -> PView::ConstructView @0x005A57B0 is drawn by the same DrawCells traversal as any flooded cell, so it is in view identically (per-cell in_view stamped by PView::InitCell @0x005A4B70). RetailPViewFrameResult now carries InViewCells (main flood + look-in cells, the existing prepareCells union; the borrowed-scratch contract is unchanged because RecycleLookInFrames runs at the start of the next DrawInside). WorldSceneRenderer feeds InViewCells to ParticleVisibilityController and the point-light scope. DrawableCells is untouched for seals, the outside-stage predicate, diagnostics, and the packed-product referee. The legacy 4-arg Reset defaults InViewCells = DrawableCells for standalone callers. User-gated 2026-08-29 at the Sanctuary Cathedral: adjacent look-in rooms show particles and lantern lights from the root cell without entry or an outside camera; recall/waterfall/Holtburg/paperdoll unregressed. The pre-existing #132-family candle-behind-opening report remains open and is unchanged by this fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
f7aa8e0eb7
commit
85530c0b7e
4 changed files with 82 additions and 9 deletions
|
|
@ -251,6 +251,29 @@ public sealed class WorldSceneRendererTests
|
|||
Assert.Equal(4, rig.PView.LastInput.RenderRadius);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PViewWorld_PublishesLookInCellsToParticleAndLightVisibility()
|
||||
{
|
||||
var root = new LoadedCell
|
||||
{
|
||||
CellId = 0x01010001u,
|
||||
IsOutdoorNode = false,
|
||||
};
|
||||
var rig = new Rig(portalVisible: false, waitingForLogin: false, clipRoot: root);
|
||||
|
||||
rig.Renderer.Render(default);
|
||||
|
||||
// Retail gates effects on cell->IsInView() (ShouldDrawParticles
|
||||
// @0x0050FE60), and a cell entered through a building portal
|
||||
// (DrawBuilding @0x0059F2A0 -> PView::ConstructView @0x005A57B0) is
|
||||
// drawn by the same traversal as a flooded cell. The particle gate and
|
||||
// the light-candidate scope must therefore receive InViewCells (flood
|
||||
// plus look-ins), not the flood-only DrawableCells.
|
||||
Assert.Contains(0x01010003u, rig.Visibility.MarkedCells);
|
||||
Assert.NotNull(rig.Frames.ObservedCells);
|
||||
Assert.Contains(0x01010003u, rig.Frames.ObservedCells!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PViewWorld_ReusesOneSynchronousFrameInputAcrossFrames()
|
||||
{
|
||||
|
|
@ -536,7 +559,8 @@ public sealed class WorldSceneRendererTests
|
|||
Frames = new FrameBuilder(Calls, frame);
|
||||
Selection = new SelectionFrame(Calls);
|
||||
var alpha = new AlphaFrame(Calls);
|
||||
var visibility = new ParticleVisibility(Calls);
|
||||
Visibility = new ParticleVisibility(Calls);
|
||||
var visibility = Visibility;
|
||||
PView = new PViewRenderer(Calls);
|
||||
Passes = new PassExecutor(Calls);
|
||||
var diagnostics = new Diagnostics(Calls);
|
||||
|
|
@ -573,6 +597,8 @@ public sealed class WorldSceneRendererTests
|
|||
|
||||
public SelectionFrame Selection { get; }
|
||||
|
||||
public ParticleVisibility Visibility { get; }
|
||||
|
||||
public PViewRenderer PView { get; }
|
||||
|
||||
public PassExecutor Passes { get; }
|
||||
|
|
@ -621,8 +647,13 @@ public sealed class WorldSceneRendererTests
|
|||
return frame;
|
||||
}
|
||||
|
||||
public void ObserveDrawableCells(IReadOnlySet<uint> drawableCells) =>
|
||||
public IReadOnlySet<uint>? ObservedCells { get; private set; }
|
||||
|
||||
public void ObserveDrawableCells(IReadOnlySet<uint> drawableCells)
|
||||
{
|
||||
calls.Add("frame:observe-cells");
|
||||
ObservedCells = new HashSet<uint>(drawableCells);
|
||||
}
|
||||
|
||||
public void ClearDrawableCells() => calls.Add("frame:clear-cells");
|
||||
}
|
||||
|
|
@ -684,8 +715,13 @@ public sealed class WorldSceneRendererTests
|
|||
private sealed class ParticleVisibility(List<string> calls) :
|
||||
IWorldSceneParticleVisibility
|
||||
{
|
||||
public void MarkVisibleCells(HashSet<uint> cellIds) =>
|
||||
public HashSet<uint> MarkedCells { get; } = [];
|
||||
|
||||
public void MarkVisibleCells(HashSet<uint> cellIds)
|
||||
{
|
||||
calls.Add("visibility:mark");
|
||||
MarkedCells.UnionWith(cellIds);
|
||||
}
|
||||
|
||||
public void CompleteFrame() => calls.Add("visibility:complete");
|
||||
|
||||
|
|
@ -701,11 +737,17 @@ public sealed class WorldSceneRendererTests
|
|||
public PViewRenderer(List<string> calls)
|
||||
{
|
||||
_calls = calls;
|
||||
// Distinct flood-only vs in-view sets: 0x01010003 is a look-in
|
||||
// cell that is drawn but never part of the main flood.
|
||||
var interiorPartition = new InteriorEntityPartition.Result();
|
||||
_interiorResult = new RetailPViewFrameResult().Reset(
|
||||
new PortalVisibilityFrame(),
|
||||
new ClipFrameAssembly(),
|
||||
[],
|
||||
new InteriorEntityPartition.Result());
|
||||
[0x01010001u],
|
||||
[0x01010001u, 0x01010003u],
|
||||
RetailPViewRenderer.LegacyDiagnosticCounts(interiorPartition),
|
||||
RetailPViewRenderer.LegacySourceCounts(interiorPartition),
|
||||
interiorPartition);
|
||||
var outdoorPortalFrame = new PortalVisibilityFrame();
|
||||
outdoorPortalFrame.OutsideView.Add(new ViewPolygon(
|
||||
[
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue