feat(render): portal-based EnvCell visibility (Step 4)
Port ACME's EnvCellManager portal visibility system: - New CellVisibility class: BFS portal traversal from camera cell, portal-side clip-plane test, FindCameraCell with grace period - LoadedCell data populated during streaming (portals, clip planes, world/inverse transforms, local AABB from CellStruct vertices) - WorldEntity.ParentCellId tags interior entities for filtering - InstancedMeshRenderer.Draw accepts optional visibleCellIds set — interior entities whose parent cell isn't visible are skipped - Conditional depth clear between terrain and static mesh when camera is inside a cell (ACME GameScene.cs pattern) When camera is outdoors, all interiors render (visibleCellIds=null). When camera enters a building, only BFS-reachable cells render. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
25090b6fc9
commit
cffc3ee343
4 changed files with 589 additions and 16 deletions
|
|
@ -37,6 +37,9 @@ public sealed class GameWindow : IDisposable
|
|||
// Phase B.3: physics engine — populated from the streaming pipeline.
|
||||
private readonly AcDream.Core.Physics.PhysicsEngine _physicsEngine = new();
|
||||
|
||||
// Step 4: portal-based interior cell visibility.
|
||||
private readonly CellVisibility _cellVisibility = new();
|
||||
|
||||
// Phase A.1 hotfix: DatCollection is NOT thread-safe. The streaming worker
|
||||
// thread and the render thread both read dats (BuildLandblockForStreaming
|
||||
// on the worker; ApplyLoadedTerrain + live-spawn handlers on the render
|
||||
|
|
@ -62,6 +65,10 @@ public sealed class GameWindow : IDisposable
|
|||
uint, System.Collections.Generic.IReadOnlyList<AcDream.Core.Meshing.GfxObjSubMesh>>
|
||||
_pendingCellMeshes = new();
|
||||
|
||||
// Step 4: pending LoadedCell objects built on the worker thread, drained
|
||||
// to _cellVisibility on the render thread in ApplyLoadedTerrain.
|
||||
private readonly System.Collections.Concurrent.ConcurrentBag<LoadedCell> _pendingCells = new();
|
||||
|
||||
/// <summary>
|
||||
/// Phase 6.4: per-entity animation playback state for entities whose
|
||||
/// MotionTable resolved to a real cycle. The render loop ticks each
|
||||
|
|
@ -397,6 +404,7 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
_terrain?.RemoveLandblock(id);
|
||||
_physicsEngine.RemoveLandblock(id);
|
||||
_cellVisibility.RemoveLandblock((id >> 16) & 0xFFFFu);
|
||||
});
|
||||
|
||||
// Phase 4.7: optional live-mode startup. Connect to the ACE server,
|
||||
|
|
@ -1318,21 +1326,18 @@ public sealed class GameWindow : IDisposable
|
|||
if (envCell is null) continue;
|
||||
|
||||
// Phase 7.1: build and register room geometry for this EnvCell.
|
||||
DatReaderWriter.Types.CellStruct? cellStruct = null;
|
||||
if (envCell.EnvironmentId != 0)
|
||||
{
|
||||
var environment = _dats.Get<DatReaderWriter.DBObjs.Environment>(0x0D000000u | envCell.EnvironmentId);
|
||||
if (environment is not null
|
||||
&& environment.Cells.TryGetValue(envCell.CellStructure, out var cellStruct))
|
||||
&& environment.Cells.TryGetValue(envCell.CellStructure, out cellStruct))
|
||||
{
|
||||
var cellSubMeshes = AcDream.Core.Meshing.CellMesh.Build(envCell, cellStruct, _dats);
|
||||
if (cellSubMeshes.Count > 0)
|
||||
{
|
||||
// Store in the pending dict so ApplyLoadedTerrain can upload on
|
||||
// the render thread. The key is the EnvCell dat id — same key
|
||||
// used in the MeshRef below so EnsureUploaded can find it.
|
||||
_pendingCellMeshes[envCellId] = cellSubMeshes;
|
||||
|
||||
// Z lift: 2 cm to avoid depth-fighting with terrain polygon.
|
||||
var cellOrigin = envCell.Position.Origin + lbOffset
|
||||
+ new System.Numerics.Vector3(0f, 0f, 0.02f);
|
||||
var cellTransform =
|
||||
|
|
@ -1348,8 +1353,12 @@ public sealed class GameWindow : IDisposable
|
|||
Position = System.Numerics.Vector3.Zero,
|
||||
Rotation = System.Numerics.Quaternion.Identity,
|
||||
MeshRefs = new[] { cellMeshRef },
|
||||
ParentCellId = envCellId,
|
||||
};
|
||||
result.Add(cellEntity);
|
||||
|
||||
// Step 4: build LoadedCell for portal visibility.
|
||||
BuildLoadedCell(envCellId, envCell, cellStruct, cellOrigin, cellTransform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1398,6 +1407,7 @@ public sealed class GameWindow : IDisposable
|
|||
Position = worldPos,
|
||||
Rotation = worldRot,
|
||||
MeshRefs = meshRefs,
|
||||
ParentCellId = envCellId,
|
||||
};
|
||||
result.Add(hydrated);
|
||||
}
|
||||
|
|
@ -1428,6 +1438,112 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Step 4: build a <see cref="LoadedCell"/> for portal visibility and queue it
|
||||
/// for render-thread registration. Called from the worker thread during
|
||||
/// <see cref="BuildInteriorEntitiesForStreaming"/>.
|
||||
/// </summary>
|
||||
private void BuildLoadedCell(
|
||||
uint envCellId,
|
||||
DatReaderWriter.DBObjs.EnvCell envCell,
|
||||
DatReaderWriter.Types.CellStruct cellStruct,
|
||||
System.Numerics.Vector3 cellOrigin,
|
||||
System.Numerics.Matrix4x4 cellTransform)
|
||||
{
|
||||
System.Numerics.Matrix4x4.Invert(cellTransform, out var inverse);
|
||||
|
||||
// Compute local AABB from CellStruct vertices.
|
||||
var boundsMin = new System.Numerics.Vector3(float.MaxValue);
|
||||
var boundsMax = new System.Numerics.Vector3(float.MinValue);
|
||||
foreach (var kvp in cellStruct.VertexArray.Vertices)
|
||||
{
|
||||
var v = kvp.Value;
|
||||
var pos = new System.Numerics.Vector3(v.Origin.X, v.Origin.Y, v.Origin.Z);
|
||||
boundsMin = System.Numerics.Vector3.Min(boundsMin, pos);
|
||||
boundsMax = System.Numerics.Vector3.Max(boundsMax, pos);
|
||||
}
|
||||
if (boundsMin.X == float.MaxValue)
|
||||
{
|
||||
boundsMin = System.Numerics.Vector3.Zero;
|
||||
boundsMax = System.Numerics.Vector3.Zero;
|
||||
}
|
||||
|
||||
// Build portal list and clip planes from CellPortals.
|
||||
var portals = new List<CellPortalInfo>();
|
||||
var clipPlanes = new List<PortalClipPlane>();
|
||||
|
||||
// Compute cell centroid in local space for InsideSide determination.
|
||||
var centroid = (boundsMin + boundsMax) * 0.5f;
|
||||
|
||||
foreach (var portal in envCell.CellPortals)
|
||||
{
|
||||
portals.Add(new CellPortalInfo(
|
||||
portal.OtherCellId,
|
||||
portal.PolygonId,
|
||||
(ushort)portal.Flags));
|
||||
|
||||
// Build clip plane from the portal polygon.
|
||||
if (cellStruct.Polygons.TryGetValue(portal.PolygonId, out var poly)
|
||||
&& poly.VertexIds.Count >= 3)
|
||||
{
|
||||
// Get first 3 vertices in local space for the plane.
|
||||
System.Numerics.Vector3 p0 = System.Numerics.Vector3.Zero,
|
||||
p1 = System.Numerics.Vector3.Zero,
|
||||
p2 = System.Numerics.Vector3.Zero;
|
||||
bool found = true;
|
||||
if (cellStruct.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[0], out var v0))
|
||||
p0 = new System.Numerics.Vector3(v0.Origin.X, v0.Origin.Y, v0.Origin.Z);
|
||||
else found = false;
|
||||
if (found && cellStruct.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[1], out var v1))
|
||||
p1 = new System.Numerics.Vector3(v1.Origin.X, v1.Origin.Y, v1.Origin.Z);
|
||||
else found = false;
|
||||
if (found && cellStruct.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[2], out var v2))
|
||||
p2 = new System.Numerics.Vector3(v2.Origin.X, v2.Origin.Y, v2.Origin.Z);
|
||||
else found = false;
|
||||
|
||||
if (found)
|
||||
{
|
||||
var normal = System.Numerics.Vector3.Normalize(
|
||||
System.Numerics.Vector3.Cross(p1 - p0, p2 - p0));
|
||||
float d = -System.Numerics.Vector3.Dot(normal, p0);
|
||||
|
||||
// Determine InsideSide: which side of the plane the cell centroid is on.
|
||||
// If centroid dot > 0 → inside is positive half-space (InsideSide=0).
|
||||
float centroidDot = System.Numerics.Vector3.Dot(normal, centroid) + d;
|
||||
int insideSide = centroidDot >= 0 ? 0 : 1;
|
||||
|
||||
clipPlanes.Add(new PortalClipPlane
|
||||
{
|
||||
Normal = normal,
|
||||
D = d,
|
||||
InsideSide = insideSide,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
clipPlanes.Add(default);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
clipPlanes.Add(default);
|
||||
}
|
||||
}
|
||||
|
||||
var loaded = new LoadedCell
|
||||
{
|
||||
CellId = envCellId,
|
||||
WorldPosition = cellOrigin,
|
||||
WorldTransform = cellTransform,
|
||||
InverseWorldTransform = inverse,
|
||||
LocalBoundsMin = boundsMin,
|
||||
LocalBoundsMax = boundsMax,
|
||||
Portals = portals,
|
||||
ClipPlanes = clipPlanes,
|
||||
};
|
||||
_pendingCells.Add(loaded);
|
||||
}
|
||||
|
||||
private void ApplyLoadedTerrainLocked(AcDream.Core.World.LoadedLandblock lb)
|
||||
{
|
||||
if (_terrain is null || _dats is null || _blendCtx is null
|
||||
|
|
@ -1448,6 +1564,10 @@ public sealed class GameWindow : IDisposable
|
|||
lb.Heightmap, lbXu, lbYu, _heightTable, _blendCtx, _surfaceCache);
|
||||
_terrain.AddLandblock(lb.LandblockId, meshData, origin);
|
||||
|
||||
// Step 4: drain pending LoadedCells from the worker thread.
|
||||
while (_pendingCells.TryTake(out var cell))
|
||||
_cellVisibility.AddCell(cell);
|
||||
|
||||
// Compute the per-landblock AABB for frustum culling. XY from the
|
||||
// landblock's world origin + 192 footprint. Z from the terrain vertex
|
||||
// range padded +50 above (for trees/buildings) and -10 below (for
|
||||
|
|
@ -1832,8 +1952,24 @@ public sealed class GameWindow : IDisposable
|
|||
playerLb = (uint)((plx << 24) | (ply << 16) | 0xFFFF);
|
||||
}
|
||||
_terrain?.Draw(camera, frustum, neverCullLandblockId: playerLb);
|
||||
|
||||
// Step 4: portal visibility — determine which interior cells to render.
|
||||
// Extract camera world position from the inverse of the view matrix.
|
||||
System.Numerics.Matrix4x4.Invert(camera.View, out var invView);
|
||||
var camPos = new System.Numerics.Vector3(invView.M41, invView.M42, invView.M43);
|
||||
var visibility = _cellVisibility.ComputeVisibility(camPos);
|
||||
bool cameraInsideCell = visibility?.CameraCell is not null;
|
||||
|
||||
// Conditional depth clear: when camera is inside a building, clear
|
||||
// depth (not color) so interior geometry writes fresh Z values on top
|
||||
// of the terrain color buffer. Exit portals show outdoor terrain color
|
||||
// because we kept the color buffer. Matching ACME GameScene.cs pattern.
|
||||
if (cameraInsideCell)
|
||||
_gl!.Clear(ClearBufferMask.DepthBufferBit);
|
||||
|
||||
_staticMesh?.Draw(camera, _worldState.LandblockEntries, frustum,
|
||||
neverCullLandblockId: playerLb);
|
||||
neverCullLandblockId: playerLb,
|
||||
visibleCellIds: visibility?.VisibleCellIds);
|
||||
|
||||
// Count visible vs total for the perf overlay.
|
||||
foreach (var entry in _worldState.LandblockEntries)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue