feat(render): Phase A8 — indoor visibility + streaming fixes batch
Lands the working A8 indoor-rendering and streaming fixes accumulated this session. User has verified these visually to some degree (e.g. lifestone / translucent meshes confirmed fine under the FrontFace flip; bridge / wall / collision regressions confirmed fixed after travel); not every path has been exhaustively gated. The cellar-flap defect remains OPEN and will be solved the retail-faithful way via a dedicated brainstorm (see handoff docs). Rendering core (reviewed, high confidence): - EnvCellRenderer SSBO stride fix: upload packed Matrix4x4[] (64B) instead of the 80B CPU InstanceData struct the shader never expected — fixes the transform/texture "explosion" for any draw with >1 instance (cells that dedupe to a shared cellGeomId). Real root cause. - WB-style global FrontFace(CW) + per-batch CullMode carried through the MDI layout (GroupKey + BuildIndirectArrays + DrawIndirectRange split into same-cull runs with absolute uDrawIDOffset per run). - EntitySet partitioning (IndoorPass / OutdoorScenery / LiveDynamic) + WorldEntity.BuildingShellAnchorCellId so building shells scope to their dat-derived building cell instead of rendering everywhere. - RenderOutsideInAcdream (look into buildings from outside) + CollectVisiblePortalBuildings frustum cull of portal bounds. - Sky-when-inside-building + per-cell audit probe + GL-state probe. Streaming / perf (test-covered; not independently code-reviewed this session): - Near/far priority queues so near work wins over far; PromoteToNear carries full landblock + mesh data; LandblockEntriesWithoutAnimatedIndex avoids rebuilding the animated-lookup dict in the hot draw path. Fixes the bridge-not-appearing / missing-walls / broken-collision-after-travel regressions and improves post-transition FPS. Tooling + docs: - tools/A8CellAudit: offline dat cell/portal/building dumper (portals + buildings modes) — reproduces the cellar-flap investigation with no launch. - docs/research cellar-flap root-cause + option-2 handoff (the didInsideStencil double-duty finding + the WB-recursive design decision + brainstorm prompt), entity-taxonomy, replan, issue-78 visibility investigation. Diagnostics retained on purpose: ACDREAM_A8_DIAG_* gates, portal_stencil.vert provisional pos.w clamp, and the probe families are kept (env-var gated, zero cost when off) because the pending option-2 cellar-flap brainstorm needs them. Strip in the option-2 ship commit. Indoor branch stays behind ACDREAM_A8_INDOOR_BRANCH=1 (default off = pre-A8 visual). Build green; App tests + Core (streaming/dispatcher/loader) tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e415bb3863
commit
5dc4140c11
38 changed files with 3965 additions and 277 deletions
|
|
@ -6,6 +6,7 @@ using AcDream.Core.Meshing;
|
|||
using AcDream.Core.Rendering;
|
||||
using AcDream.Core.Terrain;
|
||||
using AcDream.Core.World;
|
||||
using DatReaderWriter.Enums;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
|
@ -76,18 +77,22 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
|
||||
/// <summary>Cell mesh + cell statics (<see cref="WorldEntity.ParentCellId"/>
|
||||
/// non-null) PLUS building shell stabs (<see cref="WorldEntity.IsBuildingShell"/>
|
||||
/// true, regardless of ParentCellId). These render unconditionally
|
||||
/// when the camera is inside their building — building shells ARE
|
||||
/// the indoor walls. Live-dynamic (<c>ServerGuid != 0</c>) is
|
||||
/// excluded; it flows through <see cref="LiveDynamic"/>.</summary>
|
||||
/// true) whose <see cref="WorldEntity.BuildingShellAnchorCellId"/>
|
||||
/// belongs to the active building cell set. Live-dynamic
|
||||
/// (<c>ServerGuid != 0</c>) is excluded; it flows through
|
||||
/// <see cref="LiveDynamic"/>.</summary>
|
||||
IndoorPass,
|
||||
|
||||
/// <summary>Outdoor scenery stabs (<c>ParentCellId == null</c>,
|
||||
/// <c>!IsBuildingShell</c>) plus procedurally-generated scenery.
|
||||
/// Drawn stencil-gated to portal silhouettes when the camera is
|
||||
/// inside. Live-dynamic excluded.</summary>
|
||||
/// <summary>Outdoor/top-level stabs (<c>ParentCellId == null</c>),
|
||||
/// including building shells. Drawn stencil-gated to portal
|
||||
/// silhouettes when the camera is inside. Live-dynamic excluded.</summary>
|
||||
OutdoorScenery,
|
||||
|
||||
/// <summary>Top-level building shell stabs only, optionally scoped by
|
||||
/// <see cref="WorldEntity.BuildingShellAnchorCellId"/>. Used for
|
||||
/// portal depth repair without walking the full outdoor scenery set.</summary>
|
||||
BuildingShells,
|
||||
|
||||
/// <summary>Server-spawned dynamic entities (<c>ServerGuid != 0</c>):
|
||||
/// player, NPCs, monsters, dropped items, animated and idle doors.
|
||||
/// Drawn last with stencil disabled so they're depth-tested against
|
||||
|
|
@ -103,6 +108,19 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
|
||||
private readonly BindlessSupport _bindless;
|
||||
|
||||
public readonly record struct DrawStats(
|
||||
EntitySet Set,
|
||||
int EntitiesWalked,
|
||||
int MeshRefs,
|
||||
int Instances,
|
||||
int Draws,
|
||||
int CullRuns,
|
||||
int OpaqueDraws,
|
||||
int TransparentDraws,
|
||||
long Triangles);
|
||||
|
||||
public DrawStats LastDrawStats { get; private set; }
|
||||
|
||||
// Tier 1 cache (#53): per-entity classification results for static
|
||||
// entities (those NOT in GameWindow._animatedEntities). Wired here in
|
||||
// Task 7 for plumbing only — Tasks 9-10 wire the per-entity
|
||||
|
|
@ -132,6 +150,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
private float[] _instanceData = new float[256 * 16]; // mat4 floats per instance
|
||||
private BatchData[] _batchData = new BatchData[256];
|
||||
private DrawElementsIndirectCommand[] _indirectCommands = new DrawElementsIndirectCommand[256];
|
||||
private CullMode[] _drawCullModes = new CullMode[256];
|
||||
|
||||
private int _opaqueDrawCount;
|
||||
private int _transparentDrawCount;
|
||||
|
|
@ -283,6 +302,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
public struct WalkResult
|
||||
{
|
||||
public int EntitiesWalked;
|
||||
public int BuildingShellAnchorPass;
|
||||
public int BuildingShellAnchorReject;
|
||||
public List<(WorldEntity Entity, int MeshRefIndex, uint LandblockId)> ToDraw;
|
||||
}
|
||||
|
||||
|
|
@ -375,8 +396,15 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
// Phase A8: EntitySet partition for indoor/outdoor split passes.
|
||||
if (!EntityMatchesSet(entity, set)) continue;
|
||||
if (entity.MeshRefs.Count == 0) continue;
|
||||
if (entity.ParentCellId.HasValue && visibleCellIds is not null
|
||||
&& !visibleCellIds.Contains(entity.ParentCellId.Value)) continue;
|
||||
bool shellScoped = IsShellScopedSet(set)
|
||||
&& entity.IsBuildingShell
|
||||
&& visibleCellIds is not null;
|
||||
if (!EntityPassesVisibleCellGate(entity, visibleCellIds, set))
|
||||
{
|
||||
if (shellScoped) result.BuildingShellAnchorReject++;
|
||||
continue;
|
||||
}
|
||||
if (shellScoped) result.BuildingShellAnchorPass++;
|
||||
result.EntitiesWalked++;
|
||||
for (int i = 0; i < entity.MeshRefs.Count; i++)
|
||||
scratch.Add((entity, i, entry.LandblockId));
|
||||
|
|
@ -397,11 +425,13 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
bool isCellEntity = indoorProbeState is not null
|
||||
&& RenderingDiagnostics.IsEnvCellId(cellProbeId);
|
||||
|
||||
bool cellInVis = !(entity.ParentCellId.HasValue
|
||||
&& visibleCellIds is not null
|
||||
&& !visibleCellIds.Contains(entity.ParentCellId.Value));
|
||||
bool shellScoped = IsShellScopedSet(set)
|
||||
&& entity.IsBuildingShell
|
||||
&& visibleCellIds is not null;
|
||||
bool cellInVis = EntityPassesVisibleCellGate(entity, visibleCellIds, set);
|
||||
if (!cellInVis)
|
||||
{
|
||||
if (shellScoped) result.BuildingShellAnchorReject++;
|
||||
if (isCellEntity && RenderingDiagnostics.ProbeIndoorCullEnabled
|
||||
&& indoorProbeState!.ShouldEmit(cellProbeId))
|
||||
{
|
||||
|
|
@ -412,6 +442,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
}
|
||||
continue;
|
||||
}
|
||||
if (shellScoped) result.BuildingShellAnchorPass++;
|
||||
|
||||
// Per-entity AABB frustum cull (perf #3). Animated entities bypass —
|
||||
// they're tracked at landblock level + need per-frame work regardless.
|
||||
|
|
@ -545,6 +576,13 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
probeState,
|
||||
set);
|
||||
|
||||
if (set == EntitySet.IndoorPass && RenderingDiagnostics.ProbeVisibilityEnabled)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[indoor-shells] anchorPass={walkResult.BuildingShellAnchorPass} " +
|
||||
$"anchorReject={walkResult.BuildingShellAnchorReject} walked={walkResult.EntitiesWalked}");
|
||||
}
|
||||
|
||||
// Tier 1 cache (#53) flush-tracking locals. _walkScratch holds one tuple
|
||||
// per (entity, MeshRefIndex) and is in entity-order, so all MeshRefs of
|
||||
// a given entity are contiguous. We accumulate ALL of an entity's
|
||||
|
|
@ -855,6 +893,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
// Nothing visible — skip the GL pass entirely.
|
||||
if (anyVao == 0)
|
||||
{
|
||||
LastDrawStats = new DrawStats(set, walkResult.EntitiesWalked, _walkScratch.Count, 0, 0, 0, 0, 0, 0);
|
||||
_cpuStopwatch.Stop();
|
||||
if (diag) MaybeFlushDiag();
|
||||
return;
|
||||
|
|
@ -865,6 +904,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
foreach (var grp in _groups.Values) totalInstances += grp.Matrices.Count;
|
||||
if (totalInstances == 0)
|
||||
{
|
||||
LastDrawStats = new DrawStats(set, walkResult.EntitiesWalked, _walkScratch.Count, 0, 0, 0, 0, 0, 0);
|
||||
_cpuStopwatch.Stop();
|
||||
if (diag) MaybeFlushDiag();
|
||||
return;
|
||||
|
|
@ -905,11 +945,14 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
_translucentDraws.Add(grp);
|
||||
}
|
||||
|
||||
// Front-to-back sort for opaque pass: nearer groups draw first so the
|
||||
// depth test rejects fragments hidden behind them, reducing fragment
|
||||
// shader cost from overdraw on dense scenes (Holtburg courtyard,
|
||||
// Foundry interior).
|
||||
_opaqueDraws.Sort(static (a, b) => a.SortDistance.CompareTo(b.SortDistance));
|
||||
// Front-to-back sort within each cull mode. DrawIndirectRange must
|
||||
// split MDI calls whenever CullMode changes because GL state is not
|
||||
// part of an indirect command. Sorting by distance alone can turn a
|
||||
// stable 1k-draw live scene into hundreds of tiny MDI runs after a
|
||||
// landblock transition, which shows up as a GPU-command bottleneck
|
||||
// without a triangle-count spike.
|
||||
_opaqueDraws.Sort(CompareOpaqueSubmissionOrder);
|
||||
_translucentDraws.Sort(CompareTransparentSubmissionOrder);
|
||||
|
||||
// ── Phase 4: build IndirectGroupInput list (opaque sorted, then translucent),
|
||||
// fill via BuildIndirectArrays ──────────────────────────────────
|
||||
|
|
@ -918,6 +961,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
_batchData = new BatchData[totalDraws + 64];
|
||||
if (_indirectCommands.Length < totalDraws)
|
||||
_indirectCommands = new DrawElementsIndirectCommand[totalDraws + 64];
|
||||
if (_drawCullModes.Length < totalDraws)
|
||||
_drawCullModes = new CullMode[totalDraws + 64];
|
||||
|
||||
var groupInputs = new List<IndirectGroupInput>(totalDraws);
|
||||
foreach (var g in _opaqueDraws) groupInputs.Add(ToInput(g));
|
||||
|
|
@ -926,7 +971,13 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
// Cast _batchData (private BatchData) to public-mirror BatchDataPublic for BuildIndirectArrays.
|
||||
// Layout is asserted at test time (BatchDataPublic_LayoutMatchesPrivateBatchData test).
|
||||
var batchPublic = new BatchDataPublic[totalDraws];
|
||||
var layout = BuildIndirectArrays(groupInputs, _indirectCommands, batchPublic);
|
||||
var layout = BuildIndirectArrays(groupInputs, _indirectCommands, batchPublic, _drawCullModes);
|
||||
long totalTriangles = 0;
|
||||
foreach (var input in groupInputs)
|
||||
totalTriangles += (long)(input.IndexCount / 3) * input.InstanceCount;
|
||||
int cullRuns =
|
||||
CountCullRuns(_drawCullModes, 0, layout.OpaqueCount) +
|
||||
CountCullRuns(_drawCullModes, layout.OpaqueCount, layout.TransparentCount);
|
||||
|
||||
// Copy back into _batchData
|
||||
for (int i = 0; i < totalDraws; i++)
|
||||
|
|
@ -941,6 +992,16 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
_opaqueDrawCount = layout.OpaqueCount;
|
||||
_transparentDrawCount = layout.TransparentCount;
|
||||
_transparentByteOffset = layout.TransparentByteOffset;
|
||||
LastDrawStats = new DrawStats(
|
||||
set,
|
||||
walkResult.EntitiesWalked,
|
||||
_walkScratch.Count,
|
||||
totalInstances,
|
||||
totalDraws,
|
||||
cullRuns,
|
||||
_opaqueDrawCount,
|
||||
_transparentDrawCount,
|
||||
totalTriangles);
|
||||
|
||||
// ── Phase 5: upload three buffers ───────────────────────────────────
|
||||
fixed (float* ip = _instanceData)
|
||||
|
|
@ -1007,12 +1068,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
_shader.SetInt("uDrawIDOffset", 0);
|
||||
_gl.BindBuffer(BufferTargetARB.DrawIndirectBuffer, _indirectBuffer);
|
||||
if (diag && _gpuQueriesInitialized) _gl.BeginQuery(QueryTarget.TimeElapsed, _gpuQueryOpaque[gpuQuerySlot]);
|
||||
_gl.MultiDrawElementsIndirect(
|
||||
PrimitiveType.Triangles,
|
||||
DrawElementsType.UnsignedShort,
|
||||
(void*)0,
|
||||
(uint)_opaqueDrawCount,
|
||||
(uint)DrawCommandStride);
|
||||
DrawIndirectRange(0, _opaqueDrawCount);
|
||||
if (diag && _gpuQueriesInitialized) _gl.EndQuery(QueryTarget.TimeElapsed);
|
||||
if (AlphaToCoverage) _gl.Disable(EnableCap.SampleAlphaToCoverage);
|
||||
}
|
||||
|
|
@ -1030,38 +1086,13 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
// flickers to whatever opaque batch sorted first that frame. See
|
||||
// uDrawIDOffset comment in mesh_modern.vert.
|
||||
_shader.SetInt("uDrawIDOffset", _opaqueDrawCount);
|
||||
// Phase Post-A.5 (ISSUE #52, 2026-05-10): re-establish Phase 9.2's
|
||||
// back-face cull setup. The legacy StaticMeshRenderer had this
|
||||
// (commit 6f1971a, 2026-04-11) until the N.5 retirement amendment
|
||||
// (commit dcae2b6, 2026-05-08) deleted that renderer; the new
|
||||
// WbDrawDispatcher never inherited the cull-face state.
|
||||
//
|
||||
// Closed-shell translucent meshes — lifestone crystal, glow gems,
|
||||
// any convex blended mesh — NEED back-face culling in the
|
||||
// translucent pass. Without it, back faces composite OVER front
|
||||
// faces in arbitrary iteration order, because DepthMask(false)
|
||||
// means nothing records depth within the translucent set. The
|
||||
// result is the user-visible "one face missing, see into the
|
||||
// hollow interior" + frame-to-frame color flicker as rotation
|
||||
// shifts the triangle order.
|
||||
//
|
||||
// Our fan triangulation emits pos-side polygons as (0, i, i+1) —
|
||||
// CCW in standard OpenGL conventions — so GL_BACK + CCW-front is
|
||||
// the correct state. Matches WorldBuilder's per-batch CullMode
|
||||
// handling. Neg-side polygons (rare on translucent AC content)
|
||||
// use reversed winding and get culled here, matching the opaque
|
||||
// pass and the original Phase 9.2 fix's known limitation.
|
||||
_gl.Enable(EnableCap.CullFace);
|
||||
_gl.CullFace(TriangleFace.Back);
|
||||
_gl.FrontFace(FrontFaceDirection.Ccw);
|
||||
// Closed-shell translucent meshes still need culling, but the
|
||||
// cull side must come from each dat batch just like the opaque
|
||||
// section. BuildIndirectArrays preserves CullMode in _drawCullModes.
|
||||
_gl.FrontFace(FrontFaceDirection.CW);
|
||||
_shader.SetInt("uRenderPass", 1);
|
||||
if (diag && _gpuQueriesInitialized) _gl.BeginQuery(QueryTarget.TimeElapsed, _gpuQueryTransparent[gpuQuerySlot]);
|
||||
_gl.MultiDrawElementsIndirect(
|
||||
PrimitiveType.Triangles,
|
||||
DrawElementsType.UnsignedShort,
|
||||
(void*)_transparentByteOffset,
|
||||
(uint)_transparentDrawCount,
|
||||
(uint)DrawCommandStride);
|
||||
DrawIndirectRange(_opaqueDrawCount, _transparentDrawCount);
|
||||
if (diag && _gpuQueriesInitialized) _gl.EndQuery(QueryTarget.TimeElapsed);
|
||||
_gl.DepthMask(true);
|
||||
_gl.Disable(EnableCap.Blend);
|
||||
|
|
@ -1132,7 +1163,91 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
FirstInstance: g.FirstInstance,
|
||||
TextureHandle: g.BindlessTextureHandle,
|
||||
TextureLayer: g.TextureLayer,
|
||||
Translucency: g.Translucency);
|
||||
Translucency: g.Translucency,
|
||||
CullMode: g.CullMode);
|
||||
|
||||
private static int CompareOpaqueSubmissionOrder(InstanceGroup a, InstanceGroup b)
|
||||
{
|
||||
int cull = a.CullMode.CompareTo(b.CullMode);
|
||||
return cull != 0 ? cull : a.SortDistance.CompareTo(b.SortDistance);
|
||||
}
|
||||
|
||||
private static int CompareTransparentSubmissionOrder(InstanceGroup a, InstanceGroup b)
|
||||
{
|
||||
int cull = a.CullMode.CompareTo(b.CullMode);
|
||||
return cull != 0 ? cull : b.SortDistance.CompareTo(a.SortDistance);
|
||||
}
|
||||
|
||||
private static int CountCullRuns(CullMode[] modes, int startCommand, int commandCount)
|
||||
{
|
||||
if (commandCount <= 0) return 0;
|
||||
|
||||
int end = startCommand + commandCount;
|
||||
int runs = 1;
|
||||
var previous = modes[startCommand];
|
||||
for (int i = startCommand + 1; i < end; i++)
|
||||
{
|
||||
var current = modes[i];
|
||||
if (current == previous) continue;
|
||||
runs++;
|
||||
previous = current;
|
||||
}
|
||||
return runs;
|
||||
}
|
||||
|
||||
private unsafe void DrawIndirectRange(int startCommand, int commandCount)
|
||||
{
|
||||
int end = startCommand + commandCount;
|
||||
int command = startCommand;
|
||||
while (command < end)
|
||||
{
|
||||
var cullMode = _drawCullModes[command];
|
||||
ApplyCullMode(cullMode);
|
||||
|
||||
int runCount = 1;
|
||||
while (command + runCount < end && _drawCullModes[command + runCount] == cullMode)
|
||||
runCount++;
|
||||
|
||||
// Each glMultiDrawElementsIndirect call restarts gl_DrawID at 0.
|
||||
// Because this method splits one logical opaque/transparent pass
|
||||
// into CullMode runs, the shader must receive the absolute command
|
||||
// index for this run or it will read BatchData[0] again and bind
|
||||
// the wrong texture for later runs.
|
||||
_shader.SetInt("uDrawIDOffset", command);
|
||||
_gl.MultiDrawElementsIndirect(
|
||||
PrimitiveType.Triangles,
|
||||
DrawElementsType.UnsignedShort,
|
||||
(void*)(command * DrawCommandStride),
|
||||
(uint)runCount,
|
||||
(uint)DrawCommandStride);
|
||||
|
||||
command += runCount;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyCullMode(CullMode mode)
|
||||
{
|
||||
// WB BaseObjectRenderManager.cs:850-866 applies CullMode per MDI group.
|
||||
// WB GameScene.cs:843 sets FrontFace(CW) globally; SetCullMode then
|
||||
// only chooses front/back culling. Keep the same convention here so
|
||||
// splitting MDI commands by CullMode cannot resurrect stale CCW state.
|
||||
_gl.FrontFace(FrontFaceDirection.CW);
|
||||
switch (mode)
|
||||
{
|
||||
case CullMode.None:
|
||||
_gl.Disable(EnableCap.CullFace);
|
||||
break;
|
||||
case CullMode.Clockwise:
|
||||
_gl.Enable(EnableCap.CullFace);
|
||||
_gl.CullFace(TriangleFace.Front);
|
||||
break;
|
||||
case CullMode.CounterClockwise:
|
||||
case CullMode.Landblock:
|
||||
_gl.Enable(EnableCap.CullFace);
|
||||
_gl.CullFace(TriangleFace.Back);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void UploadSsbo(uint ssbo, uint binding, void* data, int byteCount)
|
||||
{
|
||||
|
|
@ -1293,6 +1408,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
BindlessTextureHandle = key.BindlessTextureHandle,
|
||||
TextureLayer = key.TextureLayer,
|
||||
Translucency = key.Translucency,
|
||||
CullMode = key.CullMode,
|
||||
};
|
||||
_groups[key] = grp;
|
||||
}
|
||||
|
|
@ -1335,7 +1451,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
|
||||
var key = new GroupKey(
|
||||
batch.IBO, batch.FirstIndex, (int)batch.BaseVertex,
|
||||
batch.IndexCount, texHandle, texLayer, translucency);
|
||||
batch.IndexCount, texHandle, texLayer, translucency, batch.CullMode);
|
||||
|
||||
if (!_groups.TryGetValue(key, out var grp))
|
||||
{
|
||||
|
|
@ -1348,6 +1464,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
BindlessTextureHandle = texHandle,
|
||||
TextureLayer = texLayer,
|
||||
Translucency = translucency,
|
||||
CullMode = batch.CullMode,
|
||||
};
|
||||
_groups[key] = grp;
|
||||
}
|
||||
|
|
@ -1403,7 +1520,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
|
||||
bool isIndoor = entity.ParentCellId.HasValue || entity.IsBuildingShell;
|
||||
if (set == EntitySet.IndoorPass) return isIndoor;
|
||||
if (set == EntitySet.OutdoorScenery) return !isIndoor;
|
||||
if (set == EntitySet.OutdoorScenery) return !entity.ParentCellId.HasValue;
|
||||
if (set == EntitySet.BuildingShells) return entity.IsBuildingShell;
|
||||
|
||||
throw new InvalidOperationException($"Unhandled EntitySet value: {set}");
|
||||
}
|
||||
|
|
@ -1425,10 +1543,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
if (!EntityMatchesSet(entity, set)) continue;
|
||||
if (entity.MeshRefs.Count == 0) continue;
|
||||
|
||||
bool cellInVis = !(entity.ParentCellId.HasValue
|
||||
&& visibleCellIds is not null
|
||||
&& !visibleCellIds.Contains(entity.ParentCellId.Value));
|
||||
if (!cellInVis) continue;
|
||||
if (!EntityPassesVisibleCellGate(entity, visibleCellIds, set)) continue;
|
||||
|
||||
output.Add(entity.Id);
|
||||
}
|
||||
|
|
@ -1441,8 +1556,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
/// 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
|
||||
/// <paramref name="cellIds"/>. Building shells are gated by
|
||||
/// BuildingShellAnchorCellId membership in the same cell set. Outdoor
|
||||
/// scenery is excluded by the EntitySet partition (no cell-list gate
|
||||
/// needed — EntityMatchesSet handles it).</para>
|
||||
/// </summary>
|
||||
|
|
@ -1458,11 +1573,40 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
if (entity.MeshRefs.Count == 0) continue;
|
||||
if (entity.ParentCellId.HasValue && !cellIds.Contains(entity.ParentCellId.Value))
|
||||
continue;
|
||||
if (IsShellScopedSet(set) && entity.IsBuildingShell)
|
||||
{
|
||||
if (entity.BuildingShellAnchorCellId is not uint anchorCellId ||
|
||||
!cellIds.Contains(anchorCellId))
|
||||
continue;
|
||||
}
|
||||
result.Add(entity.Id);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool EntityPassesVisibleCellGate(
|
||||
WorldEntity entity,
|
||||
HashSet<uint>? visibleCellIds,
|
||||
EntitySet set)
|
||||
{
|
||||
if (visibleCellIds is null)
|
||||
return true;
|
||||
|
||||
if (entity.ParentCellId.HasValue)
|
||||
return visibleCellIds.Contains(entity.ParentCellId.Value);
|
||||
|
||||
if (IsShellScopedSet(set) && entity.IsBuildingShell)
|
||||
{
|
||||
return entity.BuildingShellAnchorCellId is uint anchorCellId
|
||||
&& visibleCellIds.Contains(anchorCellId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsShellScopedSet(EntitySet set) =>
|
||||
set == EntitySet.IndoorPass || set == EntitySet.BuildingShells;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
|
@ -1503,7 +1647,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
int FirstInstance,
|
||||
ulong TextureHandle,
|
||||
uint TextureLayer,
|
||||
TranslucencyKind Translucency);
|
||||
TranslucencyKind Translucency,
|
||||
CullMode CullMode = CullMode.CounterClockwise);
|
||||
|
||||
/// <summary>
|
||||
/// Public mirror of the per-group <see cref="BatchData"/> uploaded to the SSBO.
|
||||
|
|
@ -1535,7 +1680,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
public static IndirectLayoutResult BuildIndirectArrays(
|
||||
IReadOnlyList<IndirectGroupInput> groups,
|
||||
DrawElementsIndirectCommand[] indirectScratch,
|
||||
BatchDataPublic[] batchScratch)
|
||||
BatchDataPublic[] batchScratch,
|
||||
CullMode[]? cullScratch = null)
|
||||
{
|
||||
int opaqueCount = 0;
|
||||
int transparentCount = 0;
|
||||
|
|
@ -1570,12 +1716,14 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
{
|
||||
indirectScratch[oi] = dec;
|
||||
batchScratch[oi] = bd;
|
||||
if (cullScratch is not null) cullScratch[oi] = g.CullMode;
|
||||
oi++;
|
||||
}
|
||||
else
|
||||
{
|
||||
indirectScratch[ti] = dec;
|
||||
batchScratch[ti] = bd;
|
||||
if (cullScratch is not null) cullScratch[ti] = g.CullMode;
|
||||
ti++;
|
||||
}
|
||||
}
|
||||
|
|
@ -1639,6 +1787,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
public ulong BindlessTextureHandle; // 64-bit (was uint TextureHandle in N.4)
|
||||
public uint TextureLayer; // 0 for per-instance composites; non-zero when WB atlas is adopted in N.6+
|
||||
public TranslucencyKind Translucency;
|
||||
public CullMode CullMode;
|
||||
public int FirstInstance; // offset into the shared instance VBO (in instances, not bytes)
|
||||
public int InstanceCount;
|
||||
public float SortDistance; // squared distance from camera to first instance, for opaque sort
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue