feat(render): Phase A8 RR5 — WbDrawDispatcher Draw(cellIds:) overload

Adds a new public overload accepting an explicit IReadOnlyCollection<uint>
cellIds (the camera-buildings' EnvCellIds) instead of a BFS-derived
visibility set. Used by RR7's IndoorPass to scope indoor rendering to the
camera-buildings' cells, not the full portal BFS (which causes Issues A+C).

Pure-data test helper WalkEntitiesForTestByCellIds added alongside the
production overload, mirroring the WalkEntitiesForTest pattern.

The overload internally delegates to the existing visibleCellIds path —
the dispatcher's semantic stays the same; only the caller's intent differs
(explicit cell list vs visibility-derived).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-27 11:18:21 +02:00
parent f8d0499d8b
commit 3361933ce6
2 changed files with 153 additions and 0 deletions

View file

@ -1089,6 +1089,41 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
}
}
/// <summary>
/// Phase A8 RR5 (2026-05-26): per-building draw overload. Walks only
/// entities whose ParentCellId is in <paramref name="cellIds"/>, plus
/// outdoor-style entities matching the EntitySet partition. Used by
/// the indoor render branch to scope rendering to the camera-buildings'
/// cells.
///
/// <para>Mirrors the existing visibleCellIds-based Draw but with an
/// explicit cell list (not the BFS-derived visibility set). The semantic
/// difference is at the caller: cellIds = the camera-buildings' EnvCellIds,
/// not the portal BFS result. The dispatcher's internal logic is identical
/// — it filters indoor entities by membership in the provided set.</para>
/// </summary>
public void Draw(
ICamera camera,
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
IReadOnlyList<WorldEntity> Entities,
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> landblockEntries,
IReadOnlyCollection<uint> cellIds,
FrustumPlanes? frustum = null,
uint? neverCullLandblockId = null,
HashSet<uint>? animatedEntityIds = null,
EntitySet set = EntitySet.All)
{
// Adapt IReadOnlyCollection<uint> → HashSet<uint> for the existing path.
// If the caller already passed a HashSet, avoid re-wrapping.
HashSet<uint> cellIdSet = cellIds is HashSet<uint> hs ? hs : new HashSet<uint>(cellIds);
Draw(camera, landblockEntries,
frustum: frustum,
neverCullLandblockId: neverCullLandblockId,
visibleCellIds: cellIdSet,
animatedEntityIds: animatedEntityIds,
set: set);
}
private static IndirectGroupInput ToInput(InstanceGroup g) => new(
IndexCount: g.IndexCount,
FirstIndex: g.FirstIndex,
@ -1400,6 +1435,34 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
return output;
}
/// <summary>
/// Phase A8 RR5 (2026-05-26): pure-data walk for the explicit cellIds
/// overload. Used by RR7's IndoorPass to render only the camera-buildings'
/// cells (instead of the visibility-derived set).
///
/// <para>Indoor entities (ParentCellId set) gated by membership in
/// <paramref name="cellIds"/>. Building shells (IsBuildingShell) pass
/// unconditionally when <paramref name="set"/> == IndoorPass. Outdoor
/// scenery is excluded by the EntitySet partition (no cell-list gate
/// needed — EntityMatchesSet handles it).</para>
/// </summary>
public static List<uint> WalkEntitiesForTestByCellIds(
IEnumerable<AcDream.Core.World.WorldEntity> entities,
IReadOnlyCollection<uint> cellIds,
EntitySet set)
{
var result = new List<uint>();
foreach (var entity in entities)
{
if (!EntityMatchesSet(entity, set)) continue;
if (entity.MeshRefs.Count == 0) continue;
if (entity.ParentCellId.HasValue && !cellIds.Contains(entity.ParentCellId.Value))
continue;
result.Add(entity.Id);
}
return result;
}
public void Dispose()
{
if (_disposed) return;