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:
parent
f8d0499d8b
commit
3361933ce6
2 changed files with 153 additions and 0 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
// Phase A8 RR5 — verify WbDrawDispatcher.WalkEntitiesForTestByCellIds,
|
||||
// the pure-data companion to the new Draw(cellIds:) production overload.
|
||||
//
|
||||
// Semantics: indoor entities (ParentCellId.HasValue) are gated by explicit
|
||||
// membership in cellIds. Building shells (IsBuildingShell) always pass.
|
||||
// Outdoor scenery (no ParentCellId, not a shell) is excluded by EntitySet.IndoorPass.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.Core.World;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Rendering.Wb;
|
||||
|
||||
public class WbDrawDispatcherCellIdsOverloadTests
|
||||
{
|
||||
private static WorldEntity CellEnt(uint id, uint cellId) => new()
|
||||
{
|
||||
Id = id,
|
||||
SourceGfxObjOrSetupId = 0x01000001u,
|
||||
ParentCellId = cellId,
|
||||
MeshRefs = new List<MeshRef> { new(0x01000001u, Matrix4x4.Identity) },
|
||||
Position = Vector3.Zero,
|
||||
Rotation = Quaternion.Identity,
|
||||
};
|
||||
|
||||
private static WorldEntity OutdoorScenery(uint id) => new()
|
||||
{
|
||||
Id = id,
|
||||
SourceGfxObjOrSetupId = 0x01000001u,
|
||||
ParentCellId = null,
|
||||
IsBuildingShell = false,
|
||||
MeshRefs = new List<MeshRef> { new(0x01000001u, Matrix4x4.Identity) },
|
||||
Position = Vector3.Zero,
|
||||
Rotation = Quaternion.Identity,
|
||||
};
|
||||
|
||||
private static WorldEntity BuildingShell(uint id) => new()
|
||||
{
|
||||
Id = id,
|
||||
SourceGfxObjOrSetupId = 0x02000001u,
|
||||
ParentCellId = null,
|
||||
IsBuildingShell = true,
|
||||
MeshRefs = new List<MeshRef> { new(0x01000001u, Matrix4x4.Identity) },
|
||||
Position = Vector3.Zero,
|
||||
Rotation = Quaternion.Identity,
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void WalkEntitiesByCellIds_IncludesOnlyEntitiesInListedCells()
|
||||
{
|
||||
var entities = new List<WorldEntity>
|
||||
{
|
||||
CellEnt(0x40000001u, 0xA9B40150u), // in listed cells
|
||||
CellEnt(0x40000002u, 0xA9B40151u), // in listed cells
|
||||
CellEnt(0x40000003u, 0xA9B40999u), // OUT — not in list
|
||||
BuildingShell(0xC0000001u), // always included (IsBuildingShell)
|
||||
OutdoorScenery(0xC0000002u), // OUT — not a shell, not in cell list
|
||||
};
|
||||
var cellIds = new HashSet<uint> { 0xA9B40150u, 0xA9B40151u };
|
||||
|
||||
var result = WbDrawDispatcher.WalkEntitiesForTestByCellIds(
|
||||
entities, cellIds, set: WbDrawDispatcher.EntitySet.IndoorPass);
|
||||
|
||||
Assert.Equal(3, result.Count);
|
||||
Assert.Contains(0x40000001u, result);
|
||||
Assert.Contains(0x40000002u, result);
|
||||
Assert.Contains(0xC0000001u, result);
|
||||
Assert.DoesNotContain(0x40000003u, result);
|
||||
Assert.DoesNotContain(0xC0000002u, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WalkEntitiesByCellIds_EmptyCellList_StillIncludesBuildingShells()
|
||||
{
|
||||
var entities = new List<WorldEntity>
|
||||
{
|
||||
CellEnt(0x40000001u, 0xA9B40150u),
|
||||
BuildingShell(0xC0000001u),
|
||||
};
|
||||
|
||||
var result = WbDrawDispatcher.WalkEntitiesForTestByCellIds(
|
||||
entities, new HashSet<uint>(), set: WbDrawDispatcher.EntitySet.IndoorPass);
|
||||
|
||||
// Cell entity dropped (no cells in list); building shell still passes.
|
||||
Assert.Single(result);
|
||||
Assert.Contains(0xC0000001u, result);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue