fix #451: stabilize portal seam rendering
This commit is contained in:
parent
f6fe0f2a4f
commit
1d2f2f738f
29 changed files with 1650 additions and 239 deletions
|
|
@ -128,6 +128,39 @@ public class ClipFrameAssemblerTests
|
|||
Assert.Equal(TerrainClipMode.Planes, asm.TerrainMode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppendLookInFrames_PacksEveryNestedCellViewWithoutReplacingMainRoutes()
|
||||
{
|
||||
const uint mainCell = 0xA9B40100;
|
||||
const uint lookInCell = 0xA9B40106;
|
||||
var main = new PortalVisibilityFrame();
|
||||
main.CellViews[mainCell] = ViewOf(Square(0f, 0f, 0.8f));
|
||||
main.OrderedVisibleCells.Add(mainCell);
|
||||
main.OutsideView.Add(Square(0f, 0f, 0.7f));
|
||||
|
||||
var nested = new PortalVisibilityFrame();
|
||||
nested.CellViews[lookInCell] = ViewOf(
|
||||
Square(-0.35f, 0f, 0.15f),
|
||||
Square(0.35f, 0f, 0.15f));
|
||||
nested.OrderedVisibleCells.Add(lookInCell);
|
||||
|
||||
using ClipFrame frame = ClipFrame.NoClip();
|
||||
ClipFrameAssembly assembly = ClipFrameAssembler.Assemble(frame, main);
|
||||
int mainSlot = assembly.CellIdToSlot[mainCell];
|
||||
int slotsBeforeLookIn = frame.SlotCount;
|
||||
|
||||
ClipFrameAssembler.AppendLookInFrames(frame, [nested], assembly);
|
||||
|
||||
var key = new LookInClipCell(0, lookInCell);
|
||||
ClipViewSlice[] slices = assembly.LookInCellToViewSlices[key];
|
||||
Assert.Equal(2, slices.Length);
|
||||
Assert.All(slices, slice => Assert.True(slice.Slot >= slotsBeforeLookIn));
|
||||
Assert.NotEqual(slices[0].Slot, slices[1].Slot);
|
||||
Assert.Equal(slices[0].Slot, assembly.LookInCellToSlot[key]);
|
||||
Assert.Equal(mainSlot, assembly.CellIdToSlot[mainCell]);
|
||||
Assert.DoesNotContain(lookInCell, assembly.CellIdToSlot.Keys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Assemble_OutsideViewWithExitPortal_HasOutsideViewTrue_AabbMatchesBounds()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -42,9 +42,12 @@ public sealed class VulkanShaderManifestTests
|
|||
["mesh_modern.frag.spv"] = "b702b644862aca31ce1fb0677adc5872b39c4ea87f595a89363b44d10f2cc50e",
|
||||
["mesh_modern.vert.spv"] = "7ca5fb241c4f0248884ba8fa88fbae17a7d5cbc80efe4ac4a9ffd0254012ead8",
|
||||
["particle.frag.spv"] = "680da227704e0b3afa9b5226a7d73dd65aa9d8759d081cf4d5009d30e148726b",
|
||||
["particle.vert.spv"] = "ed79461ab347bf17edaca714bbbbfabead8192e059c760578ca3a1a01409799e",
|
||||
// Re-pinned 2026-08-27: portal-view clip slots now travel with
|
||||
// deferred billboard particles, matching retail PortalList draws.
|
||||
["particle.vert.spv"] = "bf0f6b7b26a6b237e4abb2973b9959a38338868fd8304b3863f593ad56d61c35",
|
||||
["particle_mesh.frag.spv"] = "7696b1dc0613b5a724c55df465173f613ae047da9675895b149b7c71b009cc7c",
|
||||
["particle_mesh.vert.spv"] = "f7fe8b203cadcd4d54af5cdbcfd9d5bf733146e10bafa78ca730fb6970db0479",
|
||||
// Same contract for full-mesh particle geometry.
|
||||
["particle_mesh.vert.spv"] = "b5b3e0f583e00b78b56e297e60e5050013a26b3a74dbec64f6e5b286b751f0fa",
|
||||
["portal_depth.frag.spv"] = "96755196d4d0da7be4792107557465778be2ebefb5584834cc75bf90ec55a6cc",
|
||||
["portal_depth.vert.spv"] = "cd113860b7acd6afad3ebcc0a68dd7147f6baae729df51ab360c123588dc3ae2",
|
||||
// sky.frag re-pinned 2026-08-23: the dome's fog blend lost its
|
||||
|
|
|
|||
|
|
@ -14,11 +14,15 @@ public sealed class ParticleBindlessInstanceTests
|
|||
// one TextureIndex (a binding=9 handle-table slot, 4 bytes), so the
|
||||
// struct shrank by 4 bytes; TextureIndex keeps TextureHandleLow's
|
||||
// former offset (64 — right after the four vec4 fields).
|
||||
Assert.Equal(68, Marshal.SizeOf<ParticleRenderer.BillboardGpuInstance>());
|
||||
Assert.Equal(72, Marshal.SizeOf<ParticleRenderer.BillboardGpuInstance>());
|
||||
Assert.Equal(
|
||||
new IntPtr(64),
|
||||
Marshal.OffsetOf<ParticleRenderer.BillboardGpuInstance>(
|
||||
nameof(ParticleRenderer.BillboardGpuInstance.TextureIndex)));
|
||||
Assert.Equal(
|
||||
new IntPtr(68),
|
||||
Marshal.OffsetOf<ParticleRenderer.BillboardGpuInstance>(
|
||||
nameof(ParticleRenderer.BillboardGpuInstance.ClipSlot)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -40,6 +44,8 @@ public sealed class ParticleBindlessInstanceTests
|
|||
// index rather than by a null handle, because Vulkan's descriptor array
|
||||
// cannot be asked whether an element was ever written.
|
||||
Assert.Contains("layout(location = 6) in uint aTextureIndex;", vertex);
|
||||
Assert.Contains("layout(location = 7) in uint aClipSlot;", vertex);
|
||||
Assert.Contains("clipRegions[aClipSlot]", vertex);
|
||||
Assert.Contains("flat out uint vTextureIndex;", vertex);
|
||||
Assert.Contains("vTextureIndex = aTextureIndex;", vertex);
|
||||
Assert.Contains("#extension GL_ARB_bindless_texture : require", fragment);
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
RenderProjectionRecord outdoor = Record(
|
||||
0x0100_0000_0000_0001,
|
||||
RenderProjectionClass.OutdoorStatic);
|
||||
RenderProjectionRecord buildingShell = Record(
|
||||
0x0100_0000_0000_0008,
|
||||
RenderProjectionClass.OutdoorStatic,
|
||||
isBuildingShell: true);
|
||||
RenderProjectionRecord hidden = Record(
|
||||
0x0100_0000_0000_0002,
|
||||
RenderProjectionClass.OutdoorStatic,
|
||||
|
|
@ -48,6 +52,7 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
RenderProjectionRecord[] records =
|
||||
[
|
||||
outdoor,
|
||||
buildingShell,
|
||||
hidden,
|
||||
withdrawn,
|
||||
shell,
|
||||
|
|
@ -90,7 +95,7 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
try
|
||||
{
|
||||
Assert.Equal(
|
||||
[outdoor.Id, hidden.Id],
|
||||
[outdoor.Id, hidden.Id, buildingShell.Id],
|
||||
view.OutdoorStaticCandidates.ToArray()
|
||||
.Select(static item => item.Id));
|
||||
Assert.Equal(
|
||||
|
|
@ -101,28 +106,42 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
[outdoorDynamic.Id, cellDynamic.Id],
|
||||
view.DynamicCandidates.ToArray()
|
||||
.Select(static item => item.Id));
|
||||
Assert.Equal(5, view.Transforms.Length);
|
||||
Assert.Equal(5, view.RouteCandidates.Length);
|
||||
Assert.Equal(6, view.Transforms.Length);
|
||||
Assert.Equal(6, view.RouteCandidates.Length);
|
||||
Assert.Equal(3, view.RouteRanges.Length);
|
||||
RenderFrameCandidateRange outdoorRange = Assert.Single(
|
||||
view.RouteRanges.ToArray(),
|
||||
range => range.Route
|
||||
== RenderFrameCandidateRoute.LandscapeOutdoorStatic);
|
||||
Assert.Equal(3, outdoorRange.Count);
|
||||
Assert.Contains(
|
||||
view.RouteCandidates.Slice(
|
||||
outdoorRange.Offset,
|
||||
outdoorRange.Count).ToArray(),
|
||||
item => item.Id == buildingShell.Id);
|
||||
Assert.DoesNotContain(
|
||||
view.RouteRanges.ToArray(),
|
||||
range => range.Route
|
||||
== RenderFrameCandidateRoute.LandscapeBuildingShell);
|
||||
Assert.DoesNotContain(
|
||||
view.RouteCandidates.ToArray(),
|
||||
item => item.Id == withdrawn.Id || item.Id == shell.Id);
|
||||
Assert.Equal(
|
||||
new RenderFrameDiagnosticCounts(
|
||||
OutdoorStaticCandidates: 2,
|
||||
OutdoorStaticCandidates: 3,
|
||||
CellStaticCandidates: 1,
|
||||
DynamicCandidates: 2,
|
||||
TransformCount: 5,
|
||||
TransformCount: 6,
|
||||
OpaqueClassificationCount: 0,
|
||||
AlphaClassificationCount: 0,
|
||||
LightSetCount: 0,
|
||||
SelectionPartCount: 0,
|
||||
RouteCandidateCount: 5,
|
||||
EntityCandidateCount: 5,
|
||||
MeshPartCount: 5),
|
||||
RouteCandidateCount: 6,
|
||||
EntityCandidateCount: 6,
|
||||
MeshPartCount: 6),
|
||||
view.DiagnosticCounts);
|
||||
Assert.Equal(5, view.EntityCandidates.Length);
|
||||
Assert.Equal(5, view.MeshParts.Length);
|
||||
Assert.Equal(6, view.EntityCandidates.Length);
|
||||
Assert.Equal(6, view.MeshParts.Length);
|
||||
Assert.Same(portal, view.PortalFrame);
|
||||
Assert.Same(clip, view.ClipAssembly);
|
||||
Assert.Equal(digest, view.SourceDigest);
|
||||
|
|
@ -133,6 +152,178 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Builder_OrdersMultiSliceBuildingShellsAfterLookIns()
|
||||
{
|
||||
using var scene = new ArchRenderScene(Generation);
|
||||
RenderProjectionRecord outdoor = Record(
|
||||
0x0100_0000_0000_0101,
|
||||
RenderProjectionClass.OutdoorStatic);
|
||||
RenderProjectionRecord buildingShell = Record(
|
||||
0x0100_0000_0000_0102,
|
||||
RenderProjectionClass.OutdoorStatic,
|
||||
isBuildingShell: true,
|
||||
buildingShellAnchorCellId: Cell);
|
||||
RenderProjectionRecord lookInObject = Record(
|
||||
0x0100_0000_0000_0103,
|
||||
RenderProjectionClass.IndoorCellStatic,
|
||||
parentCell: Cell);
|
||||
scene.Apply(
|
||||
[
|
||||
RenderProjectionDelta.Register(Generation, 1, outdoor),
|
||||
RenderProjectionDelta.Register(Generation, 2, buildingShell),
|
||||
RenderProjectionDelta.Register(Generation, 3, lookInObject),
|
||||
]);
|
||||
|
||||
PortalVisibilityFrame portal = Portal(Cell);
|
||||
PortalVisibilityFrame lookIn = Portal(Cell);
|
||||
lookIn.SourceBuildingKey = 2u;
|
||||
lookIn.SourceBuildingLandblockId = Cell & 0xFFFF0000u;
|
||||
var anchorCell = new LoadedCell
|
||||
{
|
||||
CellId = Cell,
|
||||
BuildingId = 2u,
|
||||
};
|
||||
ClipFrameAssembly clip = TwoSliceClip(Cell);
|
||||
clip.LookInCellToViewSlices[new LookInClipCell(0, Cell)] =
|
||||
[clip.CellIdToViewSlices[Cell][0]];
|
||||
ViewconeCuller viewcone =
|
||||
ViewconeCuller.Build(clip, Matrix4x4.Identity);
|
||||
var exchange = new RenderFrameExchange();
|
||||
var builder = new RenderScenePViewFrameBuilder();
|
||||
RenderSceneDigest digest =
|
||||
scene.BuildDigest(new RenderSceneDigestBuffer());
|
||||
var input = new RenderScenePViewBuildInput(
|
||||
scene.OpenQuery(),
|
||||
digest,
|
||||
portal,
|
||||
clip,
|
||||
viewcone,
|
||||
[lookIn],
|
||||
[Cell],
|
||||
new DictionaryCellSource(anchorCell),
|
||||
[],
|
||||
RootIsOutdoor: true);
|
||||
|
||||
builder.Build(exchange, frameSequence: 1, in input);
|
||||
RenderFrameView view = exchange.BorrowLatest(Generation, 1);
|
||||
try
|
||||
{
|
||||
Assert.Equal(
|
||||
[
|
||||
(RenderFrameCandidateRoute.LandscapeOutdoorStatic, 0, 0u),
|
||||
(RenderFrameCandidateRoute.LandscapeOutdoorStatic, 1, 0u),
|
||||
(RenderFrameCandidateRoute.LookInObject, 0, Cell),
|
||||
(RenderFrameCandidateRoute.LandscapeBuildingShell, 0, 0u),
|
||||
(RenderFrameCandidateRoute.LandscapeBuildingShell, 1, 0u),
|
||||
(RenderFrameCandidateRoute.CellStatic, 0, 0u),
|
||||
],
|
||||
view.RouteRanges.ToArray().Select(static range =>
|
||||
(range.Route, range.RouteIndex, range.CellId)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
exchange.Release(in view);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Builder_InterleavesEachLookInWithItsOwnPackedBuildingShellRoutes()
|
||||
{
|
||||
const uint secondCell = 0xA9B40180u;
|
||||
using var scene = new ArchRenderScene(Generation);
|
||||
RenderProjectionRecord firstShell = Record(
|
||||
0x0100_0000_0000_0201,
|
||||
RenderProjectionClass.OutdoorStatic,
|
||||
isBuildingShell: true,
|
||||
buildingShellAnchorCellId: Cell);
|
||||
RenderProjectionRecord secondShell = Record(
|
||||
0x0100_0000_0000_0202,
|
||||
RenderProjectionClass.OutdoorStatic,
|
||||
isBuildingShell: true,
|
||||
buildingShellAnchorCellId: secondCell);
|
||||
RenderProjectionRecord firstObject = Record(
|
||||
0x0100_0000_0000_0203,
|
||||
RenderProjectionClass.IndoorCellStatic,
|
||||
parentCell: Cell);
|
||||
RenderProjectionRecord secondObject = Record(
|
||||
0x0100_0000_0000_0204,
|
||||
RenderProjectionClass.IndoorCellStatic,
|
||||
parentCell: secondCell);
|
||||
scene.Apply(
|
||||
[
|
||||
RenderProjectionDelta.Register(Generation, 1, firstShell),
|
||||
RenderProjectionDelta.Register(Generation, 2, secondShell),
|
||||
RenderProjectionDelta.Register(Generation, 3, firstObject),
|
||||
RenderProjectionDelta.Register(Generation, 4, secondObject),
|
||||
]);
|
||||
|
||||
PortalVisibilityFrame portal = Portal(Cell);
|
||||
PortalVisibilityFrame firstLookIn = Portal(Cell);
|
||||
firstLookIn.SourceBuildingKey = 2u;
|
||||
firstLookIn.SourceBuildingLandblockId = Cell & 0xFFFF0000u;
|
||||
PortalVisibilityFrame secondLookIn = Portal(secondCell);
|
||||
secondLookIn.SourceBuildingKey = 3u;
|
||||
secondLookIn.SourceBuildingLandblockId = secondCell & 0xFFFF0000u;
|
||||
var firstAnchor = new LoadedCell
|
||||
{
|
||||
CellId = Cell,
|
||||
BuildingId = 2u,
|
||||
};
|
||||
var secondAnchor = new LoadedCell
|
||||
{
|
||||
CellId = secondCell,
|
||||
BuildingId = 3u,
|
||||
};
|
||||
ClipFrameAssembly clip = TwoSliceClip(Cell);
|
||||
clip.LookInCellToViewSlices[new LookInClipCell(0, Cell)] =
|
||||
[clip.CellIdToViewSlices[Cell][0]];
|
||||
clip.LookInCellToViewSlices[new LookInClipCell(1, secondCell)] =
|
||||
[clip.CellIdToViewSlices[Cell][0]];
|
||||
ViewconeCuller viewcone =
|
||||
ViewconeCuller.Build(clip, Matrix4x4.Identity);
|
||||
var exchange = new RenderFrameExchange();
|
||||
var builder = new RenderScenePViewFrameBuilder();
|
||||
RenderSceneDigest digest =
|
||||
scene.BuildDigest(new RenderSceneDigestBuffer());
|
||||
var input = new RenderScenePViewBuildInput(
|
||||
scene.OpenQuery(),
|
||||
digest,
|
||||
portal,
|
||||
clip,
|
||||
viewcone,
|
||||
[firstLookIn, secondLookIn],
|
||||
[Cell],
|
||||
new DictionaryCellSource(firstAnchor, secondAnchor),
|
||||
[],
|
||||
RootIsOutdoor: true);
|
||||
|
||||
builder.Build(exchange, frameSequence: 1, in input);
|
||||
RenderFrameView view = exchange.BorrowLatest(Generation, 1);
|
||||
try
|
||||
{
|
||||
Assert.Equal(
|
||||
[
|
||||
(RenderFrameCandidateRoute.LookInObject, 0, Cell),
|
||||
(RenderFrameCandidateRoute.LandscapeBuildingShell, 0, 0u),
|
||||
(RenderFrameCandidateRoute.LandscapeBuildingShell, 1, 0u),
|
||||
(RenderFrameCandidateRoute.LookInObject, 1, secondCell),
|
||||
(RenderFrameCandidateRoute.LandscapeBuildingShell, 2, 0u),
|
||||
(RenderFrameCandidateRoute.LandscapeBuildingShell, 3, 0u),
|
||||
],
|
||||
view.RouteRanges.ToArray()
|
||||
.Where(static range =>
|
||||
range.Route is RenderFrameCandidateRoute.LookInObject
|
||||
or RenderFrameCandidateRoute.LandscapeBuildingShell)
|
||||
.Select(static range =>
|
||||
(range.Route, range.RouteIndex, range.CellId)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
exchange.Release(in view);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Builder_ReusesOrderedIndicesWhileRefreshingDirtyRecords()
|
||||
{
|
||||
|
|
@ -501,7 +692,9 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
uint? parentCell = null,
|
||||
RenderProjectionFlags flags =
|
||||
RenderProjectionFlags.Draw
|
||||
| RenderProjectionFlags.SpatiallyResident)
|
||||
| RenderProjectionFlags.SpatiallyResident,
|
||||
bool isBuildingShell = false,
|
||||
uint buildingShellAnchorCellId = 0)
|
||||
{
|
||||
uint fullCellId = parentCell ?? Landblock;
|
||||
Matrix4x4 transform = Matrix4x4.Identity;
|
||||
|
|
@ -531,14 +724,14 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
SourceId: 1,
|
||||
ParentCellId: parentCell ?? 0,
|
||||
EffectCellId: 0,
|
||||
BuildingShellAnchorCellId: 0,
|
||||
BuildingShellAnchorCellId: buildingShellAnchorCellId,
|
||||
TransformFingerprint: default,
|
||||
GeometryFingerprint: default,
|
||||
AppearanceFingerprint: default),
|
||||
new RenderEntityPayload(
|
||||
[new MeshRef((uint)id, Matrix4x4.Identity)],
|
||||
PaletteOverride: null,
|
||||
IsBuildingShell: false));
|
||||
IsBuildingShell: isBuildingShell));
|
||||
}
|
||||
|
||||
private static WorldEntity Entity(
|
||||
|
|
@ -578,6 +771,22 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
return assembly;
|
||||
}
|
||||
|
||||
private static ClipFrameAssembly TwoSliceClip(uint cellId)
|
||||
{
|
||||
var first = new ClipViewSlice(
|
||||
Slot: 0,
|
||||
NdcAabb: new Vector4(-1, -1, 0, 1),
|
||||
Planes: []);
|
||||
var second = new ClipViewSlice(
|
||||
Slot: 1,
|
||||
NdcAabb: new Vector4(0, -1, 1, 1),
|
||||
Planes: []);
|
||||
var assembly = new ClipFrameAssembly();
|
||||
assembly.SetOutsideViewSlices([first, second]);
|
||||
assembly.CellIdToViewSlices[cellId] = [first];
|
||||
return assembly;
|
||||
}
|
||||
|
||||
private static (
|
||||
uint LandblockId,
|
||||
Vector3 AabbMin,
|
||||
|
|
@ -602,4 +811,15 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
|
||||
public LoadedCell? Find(uint cellId) => null;
|
||||
}
|
||||
|
||||
private sealed class DictionaryCellSource : IRetailPViewCellSource
|
||||
{
|
||||
private readonly Dictionary<uint, LoadedCell> _cells;
|
||||
|
||||
public DictionaryCellSource(params LoadedCell[] cells) =>
|
||||
_cells = cells.ToDictionary(static cell => cell.CellId);
|
||||
|
||||
public LoadedCell? Find(uint cellId) =>
|
||||
_cells.TryGetValue(cellId, out LoadedCell? cell) ? cell : null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ public sealed class RetailPViewPassExecutorTests
|
|||
[
|
||||
"begin",
|
||||
"assemble",
|
||||
"append-look-in-clips",
|
||||
"prepare-clip:3",
|
||||
"indoor-routing",
|
||||
"prepare-cells",
|
||||
|
|
@ -33,6 +34,7 @@ public sealed class RetailPViewPassExecutorTests
|
|||
"terrain-clip",
|
||||
"clear-routing",
|
||||
"landscape-late",
|
||||
"unattached-particles",
|
||||
"landscape-alpha",
|
||||
"indoor-routing",
|
||||
"indoor-routing",
|
||||
|
|
@ -272,6 +274,88 @@ public sealed class RetailPViewPassExecutorTests
|
|||
executor);
|
||||
|
||||
Assert.Contains("look-in-punch", executor.Operations);
|
||||
AssertAppearsInOrder(
|
||||
string.Join('|', executor.Operations),
|
||||
"landscape-early",
|
||||
"unattached-particles",
|
||||
"landscape-static-particles",
|
||||
"landscape-alpha",
|
||||
"look-in-punch",
|
||||
"landscape-late",
|
||||
"landscape-alpha",
|
||||
"interior-depth-clear");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DrawInside_repaints_the_exterior_building_shell_after_its_look_in()
|
||||
{
|
||||
var renderer = new RetailPViewRenderer();
|
||||
using var executor = new RecordingExecutor();
|
||||
LoadedCell root = InteriorWithExit(0xA9B40100u);
|
||||
root.BuildingId = 1u;
|
||||
LoadedCell[] building = NearbyTwoCellBuilding();
|
||||
WorldEntity exteriorShell = Entity(
|
||||
0x700u,
|
||||
isBuildingShell: true,
|
||||
buildingShellAnchorCellId: building[0].CellId);
|
||||
|
||||
renderer.DrawInside(
|
||||
Frame(
|
||||
root,
|
||||
[exteriorShell],
|
||||
nearbyBuildingCells: building,
|
||||
additionalCells: building),
|
||||
executor);
|
||||
|
||||
AssertAppearsInOrder(
|
||||
string.Join('|', executor.Operations),
|
||||
"landscape-early",
|
||||
"look-in-punch",
|
||||
"landscape-building-shell",
|
||||
"landscape-late");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DrawInside_pairs_each_look_in_with_only_its_own_shell_and_alpha_barrier()
|
||||
{
|
||||
var renderer = new RetailPViewRenderer();
|
||||
using var executor = new RecordingExecutor();
|
||||
LoadedCell root = InteriorWithExit(0xA9B40100u);
|
||||
root.BuildingId = 1u;
|
||||
LoadedCell[] first = NearbyTwoCellBuilding();
|
||||
LoadedCell[] second = NearbyTwoCellBuilding(
|
||||
0xA9B40172u,
|
||||
0xA9B40173u,
|
||||
buildingId: 3u);
|
||||
LoadedCell[] buildings = [.. first, .. second];
|
||||
WorldEntity[] shells =
|
||||
[
|
||||
Entity(
|
||||
0x700u,
|
||||
isBuildingShell: true,
|
||||
buildingShellAnchorCellId: first[0].CellId),
|
||||
Entity(
|
||||
0x701u,
|
||||
isBuildingShell: true,
|
||||
buildingShellAnchorCellId: second[0].CellId),
|
||||
];
|
||||
|
||||
renderer.DrawInside(
|
||||
Frame(
|
||||
root,
|
||||
shells,
|
||||
nearbyBuildingCells: buildings,
|
||||
additionalCells: buildings),
|
||||
executor);
|
||||
|
||||
AssertAppearsInOrder(
|
||||
string.Join('|', executor.Operations),
|
||||
"look-in-punch",
|
||||
"landscape-building-shell",
|
||||
"landscape-static-particles",
|
||||
"landscape-alpha",
|
||||
"look-in-punch",
|
||||
"landscape-building-shell");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -429,20 +513,21 @@ public sealed class RetailPViewPassExecutorTests
|
|||
return cell;
|
||||
}
|
||||
|
||||
private static LoadedCell[] NearbyTwoCellBuilding()
|
||||
private static LoadedCell[] NearbyTwoCellBuilding(
|
||||
uint vestibuleId = 0xA9B40170u,
|
||||
uint roomId = 0xA9B40171u,
|
||||
uint buildingId = 2u)
|
||||
{
|
||||
const uint vestibuleId = 0xA9B40170u;
|
||||
const uint roomId = 0xA9B40171u;
|
||||
var vestibule = new LoadedCell
|
||||
{
|
||||
CellId = vestibuleId,
|
||||
BuildingId = 2u,
|
||||
BuildingId = buildingId,
|
||||
WorldTransform = Matrix4x4.Identity,
|
||||
InverseWorldTransform = Matrix4x4.Identity,
|
||||
Portals =
|
||||
[
|
||||
new CellPortalInfo(0xFFFF, 0, 0, 0),
|
||||
new CellPortalInfo(0x0171, 1, 0, 0),
|
||||
new CellPortalInfo((ushort)(roomId & 0xFFFFu), 1, 0, 0),
|
||||
],
|
||||
ClipPlanes =
|
||||
[
|
||||
|
|
@ -472,10 +557,17 @@ public sealed class RetailPViewPassExecutorTests
|
|||
var room = new LoadedCell
|
||||
{
|
||||
CellId = roomId,
|
||||
BuildingId = 2u,
|
||||
BuildingId = buildingId,
|
||||
WorldTransform = Matrix4x4.Identity,
|
||||
InverseWorldTransform = Matrix4x4.Identity,
|
||||
Portals = [new CellPortalInfo(0x0170, 0, 0, 1)],
|
||||
Portals =
|
||||
[
|
||||
new CellPortalInfo(
|
||||
(ushort)(vestibuleId & 0xFFFFu),
|
||||
0,
|
||||
0,
|
||||
1),
|
||||
],
|
||||
};
|
||||
room.PortalPolygons.Add(
|
||||
[
|
||||
|
|
@ -490,7 +582,9 @@ public sealed class RetailPViewPassExecutorTests
|
|||
private static WorldEntity Entity(
|
||||
uint id,
|
||||
uint serverGuid = 0,
|
||||
uint? parentCellId = null) => new()
|
||||
uint? parentCellId = null,
|
||||
bool isBuildingShell = false,
|
||||
uint? buildingShellAnchorCellId = null) => new()
|
||||
{
|
||||
Id = id,
|
||||
ServerGuid = serverGuid,
|
||||
|
|
@ -499,6 +593,8 @@ public sealed class RetailPViewPassExecutorTests
|
|||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = [new MeshRef(1u, Matrix4x4.Identity)],
|
||||
ParentCellId = parentCellId,
|
||||
IsBuildingShell = isBuildingShell,
|
||||
BuildingShellAnchorCellId = buildingShellAnchorCellId,
|
||||
};
|
||||
|
||||
private static void AssertAppearsInOrder(string source, params string[] needles)
|
||||
|
|
@ -566,6 +662,17 @@ public sealed class RetailPViewPassExecutorTests
|
|||
return ClipFrameAssembler.Assemble(_clipFrame, portalFrame, reuseAssembly);
|
||||
}
|
||||
|
||||
public void AppendLookInClipFrames(
|
||||
IReadOnlyList<PortalVisibilityFrame> lookInFrames,
|
||||
ClipFrameAssembly assembly)
|
||||
{
|
||||
Operations.Add("append-look-in-clips");
|
||||
ClipFrameAssembler.AppendLookInFrames(
|
||||
_clipFrame,
|
||||
lookInFrames,
|
||||
assembly);
|
||||
}
|
||||
|
||||
public void PrepareClipFrame(int terrainUploadCount) =>
|
||||
Operations.Add($"prepare-clip:{terrainUploadCount}");
|
||||
|
||||
|
|
@ -574,6 +681,8 @@ public sealed class RetailPViewPassExecutorTests
|
|||
|
||||
public void ClearClipRouting() => Operations.Add("clear-routing");
|
||||
public void UseIndoorMembershipOnlyRouting() => Operations.Add("indoor-routing");
|
||||
public void UseCellPortalViewRouting(uint cellId, ClipViewSlice slice) =>
|
||||
Operations.Add($"cell-portal-routing:{cellId:X8}:{slice.Slot}");
|
||||
public void PrepareCellBatches(RetailPViewFrameInput frame, HashSet<uint> visibleCellIds) =>
|
||||
Operations.Add("prepare-cells");
|
||||
public void DrawOpaqueCellShells(HashSet<uint> cellIds) => Operations.Add("opaque-shells");
|
||||
|
|
@ -624,10 +733,37 @@ public sealed class RetailPViewPassExecutorTests
|
|||
request.TupleLandblockId);
|
||||
}
|
||||
}
|
||||
|
||||
public void DrawLandscapeStaticParticles(
|
||||
RetailPViewFrameInput frame,
|
||||
RetailPViewLandscapeStaticParticleContext context) =>
|
||||
Operations.Add("landscape-static-particles");
|
||||
public void DrawLandscapeBuildingShellSlice(
|
||||
RetailPViewFrameInput frame,
|
||||
RetailPViewLandscapeBuildingShellSliceContext context)
|
||||
{
|
||||
Operations.Add("landscape-building-shell");
|
||||
if (context.EntityDraw is RenderFrameEntityDrawRequest request)
|
||||
{
|
||||
RenderFrameView view = request.View;
|
||||
DrawEntityRoute(
|
||||
frame.Camera,
|
||||
in view,
|
||||
request.Route,
|
||||
request.RouteIndex,
|
||||
request.CellId,
|
||||
request.TupleLandblockId);
|
||||
}
|
||||
}
|
||||
public void ClearInteriorDepth() => Operations.Add("interior-depth-clear");
|
||||
public void DrawExitPortalMask(RetailPViewFrameInput frame, RetailPViewCellSliceContext context) => Operations.Add("exit-mask");
|
||||
public void DrawLookInPortalPunch(RetailPViewFrameInput frame, RetailPViewCellSliceContext context) => Operations.Add("look-in-punch");
|
||||
public void DrawUnattachedSceneParticles(RetailPViewFrameInput frame) => Operations.Add("unattached-particles");
|
||||
public void DrawLookInPortalPunch(
|
||||
RetailPViewFrameInput frame,
|
||||
RetailPViewCellSliceContext context,
|
||||
int portalIndex) => Operations.Add("look-in-punch");
|
||||
public void DrawUnattachedSceneParticles(
|
||||
RetailPViewFrameInput frame,
|
||||
ClipViewSlice slice) => Operations.Add("unattached-particles");
|
||||
public void FlushLandscapeAlpha() => Operations.Add("landscape-alpha");
|
||||
public void DrawCellParticles(RetailPViewFrameInput frame, RetailPViewCellSliceContext context) => Operations.Add("cell-particles");
|
||||
public void DrawDynamicsParticles(
|
||||
|
|
|
|||
|
|
@ -105,10 +105,9 @@ public class RhiVertexLayoutStrideTests
|
|||
layout.StrideOf(0));
|
||||
Assert.Equal(GpuVertexInputRate.Vertex, layout.InputRateOf(0));
|
||||
|
||||
// Binding 1 is a mat4 model plus an RGBA colour, written as loose floats
|
||||
// by WriteMeshGpuInstance.
|
||||
// Binding 1 is the exact mesh-particle record: model, RGBA, clip slot.
|
||||
Assert.Equal(
|
||||
(uint)(ParticleRenderer.MeshInstanceFloats * sizeof(float)),
|
||||
(uint)Unsafe.SizeOf<ParticleRenderer.MeshParticleGpuInstance>(),
|
||||
layout.StrideOf(1));
|
||||
Assert.Equal(GpuVertexInputRate.Instance, layout.InputRateOf(1));
|
||||
}
|
||||
|
|
|
|||
226
tests/AcDream.App.Tests/Rendering/SanctuaryPortalSeamTests.cs
Normal file
226
tests/AcDream.App.Tests/Rendering/SanctuaryPortalSeamTests.cs
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Options;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Sanctuary cathedral seam captured 2026-08-27. The stationary player is in
|
||||
/// F4180104 while chase-camera zoom crosses the coincident outside portals at
|
||||
/// world Y=48 and the viewer root switches between two separate buildings:
|
||||
/// B3={0103,0104,0105} and B4={0106..0111}. The opposite cathedral half must
|
||||
/// remain available through the interior-root building look-in on both sides.
|
||||
/// </summary>
|
||||
[Trait("Lane", "InstalledDat")]
|
||||
public sealed class SanctuaryPortalSeamTests
|
||||
{
|
||||
private const uint Landblock = 0xF4180000u;
|
||||
private const uint Cell0104 = Landblock | 0x0104u;
|
||||
private const uint Cell0106 = Landblock | 0x0106u;
|
||||
|
||||
private static Matrix4x4 ViewProjection(Vector3 eye)
|
||||
{
|
||||
// Derived from the two exact flap-sweep points. Extending their zoom
|
||||
// ray reaches the stable chase target at the player's head.
|
||||
var target = new Vector3(36.033f, 49.638f, 171.353f);
|
||||
var view = Matrix4x4.CreateLookAt(eye, target, Vector3.UnitZ);
|
||||
var projection = Matrix4x4.CreatePerspectiveFieldOfView(
|
||||
1.2f, 893f / 522f, 1f, 5000f);
|
||||
return view * projection;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<LoadedCell> Cells(
|
||||
Dictionary<uint, LoadedCell> cells,
|
||||
uint firstLow,
|
||||
uint lastLow)
|
||||
{
|
||||
var result = new List<LoadedCell>();
|
||||
for (uint low = firstLow; low <= lastLow; low++)
|
||||
result.Add(cells[Landblock | low]);
|
||||
return result;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CapturedZoomHandoff_BothRootsBuildTheExactOppositeCathedralSeed()
|
||||
{
|
||||
string? datDir = CornerFloodReplayTests.ResolveDatDir();
|
||||
if (datDir is null)
|
||||
{
|
||||
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.");
|
||||
}
|
||||
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
LandBlockInfo info = Assert.IsType<LandBlockInfo>(dats.Get<LandBlockInfo>(Landblock | 0xfffeu));
|
||||
Dictionary<uint, LoadedCell> cells =
|
||||
Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, Landblock);
|
||||
LoadedCell? Lookup(uint id) => cells.TryGetValue(id, out LoadedCell? cell) ? cell : null;
|
||||
|
||||
IReadOnlyList<LoadedCell> building3 = Cells(cells, 0x0103u, 0x0105u);
|
||||
IReadOnlyList<LoadedCell> building4 = Cells(cells, 0x0106u, 0x0111u);
|
||||
Assert.Equal(0x01001FB2u, info.Buildings[3].ModelId);
|
||||
Assert.Equal(0x01001FB3u, info.Buildings[4].ModelId);
|
||||
|
||||
var captures = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
Name = "near/root0104",
|
||||
Eye = new Vector3(32.742317f, 48.034306f, 172.447845f),
|
||||
Root = cells[Cell0104],
|
||||
Opposite = building4,
|
||||
ExpectedOppositeCell = Cell0106,
|
||||
ExpectedSeedPortal = 2,
|
||||
OppositeBuildingIndex = 4,
|
||||
},
|
||||
new
|
||||
{
|
||||
Name = "far/root0106",
|
||||
Eye = new Vector3(32.308865f, 47.823200f, 172.592255f),
|
||||
Root = cells[Cell0106],
|
||||
Opposite = building3,
|
||||
ExpectedOppositeCell = Cell0104,
|
||||
ExpectedSeedPortal = 0,
|
||||
OppositeBuildingIndex = 3,
|
||||
},
|
||||
};
|
||||
|
||||
foreach (var capture in captures)
|
||||
{
|
||||
Matrix4x4 viewProjection = ViewProjection(capture.Eye);
|
||||
PortalVisibilityFrame main = PortalVisibilityBuilder.Build(
|
||||
capture.Root, capture.Eye, Lookup, viewProjection);
|
||||
PortalVisibilityFrame lookIn = PortalVisibilityBuilder.ConstructViewBuilding(
|
||||
capture.Opposite,
|
||||
capture.Eye,
|
||||
Lookup,
|
||||
viewProjection,
|
||||
maxSeedDistance: float.PositiveInfinity,
|
||||
seedRegion: main.OutsideView.Polygons);
|
||||
|
||||
Assert.Contains(capture.ExpectedOppositeCell, lookIn.OrderedVisibleCells);
|
||||
Assert.True(
|
||||
lookIn.CellViews.TryGetValue(capture.ExpectedOppositeCell, out CellView? view)
|
||||
&& view.Polygons.Count > 0,
|
||||
$"{capture.Name} must retain a clipped aperture for the opposite cathedral half");
|
||||
ExteriorPortalSeed seed = Assert.Single(lookIn.ExteriorSeedPortals);
|
||||
Assert.Equal(capture.ExpectedOppositeCell, seed.CellId);
|
||||
Assert.Equal(capture.ExpectedSeedPortal, seed.PortalIndex);
|
||||
Assert.NotEmpty(seed.View.Polygons);
|
||||
|
||||
using ClipFrame clipFrame = ClipFrame.NoClip();
|
||||
ClipFrameAssembly assembly = ClipFrameAssembler.Assemble(
|
||||
clipFrame,
|
||||
main);
|
||||
ClipFrameAssembler.AppendLookInFrames(
|
||||
clipFrame,
|
||||
[lookIn],
|
||||
assembly);
|
||||
ClipViewSlice[] nestedSlices =
|
||||
assembly.LookInCellToViewSlices[
|
||||
new LookInClipCell(0, capture.ExpectedOppositeCell)];
|
||||
Assert.Equal(view.Polygons.Count, nestedSlices.Length);
|
||||
Assert.All(nestedSlices, slice => Assert.True(slice.Slot > 0));
|
||||
|
||||
Assert.Contains(
|
||||
info.Buildings[capture.OppositeBuildingIndex].Portals,
|
||||
portal => portal.OtherCellId == (capture.ExpectedOppositeCell & 0xffffu)
|
||||
&& portal.OtherPortalId == capture.ExpectedSeedPortal);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReportedLateralTransition_BothRootsKeepTheOppositeFacadePortal()
|
||||
{
|
||||
string? datDir = CornerFloodReplayTests.ResolveDatDir();
|
||||
if (datDir is null)
|
||||
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory.");
|
||||
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
Dictionary<uint, LoadedCell> cells =
|
||||
Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, Landblock);
|
||||
LoadedCell? Lookup(uint id) => cells.TryGetValue(id, out LoadedCell? cell) ? cell : null;
|
||||
|
||||
static (PortalVisibilityFrame Main, PortalVisibilityFrame LookIn) Build(
|
||||
Vector3 eye,
|
||||
Vector3 target,
|
||||
LoadedCell root,
|
||||
IReadOnlyList<LoadedCell> opposite,
|
||||
Func<uint, LoadedCell?> lookup)
|
||||
{
|
||||
Matrix4x4 viewProjection = Matrix4x4.CreateLookAt(eye, target, Vector3.UnitZ)
|
||||
* Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 3f, 1.6f, 0.1f, 5000f);
|
||||
PortalVisibilityFrame main = PortalVisibilityBuilder.Build(
|
||||
root, eye, lookup, viewProjection);
|
||||
PortalVisibilityFrame lookIn = PortalVisibilityBuilder.ConstructViewBuilding(
|
||||
opposite, eye, lookup, viewProjection, seedRegion: main.OutsideView.Polygons);
|
||||
return (main, lookIn);
|
||||
}
|
||||
|
||||
(PortalVisibilityFrame Main, PortalVisibilityFrame LookIn) from0106 = Build(
|
||||
new Vector3(31.189594f, 46.280170f, 171.783646f),
|
||||
new Vector3(32.787731f, 46.275948f, 171.354993f),
|
||||
cells[Cell0106],
|
||||
Cells(cells, 0x0103u, 0x0105u),
|
||||
Lookup);
|
||||
(PortalVisibilityFrame Main, PortalVisibilityFrame LookIn) from0104 = Build(
|
||||
new Vector3(31.198704f, 49.733287f, 171.783646f),
|
||||
new Vector3(32.796841f, 49.729065f, 171.354993f),
|
||||
cells[Cell0104],
|
||||
Cells(cells, 0x0106u, 0x0111u),
|
||||
Lookup);
|
||||
|
||||
Assert.Equal([Cell0106, Landblock | 0x010Fu], from0106.Main.OrderedVisibleCells);
|
||||
Assert.Equal([Cell0104], from0106.LookIn.OrderedVisibleCells);
|
||||
ExteriorPortalSeed seed0104 = Assert.Single(from0106.LookIn.ExteriorSeedPortals);
|
||||
Assert.Equal(Cell0104, seed0104.CellId);
|
||||
Assert.Equal(0, seed0104.PortalIndex);
|
||||
Assert.NotEmpty(seed0104.View.Polygons);
|
||||
|
||||
Assert.Equal([Cell0104], from0104.Main.OrderedVisibleCells);
|
||||
Assert.Equal([Cell0106, Landblock | 0x010Fu], from0104.LookIn.OrderedVisibleCells);
|
||||
ExteriorPortalSeed seed0106 = Assert.Single(from0104.LookIn.ExteriorSeedPortals);
|
||||
Assert.Equal(Cell0106, seed0106.CellId);
|
||||
Assert.Equal(2, seed0106.PortalIndex);
|
||||
Assert.NotEmpty(seed0106.View.Polygons);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReportedExactSteamSeam_Root0104KeepsTheOppositeCathedralPortal()
|
||||
{
|
||||
string? datDir = CornerFloodReplayTests.ResolveDatDir();
|
||||
if (datDir is null)
|
||||
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory.");
|
||||
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
Dictionary<uint, LoadedCell> cells =
|
||||
Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, Landblock);
|
||||
LoadedCell? Lookup(uint id) => cells.TryGetValue(id, out LoadedCell? cell) ? cell : null;
|
||||
|
||||
// Live ACDREAM_PROBE_CELL capture for the user-reported stationary
|
||||
// frame at player [32.792290, 48.000618, 169.804993]. The chase eye is
|
||||
// only ~4 mm across the coincident 0104/0106 exterior portal plane.
|
||||
var eye = new Vector3(31.189665f, 48.004852f, 171.784988f);
|
||||
var target = new Vector3(32.792290f, 48.000618f, 171.354993f);
|
||||
Matrix4x4 viewProjection = Matrix4x4.CreateLookAt(eye, target, Vector3.UnitZ)
|
||||
* Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1555f / 1019f, 1f, 5000f);
|
||||
|
||||
PortalVisibilityFrame main = PortalVisibilityBuilder.Build(
|
||||
cells[Cell0104], eye, Lookup, viewProjection);
|
||||
PortalVisibilityFrame lookIn = PortalVisibilityBuilder.ConstructViewBuilding(
|
||||
Cells(cells, 0x0106u, 0x0111u),
|
||||
eye,
|
||||
Lookup,
|
||||
viewProjection,
|
||||
seedRegion: main.OutsideView.Polygons);
|
||||
|
||||
Assert.Contains(Cell0106, lookIn.OrderedVisibleCells);
|
||||
ExteriorPortalSeed seed = Assert.Single(lookIn.ExteriorSeedPortals);
|
||||
Assert.Equal(Cell0106, seed.CellId);
|
||||
Assert.Equal(2, seed.PortalIndex);
|
||||
Assert.NotEmpty(seed.View.Polygons);
|
||||
}
|
||||
}
|
||||
|
|
@ -213,6 +213,9 @@ public sealed class WorldRenderFrameBuilderTests
|
|||
Assert.False(result.PlayerSeenOutside);
|
||||
Assert.True(result.RootSeenOutside);
|
||||
Assert.True(result.RenderSky);
|
||||
Assert.False(result.CameraInsideEnclosedCell);
|
||||
Assert.True(result.PlayerOrCameraInsideEnclosedCell);
|
||||
Assert.True(result.IsAtmosphericallyOutdoor);
|
||||
Assert.Equal(playerCellId, result.PlayerCellId);
|
||||
Assert.Equal(viewerCellId, result.ViewerCellId);
|
||||
Assert.Equal(camera.Position, result.ViewerEyePosition);
|
||||
|
|
@ -245,6 +248,9 @@ public sealed class WorldRenderFrameBuilderTests
|
|||
Assert.False(result.PlayerInsideCell);
|
||||
Assert.False(result.CameraInsideCell);
|
||||
Assert.True(result.RenderSky);
|
||||
Assert.False(result.CameraInsideEnclosedCell);
|
||||
Assert.False(result.PlayerOrCameraInsideEnclosedCell);
|
||||
Assert.True(result.IsAtmosphericallyOutdoor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -290,7 +290,7 @@ public sealed class WorldSceneRendererTests
|
|||
WorldRenderFrameOutcome result = rig.Renderer.Render(default);
|
||||
|
||||
Assert.True(result.NormalWorldDrawn);
|
||||
Assert.Contains("particles:pviewScoped+unattached", rig.Calls);
|
||||
Assert.Contains("particles:pviewScoped", rig.Calls);
|
||||
Assert.Same(rig.PView.OutdoorSceneParticleEntityIds, rig.Passes.ParticleOwners);
|
||||
Assert.Equal([0xCAFEu], rig.Passes.ParticleOwners);
|
||||
Assert.DoesNotContain("flat:weather", rig.Calls);
|
||||
|
|
@ -809,9 +809,7 @@ public sealed class WorldSceneRendererTests
|
|||
string kind = clipRoot switch
|
||||
{
|
||||
null => "global",
|
||||
{ IsOutdoorNode: true } => currentSignature == "none"
|
||||
? "unattached"
|
||||
: currentSignature + "+unattached",
|
||||
{ IsOutdoorNode: true } => currentSignature,
|
||||
_ => currentSignature,
|
||||
};
|
||||
calls.Add($"particles:{kind}");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue