feat(overhaul): port S5 particle cell visibility
Publish the exact completed walk landscape set as the typed retained particle-view product. Apply retail CLandCell membership versus constant-true non-null CEnvCell eligibility while preserving the x87 distance and AP-116 behavior. Delete the null-root terrain visibility reconstruction and the dead drawable-cell point-light feedback chain. Retire AP-117, correct AP-85 and AD-21, and keep directional shadows, building degrade, AP-232, probes, RHI, shaders, and DAT outside this chunk. Automated return: Release 0 warnings/0 errors; Core VFX 111/111; App particle/frame/renderer/terrain 146/146; warmed production allocation 1/1 at 0 B. First official hermetic artifact s5-c1-hermetic-20260904 is preserved at 16759/16760 with only the stale 162-row assertion; after the bounded 161 correction, exact pin 1/1 and fresh s5-c1-hermetic-corrected-20260904 16760/16760. InstalledDat 385 pass, 10 documented failures, 1 documented skip, no new identity. Mutation 1: making EnvCell eligibility set-dependent first failed ParticleSystemTests.cs:553 Assert.True, expected true actual false. Mutation 2: making outdoor eligibility constant true first failed ParticleSystemTests.cs:559 Assert.False, expected false actual true. Mutation 3: feeding the diagnostic union first failed WorldSceneRendererTests.cs:279 HashSet equality, expected [16842755], actual [16843008, 16842755]. Mutation 4: restoring CollectVisibleCells first failed TerrainParticleCellVisibilityTests.cs:37 and named TerrainModernRenderer.cs. Mutation 5: restoring ObserveDrawableCells first failed TerrainParticleCellVisibilityTests.cs:37 and named WorldRenderFrameBuilder.cs. Mutation 6: changing the inclusive boundary from <= to < first failed ParticleSystemTests.cs:519 Assert.True, expected true actual false. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
efb0756190
commit
94a6b5ef39
18 changed files with 352 additions and 269 deletions
|
|
@ -23,6 +23,7 @@ internal sealed class RetailPViewRenderer
|
|||
// call. Every walk consumer reads it synchronously in this frame.
|
||||
private readonly HashSet<uint> _drawableCellsScratch = new();
|
||||
private readonly HashSet<uint> _visibleCellsScratch = new();
|
||||
private readonly HashSet<uint> _visibleLandscapeCellsScratch = new();
|
||||
|
||||
// FW3 visual-gate fix: the interior root's dynamics phase, invoked by
|
||||
// the driver's clearInteriorDepth closure at the walk's pre-clear
|
||||
|
|
@ -294,6 +295,8 @@ internal sealed class RetailPViewRenderer
|
|||
_drawableCellsScratch.Clear();
|
||||
_drawableCellsScratch.UnionWith(walkDriver.VisitedCells);
|
||||
walkDriver.CopyVisibleCellsTo(_visibleCellsScratch);
|
||||
_visibleLandscapeCellsScratch.Clear();
|
||||
_visibleLandscapeCellsScratch.UnionWith(walkDriver.VisitedLandscapeCellIds);
|
||||
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFacilityStairsEnabled)
|
||||
{
|
||||
|
|
@ -437,6 +440,7 @@ internal sealed class RetailPViewRenderer
|
|||
clipAssembly,
|
||||
drawableCells,
|
||||
_visibleCellsScratch,
|
||||
_visibleLandscapeCellsScratch,
|
||||
counts,
|
||||
sourceCounts,
|
||||
diagnosticPartition: null);
|
||||
|
|
@ -865,10 +869,17 @@ public sealed class RetailPViewFrameResult
|
|||
public ClipFrameAssembly ClipAssembly { get; private set; } = null!;
|
||||
public HashSet<uint> DrawableCells { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The production retail landscape walk's exact outdoor land-cell
|
||||
/// <c>in_view</c> set. Particle consumers borrow this typed half rather
|
||||
/// than the diagnostic union.
|
||||
/// </summary>
|
||||
public HashSet<uint> VisibleLandscapeCells { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The production retail walk's exact visited-cell set. This is the one
|
||||
/// visibility answer consumed by EnvCell preparation, particles, lights,
|
||||
/// and directional-shadow filtering.
|
||||
/// diagnostic visibility union. Typed consumers must use
|
||||
/// <see cref="DrawableCells"/> or <see cref="VisibleLandscapeCells"/>.
|
||||
/// </summary>
|
||||
public HashSet<uint> VisibleCells { get; private set; } = null!;
|
||||
|
||||
|
|
@ -881,6 +892,7 @@ public sealed class RetailPViewFrameResult
|
|||
ClipFrameAssembly clipAssembly,
|
||||
HashSet<uint> drawableCells,
|
||||
HashSet<uint> visibleCells,
|
||||
HashSet<uint> visibleLandscapeCells,
|
||||
RenderFrameDiagnosticCounts diagnosticCounts,
|
||||
RenderProjectionCounts sourceCounts,
|
||||
InteriorEntityPartition.Result? diagnosticPartition)
|
||||
|
|
@ -888,6 +900,7 @@ public sealed class RetailPViewFrameResult
|
|||
ClipAssembly = clipAssembly;
|
||||
DrawableCells = drawableCells;
|
||||
VisibleCells = visibleCells;
|
||||
VisibleLandscapeCells = visibleLandscapeCells;
|
||||
DiagnosticCounts = diagnosticCounts;
|
||||
SourceCounts = sourceCounts;
|
||||
DiagnosticPartition = diagnosticPartition;
|
||||
|
|
|
|||
|
|
@ -67,7 +67,6 @@ public sealed partial class TerrainModernRenderer : IDisposable
|
|||
|
||||
// Reusable per-frame buffers.
|
||||
private readonly List<int> _visibleSlots = new();
|
||||
private readonly HashSet<uint> _visibleCellIds = new();
|
||||
private readonly HashSet<uint> _walkVisibleLandblocks = new();
|
||||
private DrawElementsIndirectCommand[] _deicScratch = Array.Empty<DrawElementsIndirectCommand>();
|
||||
|
||||
|
|
@ -108,15 +107,6 @@ public sealed partial class TerrainModernRenderer : IDisposable
|
|||
/// cref="DrawLandCells"/> batches (indirect draws) submitted THIS FRAME.</summary>
|
||||
internal int WalkDrawCount => _walkDrawsThisFrame;
|
||||
|
||||
/// <summary>
|
||||
/// Outdoor landcells admitted by the current landscape view. The set is
|
||||
/// accumulated across doorway landscape slices and consumed after the
|
||||
/// completed render frame by particle visibility.
|
||||
/// </summary>
|
||||
internal HashSet<uint> VisibleCellIds => _visibleCellIds;
|
||||
|
||||
public void BeginVisibilityFrame() => _visibleCellIds.Clear();
|
||||
|
||||
/// <summary>
|
||||
/// Resets the per-GPU-fenced-frame-slot draw state. A retail outside view
|
||||
/// may draw terrain more than once in a frame.
|
||||
|
|
@ -249,16 +239,15 @@ public sealed partial class TerrainModernRenderer : IDisposable
|
|||
|
||||
// Campaign FW4: RetailFrameWalk is the sole visibility authority.
|
||||
// Terrain remains one full-landblock MDI draw, but only landblocks
|
||||
// containing a walk-admitted landcell participate, and the published
|
||||
// landcell set is the walk's exact in_view set rather than a second
|
||||
// frustum-derived approximation.
|
||||
// containing a walk-admitted landcell participate. The walk publishes
|
||||
// its exact in_view set directly to typed consumers; this draw path
|
||||
// does not reconstruct or retain a second visibility answer.
|
||||
_walkVisibleLandblocks.Clear();
|
||||
if (inViewLandcells is not null)
|
||||
{
|
||||
foreach (uint cellId in inViewLandcells)
|
||||
{
|
||||
_walkVisibleLandblocks.Add(cellId & 0xFFFF0000u);
|
||||
_visibleCellIds.Add(cellId);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -279,16 +268,6 @@ public sealed partial class TerrainModernRenderer : IDisposable
|
|||
continue;
|
||||
}
|
||||
_visibleSlots.Add(slot);
|
||||
if (inViewLandcells is null)
|
||||
{
|
||||
CollectVisibleCells(
|
||||
_visibleCellIds,
|
||||
data.LandblockId,
|
||||
data.WorldOrigin,
|
||||
data.AabbMin.Z,
|
||||
data.AabbMax.Z,
|
||||
frustum);
|
||||
}
|
||||
}
|
||||
if (_visibleSlots.Count == 0) return;
|
||||
|
||||
|
|
@ -480,53 +459,6 @@ public sealed partial class TerrainModernRenderer : IDisposable
|
|||
// Private helpers
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// S3 landing hygiene (H2): frustum-only now. Retail never view-clips
|
||||
/// terrain (see <see cref="DrawLandCells"/>'s doc); the CPU/GPU
|
||||
/// clip-region equivalence check this used to also run (a doorway
|
||||
/// slice's clip-space planes plus its NDC-AABB scissor fallback) had
|
||||
/// exactly one caller — <see cref="Draw"/> — and that caller's own two
|
||||
/// clip-region parameters were themselves dead (grep: no production
|
||||
/// caller ever passed either), so the equivalence helper and its
|
||||
/// homogeneous-plane subroutine are deleted along with them.
|
||||
/// </summary>
|
||||
internal static void CollectVisibleCells(
|
||||
HashSet<uint> destination,
|
||||
uint landblockId,
|
||||
Vector3 worldOrigin,
|
||||
float zMin,
|
||||
float zMax,
|
||||
FrustumPlanes? frustum)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(destination);
|
||||
const float cellSize = AcDream.Core.Physics.TerrainSurface.CellSize;
|
||||
const int cellsPerSide = AcDream.Core.Physics.TerrainSurface.CellsPerSide;
|
||||
uint prefix = landblockId & 0xFFFF0000u;
|
||||
|
||||
for (int cellX = 0; cellX < cellsPerSide; cellX++)
|
||||
{
|
||||
float minX = worldOrigin.X + cellX * cellSize;
|
||||
float maxX = minX + cellSize;
|
||||
for (int cellY = 0; cellY < cellsPerSide; cellY++)
|
||||
{
|
||||
float minY = worldOrigin.Y + cellY * cellSize;
|
||||
float maxY = minY + cellSize;
|
||||
var cellMin = new Vector3(minX, minY, zMin);
|
||||
var cellMax = new Vector3(maxX, maxY, zMax);
|
||||
if (frustum is not null
|
||||
&& !FrustumCuller.IsAabbVisible(frustum.Value, cellMin, cellMax))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
uint low = AcDream.Core.Physics.TerrainSurface.ComputeOutdoorCellLowId(
|
||||
cellX * cellSize,
|
||||
cellY * cellSize);
|
||||
destination.Add(prefix | low);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureCapacity(int newCapacity)
|
||||
{
|
||||
if (newCapacity <= _alloc.Capacity)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ namespace AcDream.App.Rendering.Vfx;
|
|||
|
||||
internal interface IWorldSceneParticleVisibility
|
||||
{
|
||||
void MarkVisibleCells(HashSet<uint> cellIds);
|
||||
void MarkVisibleLandscapeCells(HashSet<uint> cellIds);
|
||||
|
||||
void CompleteFrame();
|
||||
|
||||
|
|
@ -15,8 +15,10 @@ internal interface IWorldSceneParticleVisibility
|
|||
/// <summary>
|
||||
/// Bridges the retained retail PView result into the next physics update's
|
||||
/// <c>CObjCell::IsInView</c> particle gate. The controller owns only immutable
|
||||
/// frame meaning: one completed viewer position plus the AC cells admitted by
|
||||
/// that completed view. It neither creates emitters nor performs rendering.
|
||||
/// frame meaning: one completed viewer position plus the outdoor landscape
|
||||
/// cells admitted by that completed view. EnvCells use retail's constant-true
|
||||
/// virtual and therefore never enter this retained set. The controller neither
|
||||
/// creates emitters nor performs rendering.
|
||||
/// </summary>
|
||||
public sealed class ParticleVisibilityController : IWorldSceneParticleVisibility
|
||||
{
|
||||
|
|
@ -50,12 +52,24 @@ public sealed class ParticleVisibilityController : IWorldSceneParticleVisibility
|
|||
_frameUsesWorldView = true;
|
||||
}
|
||||
|
||||
public void MarkVisibleCells(HashSet<uint> cellIds)
|
||||
public void MarkVisibleLandscapeCells(HashSet<uint> cellIds)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(cellIds);
|
||||
if (!_frameOpen || !_frameUsesWorldView)
|
||||
return;
|
||||
|
||||
foreach (uint cellId in cellIds)
|
||||
{
|
||||
uint low = cellId & 0xFFFFu;
|
||||
if (low == 0u || low >= 0x0100u)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Landscape visibility accepts only outdoor land cells; "
|
||||
+ $"0x{cellId:X8} is not one.",
|
||||
nameof(cellIds));
|
||||
}
|
||||
}
|
||||
|
||||
_buildingCellIds.UnionWith(cellIds);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -100,9 +100,6 @@ internal interface IWorldRenderFrameBuilder
|
|||
bool waitingForLogin,
|
||||
DayGroupData? activeDayGroup);
|
||||
|
||||
void ObserveDrawableCells(IReadOnlySet<uint> drawableCells);
|
||||
|
||||
void ClearDrawableCells();
|
||||
}
|
||||
|
||||
internal interface IWorldFrameCameraSource
|
||||
|
|
@ -135,9 +132,6 @@ internal interface IWorldFrameEnvironmentPreparation
|
|||
in RenderFrameFoundation foundation,
|
||||
DayGroupData? activeDayGroup);
|
||||
|
||||
void ObserveDrawableCells(IReadOnlySet<uint> drawableCells);
|
||||
|
||||
void ClearDrawableCells();
|
||||
}
|
||||
|
||||
internal interface IWorldFrameAnimatedEntitySource
|
||||
|
|
@ -205,10 +199,6 @@ internal sealed class WorldRenderFrameBuilder : IWorldRenderFrameBuilder
|
|||
return new WorldRenderFrame(camera, roots, buildings, animated);
|
||||
}
|
||||
|
||||
public void ObserveDrawableCells(IReadOnlySet<uint> drawableCells) =>
|
||||
_environment.ObserveDrawableCells(drawableCells);
|
||||
|
||||
public void ClearDrawableCells() => _environment.ClearDrawableCells();
|
||||
}
|
||||
|
||||
internal sealed class RuntimeWorldFrameCameraSource : IWorldFrameCameraSource
|
||||
|
|
@ -343,7 +333,6 @@ internal sealed class RuntimeWorldFrameVisibilityPreparation
|
|||
{
|
||||
private readonly RetailSelectionScene? _selection;
|
||||
private readonly ParticleVisibilityController _particles;
|
||||
private readonly TerrainModernRenderer? _terrain;
|
||||
private readonly WorldRevealCoordinator? _reveal;
|
||||
private readonly WbFrustum? _environmentFrustum;
|
||||
|
||||
|
|
@ -356,7 +345,7 @@ internal sealed class RuntimeWorldFrameVisibilityPreparation
|
|||
{
|
||||
_selection = selection;
|
||||
_particles = particles ?? throw new ArgumentNullException(nameof(particles));
|
||||
_terrain = terrain;
|
||||
_ = terrain;
|
||||
_reveal = reveal;
|
||||
_environmentFrustum = environmentFrustum;
|
||||
}
|
||||
|
|
@ -365,7 +354,6 @@ internal sealed class RuntimeWorldFrameVisibilityPreparation
|
|||
{
|
||||
_selection?.SetViewFrustum(camera.Frustum);
|
||||
_particles.BeginFrame(camera.Position);
|
||||
_terrain?.BeginVisibilityFrame();
|
||||
if (waitingForLogin)
|
||||
return;
|
||||
|
||||
|
|
@ -541,13 +529,6 @@ internal sealed class RuntimeWorldFrameEnvironmentPreparation
|
|||
_lightingUbo?.Upload(ubo);
|
||||
}
|
||||
|
||||
public void ObserveDrawableCells(IReadOnlySet<uint> drawableCells) =>
|
||||
ArgumentNullException.ThrowIfNull(drawableCells);
|
||||
|
||||
public void ClearDrawableCells()
|
||||
{
|
||||
}
|
||||
|
||||
private void UpdateSunFromSky(SkyKeyframe keyframe, bool playerInsideCell)
|
||||
{
|
||||
// CellManager::ChangePosition @ 0x004559B0 keys this on the player
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ namespace AcDream.App.Rendering;
|
|||
|
||||
internal interface IWorldScenePassExecutor
|
||||
{
|
||||
HashSet<uint>? TerrainVisibleCellIds { get; }
|
||||
|
||||
void BeginFrame();
|
||||
|
||||
void PrepareFlatWorldClip();
|
||||
|
|
@ -100,8 +98,6 @@ internal sealed class WorldScenePassExecutor : IWorldScenePassExecutor
|
|||
_particleRenderer = particleRenderer;
|
||||
}
|
||||
|
||||
public HashSet<uint>? TerrainVisibleCellIds => _terrain?.VisibleCellIds;
|
||||
|
||||
public void BeginFrame()
|
||||
{
|
||||
_visibleParticleOwners.Clear();
|
||||
|
|
|
|||
|
|
@ -209,10 +209,8 @@ internal sealed class WorldSceneRenderer : IPreparedWorldSceneFramePhase
|
|||
// so the portal-visible return above already covers every
|
||||
// waiting frame. One gate computes the frame's visibility;
|
||||
// this phase only enforces it.
|
||||
_passes.DrawFlatTerrain(in camera, roots.PlayerLandblockId);
|
||||
terrainDrawn = true;
|
||||
if (_passes.TerrainVisibleCellIds is { } flatTerrainCells)
|
||||
_particleVisibility.MarkVisibleCells(flatTerrainCells);
|
||||
_passes.DrawFlatTerrain(in camera, roots.PlayerLandblockId);
|
||||
terrainDrawn = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -250,11 +248,11 @@ internal sealed class WorldSceneRenderer : IPreparedWorldSceneFramePhase
|
|||
camera.Camera.View,
|
||||
_diagnostics.CameraCellResolution));
|
||||
|
||||
// One visibility answer: the walk's exact visited-cell set
|
||||
// feeds effects and the frame environment (point lights and
|
||||
// directional-shadow filtering).
|
||||
_particleVisibility.MarkVisibleCells(pviewResult.VisibleCells);
|
||||
_frames.ObserveDrawableCells(pviewResult.VisibleCells);
|
||||
// CLandCell::IsInView consumes only the walk's exact landscape
|
||||
// half. EnvCells use CEnvCell::IsInView's constant-true result;
|
||||
// VisibleCells remains a diagnostics-only union.
|
||||
_particleVisibility.MarkVisibleLandscapeCells(
|
||||
pviewResult.VisibleLandscapeCells);
|
||||
_diagnostics.EmitPViewInput(
|
||||
pviewResult.VisibleCells,
|
||||
pviewResult.ClipAssembly.OutsideViewSlices.Length,
|
||||
|
|
@ -287,7 +285,6 @@ internal sealed class WorldSceneRenderer : IPreparedWorldSceneFramePhase
|
|||
_entities.LandblockEntries,
|
||||
roots.PlayerLandblockId,
|
||||
world.AnimatedEntityIds);
|
||||
_frames.ClearDrawableCells();
|
||||
}
|
||||
|
||||
_passes.DisableClipDistances();
|
||||
|
|
|
|||
|
|
@ -20,7 +20,11 @@ public sealed class ParticleSystem : IParticleSystem
|
|||
// thousands of finite/fading emitters.
|
||||
private readonly SortedSet<int> _allHandles = [];
|
||||
private readonly SortedSet<int> _simulationHandles = [];
|
||||
private readonly SortedSet<int> _worldSimulationHandles = [];
|
||||
// ApplyRetailView is a per-frame production path. SortedSet's enumerator
|
||||
// allocates its traversal stack, so retain this subset as a sorted list;
|
||||
// lifecycle mutations pay the binary insertion/removal cost and the
|
||||
// warmed view walk stays allocation-free in emitter-spawn order.
|
||||
private readonly List<int> _worldSimulationHandles = [];
|
||||
private readonly SortedSet<int>[] _renderableHandlesByPass =
|
||||
[[], [], []];
|
||||
private readonly SortedSet<int>[] _renderableUnattachedHandlesByPass =
|
||||
|
|
@ -147,7 +151,7 @@ public sealed class ParticleSystem : IParticleSystem
|
|||
_allHandles.Add(handle);
|
||||
_simulationHandles.Add(handle);
|
||||
if (visibilityPolicy == ParticleVisibilityPolicy.World)
|
||||
_worldSimulationHandles.Add(handle);
|
||||
AddWorldSimulationHandle(handle);
|
||||
AddEmitterToRenderIndexes(emitter);
|
||||
|
||||
for (int i = 0; i < desc.InitialParticles; i++)
|
||||
|
|
@ -330,7 +334,7 @@ public sealed class ParticleSystem : IParticleSystem
|
|||
if (visibilityPolicy == ParticleVisibilityPolicy.World)
|
||||
{
|
||||
if (emitter.SimulationEnabled)
|
||||
_worldSimulationHandles.Add(handle);
|
||||
AddWorldSimulationHandle(handle);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -343,22 +347,25 @@ public sealed class ParticleSystem : IParticleSystem
|
|||
/// <summary>
|
||||
/// Applies <c>CPhysicsObj::ShouldDrawParticles</c> (0x0050FE60) to every
|
||||
/// live emitter. The App layer supplies the previous completed retail
|
||||
/// PView's cell set, equivalent to <c>CObjCell::IsInView</c> when the next
|
||||
/// physics update runs.
|
||||
/// PView's exact landscape-cell set. The cell-family branch below invokes
|
||||
/// the matching retail virtual when the next physics update runs:
|
||||
/// <c>CLandCell::IsInView</c> reads that completed set, while
|
||||
/// <c>CEnvCell::IsInView</c> is constant true.
|
||||
/// </summary>
|
||||
public void ApplyRetailView(
|
||||
Vector3 viewerPosition,
|
||||
IReadOnlySet<uint> visibleCellIds,
|
||||
IReadOnlySet<uint> visibleLandscapeCellIds,
|
||||
bool hasCompletedView,
|
||||
float rangeMultiplier = 1f)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(visibleCellIds);
|
||||
ArgumentNullException.ThrowIfNull(visibleLandscapeCellIds);
|
||||
if (!float.IsFinite(rangeMultiplier) || rangeMultiplier <= 0f)
|
||||
rangeMultiplier = 1f;
|
||||
|
||||
LastRetailViewEmitterVisitCount = 0;
|
||||
foreach (int handle in _worldSimulationHandles)
|
||||
for (int i = 0; i < _worldSimulationHandles.Count; i++)
|
||||
{
|
||||
int handle = _worldSimulationHandles[i];
|
||||
if (!_byHandle.TryGetValue(handle, out ParticleEmitter? emitter))
|
||||
continue;
|
||||
LastRetailViewEmitterVisitCount++;
|
||||
|
|
@ -374,8 +381,12 @@ public sealed class ParticleSystem : IParticleSystem
|
|||
|
||||
float maxDistance = emitter.Desc.MaxDegradeDistance * rangeMultiplier;
|
||||
float distance = RetailDistance(emitter.OwnerPosition, viewerPosition);
|
||||
uint ownerCellLow = emitter.OwnerCellId & 0xFFFFu;
|
||||
bool ownerCellInView = ownerCellLow >= 0x0100u
|
||||
|| (ownerCellLow > 0u
|
||||
&& visibleLandscapeCellIds.Contains(emitter.OwnerCellId));
|
||||
emitter.ViewEligible = emitter.OwnerCellId != 0
|
||||
&& visibleCellIds.Contains(emitter.OwnerCellId)
|
||||
&& ownerCellInView
|
||||
// The x87 comparison in ShouldDrawParticles admits unordered
|
||||
// comparisons (NaN) and reject a negative authored range.
|
||||
&& (float.IsNaN(distance)
|
||||
|
|
@ -442,7 +453,7 @@ public sealed class ParticleSystem : IParticleSystem
|
|||
emitter.SimulationEnabled = true;
|
||||
_simulationHandles.Add(handle);
|
||||
if (emitter.VisibilityPolicy == ParticleVisibilityPolicy.World)
|
||||
_worldSimulationHandles.Add(handle);
|
||||
AddWorldSimulationHandle(handle);
|
||||
else
|
||||
emitter.ViewEligible = true;
|
||||
}
|
||||
|
|
@ -1007,6 +1018,13 @@ public sealed class ParticleSystem : IParticleSystem
|
|||
}
|
||||
}
|
||||
|
||||
private void AddWorldSimulationHandle(int handle)
|
||||
{
|
||||
int index = _worldSimulationHandles.BinarySearch(handle);
|
||||
if (index < 0)
|
||||
_worldSimulationHandles.Insert(~index, handle);
|
||||
}
|
||||
|
||||
private static bool IsRenderable(ParticleEmitter emitter)
|
||||
=> emitter.PresentationVisible && emitter.ViewEligible;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue