diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs index 86654d1..7fe8d77 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs @@ -357,12 +357,9 @@ public sealed unsafe class WbDrawDispatcher : IDisposable foreach (var animatedId in animatedEntityIds) { if (!entry.AnimatedById.TryGetValue(animatedId, out var entity)) continue; - // Phase A8 fix: every entity in this loop IS animated (we're iterating - // animatedEntityIds). Animated entities (player, NPCs, monsters) are live - // server-spawned objects that have ParentCellId == null but must ALWAYS - // render in the indoor pass — never stencil-gated by OutdoorOnly. - // Otherwise the character disappears when the camera enters a building. - if (set == EntitySet.OutdoorOnly) continue; + // Phase A8: EntitySet partition for indoor/outdoor split passes. + if (set == EntitySet.IndoorOnly && !entity.ParentCellId.HasValue) continue; + if (set == EntitySet.OutdoorOnly && entity.ParentCellId.HasValue) continue; if (entity.MeshRefs.Count == 0) continue; if (entity.ParentCellId.HasValue && visibleCellIds is not null && !visibleCellIds.Contains(entity.ParentCellId.Value)) continue; @@ -375,13 +372,9 @@ public sealed unsafe class WbDrawDispatcher : IDisposable foreach (var entity in entry.Entities) { - // Phase A8 fix: animated entities (player, NPCs, monsters) are live - // server-spawned objects with ParentCellId == null. They must ALWAYS render - // in the indoor pass — never stencil-gated by OutdoorOnly — or the character - // disappears when the camera enters a building. - bool isAnimated = animatedEntityIds is not null && animatedEntityIds.Contains(entity.Id); - if (set == EntitySet.IndoorOnly && !entity.ParentCellId.HasValue && !isAnimated) continue; - if (set == EntitySet.OutdoorOnly && (entity.ParentCellId.HasValue || isAnimated)) continue; + // Phase A8: EntitySet partition for indoor/outdoor split passes. + if (set == EntitySet.IndoorOnly && !entity.ParentCellId.HasValue) continue; + if (set == EntitySet.OutdoorOnly && entity.ParentCellId.HasValue) continue; if (entity.MeshRefs.Count == 0) continue; // Detect cell entity for indoor probes — first MeshRef.GfxObjId @@ -410,7 +403,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable // Per-entity AABB frustum cull (perf #3). Animated entities bypass — // they're tracked at landblock level + need per-frame work regardless. // A.5 T18 Change #2: read cached AABB, refresh lazily on AabbDirty. - // Note: isAnimated already computed above for the EntitySet partition. + bool isAnimated = animatedEntityIds?.Contains(entity.Id) == true; bool aabbVisible = true; if (frustum is not null && !isAnimated && entry.LandblockId != neverCullLandblockId) { @@ -1357,18 +1350,13 @@ public sealed unsafe class WbDrawDispatcher : IDisposable public static List WalkEntitiesForTest( IReadOnlyList entities, HashSet? visibleCellIds, - EntitySet set, - HashSet? animatedEntityIds = null) + EntitySet set) { var output = new List(); foreach (var entity in entities) { - // Phase A8 fix: animated entities (player, NPCs, monsters) are live - // server-spawned objects with ParentCellId == null. They must ALWAYS render - // in the indoor pass — never stencil-gated by OutdoorOnly. - bool isAnimated = animatedEntityIds is not null && animatedEntityIds.Contains(entity.Id); - if (set == EntitySet.IndoorOnly && !entity.ParentCellId.HasValue && !isAnimated) continue; - if (set == EntitySet.OutdoorOnly && (entity.ParentCellId.HasValue || isAnimated)) continue; + if (set == EntitySet.IndoorOnly && !entity.ParentCellId.HasValue) continue; + if (set == EntitySet.OutdoorOnly && entity.ParentCellId.HasValue) continue; if (entity.MeshRefs.Count == 0) continue; bool cellInVis = !(entity.ParentCellId.HasValue diff --git a/tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherEntitySetTests.cs b/tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherEntitySetTests.cs index 67126d5..ec2d2e6 100644 --- a/tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherEntitySetTests.cs +++ b/tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherEntitySetTests.cs @@ -107,69 +107,4 @@ public class WbDrawDispatcherEntitySetTests Assert.Contains(0x10000002u, result); Assert.DoesNotContain(0x10000003u, result); } - - // --------------------------------------------------------------------- - // Phase A8 fix (post-visual-verification): animated entities (player, - // NPCs, monsters) are live server-spawned objects with ParentCellId == null. - // Without these tests, they would be classified as outdoor scenery and - // stencil-gated by the OutdoorOnly pass — causing the character to - // disappear when the camera enters a building. Fix: animatedEntityIds - // overrides the ParentCellId-based partition. Animated entities always - // belong in the IndoorOnly pass, never in OutdoorOnly. - // --------------------------------------------------------------------- - - [Fact] - public void EntitySet_IndoorOnly_IncludesAnimatedEntitiesEvenWithNullParentCellId() - { - var entities = new List - { - Indoor(0x10000001, 0xA9B40143), - Outdoor(0x10000002), // static outdoor scenery - Outdoor(0x40000005), // animated (player/NPC) - }; - - var visible = new HashSet { 0xA9B40143u }; - var animated = new HashSet { 0x40000005u }; - - var result = WbDrawDispatcher.WalkEntitiesForTest( - entities, - visibleCellIds: visible, - set: WbDrawDispatcher.EntitySet.IndoorOnly, - animatedEntityIds: animated); - - // Indoor entity passes via ParentCellId.HasValue. - // Outdoor scenery (0x10000002) fails — not animated, no ParentCellId. - // Animated entity (0x40000005) passes via animatedEntityIds override. - Assert.Equal(2, result.Count); - Assert.Contains(0x10000001u, result); - Assert.Contains(0x40000005u, result); - Assert.DoesNotContain(0x10000002u, result); - } - - [Fact] - public void EntitySet_OutdoorOnly_ExcludesAnimatedEntities() - { - var entities = new List - { - Outdoor(0x10000002), // static outdoor scenery - Outdoor(0x40000005), // animated (player/NPC) - Outdoor(0x40000006), // animated (NPC) - }; - - var animated = new HashSet { 0x40000005u, 0x40000006u }; - - var result = WbDrawDispatcher.WalkEntitiesForTest( - entities, - visibleCellIds: null, - set: WbDrawDispatcher.EntitySet.OutdoorOnly, - animatedEntityIds: animated); - - // Only static outdoor scenery passes the OutdoorOnly partition. - // Animated entities are explicitly excluded so they don't get - // stencil-gated (they're drawn in the IndoorOnly pass instead). - Assert.Single(result); - Assert.Contains(0x10000002u, result); - Assert.DoesNotContain(0x40000005u, result); - Assert.DoesNotContain(0x40000006u, result); - } }