feat(render): R1 — InteriorEntityPartition (3-bucket per-cell entity split)
Pure helper splitting a frame's entities into live-dynamic / per-cell statics / outdoor scenery, by the same precedence as WbDrawDispatcher.ResolveEntitySlot (serverGuid first — live entities have no ParentCellId). Feeds the per-cell DrawInside loop. 3 unit tests, GL-free. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ce7404b92b
commit
cf85ea4e17
2 changed files with 144 additions and 0 deletions
|
|
@ -0,0 +1,82 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.Core.World;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
public class InteriorEntityPartitionTests
|
||||
{
|
||||
private const uint CellA = 0xA9B40170;
|
||||
private const uint CellB = 0xA9B40171;
|
||||
|
||||
private static WorldEntity Ent(uint id, uint serverGuid, uint? parentCell) => new()
|
||||
{
|
||||
Id = id,
|
||||
ServerGuid = serverGuid,
|
||||
SourceGfxObjOrSetupId = 0x01000001,
|
||||
Position = Vector3.Zero,
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = new[] { new MeshRef(0x01000001, Matrix4x4.Identity) },
|
||||
ParentCellId = parentCell,
|
||||
};
|
||||
|
||||
private static IEnumerable<(uint, Vector3, Vector3, IReadOnlyList<WorldEntity>,
|
||||
IReadOnlyDictionary<uint, WorldEntity>?)> OneLb(uint lbId, params WorldEntity[] ents)
|
||||
=> new[] { (lbId, Vector3.Zero, Vector3.Zero, (IReadOnlyList<WorldEntity>)ents,
|
||||
(IReadOnlyDictionary<uint, WorldEntity>?)null) };
|
||||
|
||||
[Fact]
|
||||
public void Partitions_ByServerGuidThenParentCell_IntoThreeBuckets()
|
||||
{
|
||||
var livePlayer = Ent(1, serverGuid: 0x5000000A, parentCell: null); // live-dynamic
|
||||
var liveNpcInCell = Ent(2, serverGuid: 0x80001234, parentCell: CellA); // live-dynamic WINS over cell
|
||||
var staticA = Ent(3, serverGuid: 0, parentCell: CellA); // per-cell static
|
||||
var staticB = Ent(4, serverGuid: 0, parentCell: CellB); // per-cell static
|
||||
var scenery = Ent(5, serverGuid: 0, parentCell: null); // outdoor scenery
|
||||
|
||||
var visible = new HashSet<uint> { CellA, CellB };
|
||||
var result = InteriorEntityPartition.Partition(
|
||||
visible, OneLb(0xA9B4FFFF, livePlayer, liveNpcInCell, staticA, staticB, scenery));
|
||||
|
||||
Assert.Equal(2, result.LiveDynamic.Count); // player + npc (serverGuid != 0)
|
||||
Assert.Contains(livePlayer, result.LiveDynamic);
|
||||
Assert.Contains(liveNpcInCell, result.LiveDynamic);
|
||||
|
||||
Assert.Single(result.ByCell[CellA]); // only staticA (npc went live-dynamic)
|
||||
Assert.Contains(staticA, result.ByCell[CellA]);
|
||||
Assert.Single(result.ByCell[CellB]);
|
||||
Assert.Contains(staticB, result.ByCell[CellB]);
|
||||
|
||||
Assert.Single(result.Outdoor);
|
||||
Assert.Contains(scenery, result.Outdoor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Static_InNonVisibleCell_IsDropped()
|
||||
{
|
||||
var staticHidden = Ent(3, serverGuid: 0, parentCell: 0xA9B40199); // not in the visible set
|
||||
var visible = new HashSet<uint> { CellA };
|
||||
var result = InteriorEntityPartition.Partition(visible, OneLb(0xA9B4FFFF, staticHidden));
|
||||
|
||||
Assert.False(result.ByCell.ContainsKey(0xA9B40199));
|
||||
Assert.Empty(result.Outdoor);
|
||||
Assert.Empty(result.LiveDynamic);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EntityWithNoMeshRefs_IsSkipped()
|
||||
{
|
||||
var noMesh = new WorldEntity
|
||||
{
|
||||
Id = 9, ServerGuid = 0, SourceGfxObjOrSetupId = 0x01000001,
|
||||
Position = Vector3.Zero, Rotation = Quaternion.Identity,
|
||||
MeshRefs = System.Array.Empty<MeshRef>(), ParentCellId = CellA,
|
||||
};
|
||||
var result = InteriorEntityPartition.Partition(
|
||||
new HashSet<uint> { CellA }, OneLb(0xA9B4FFFF, noMesh));
|
||||
|
||||
Assert.False(result.ByCell.ContainsKey(CellA));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue