fix(render): A7 Fix B — per-OBJECT point-light selection (minimize_object_lighting)
Outdoor objects brightened as the camera approached: lighting selected the nearest 8 lights to the VIEWER and fed that one global set to everything (LightManager.Tick), so a building's wall torches only lit it once the camera got close enough for them to win the global top-8. Probe confirmed the scale of the problem: a single Holtburg view registers 129 point lights — the global cap of 8 was hopeless. Retail selects up to 8 lights PER OBJECT by the object's own position (minimize_object_lighting 0x0054d480), so a torch always lights the wall it sits on, camera-independent. Ported faithfully: - LightManager.SelectForObject (pure, TDD, 8 new tests): candidacy (light.pos − center)² < (Range + radius)², nearest-8 among those. Plus BuildPointLightSnapshot for the per-frame stable-indexed light list. - mesh_modern.vert: two SSBOs — binding=4 GLOBAL point-light array (the snapshot), binding=5 per-instance light SET (8 int indices into it, -1 = unused), parallel to the binding=0 instance buffer (mirrors the U.3 clip-slot mechanism). accumulateLights keeps ambient + sun from the SceneLighting UBO (cleared as faithful by the lighting audit) and loops THIS instance's point lights. pointContribution factored out (same calc_point_light wrap+norm shape). - WbDrawDispatcher: per-entity light set computed ONCE at the isNewEntity site (constant across the entity's parts), by the entity's AABB sphere; threaded into grp.LightSets parallel to grp.Matrices; global + per-instance buffers uploaded in Phase 5. Camera-independent ⇒ stable for static buildings. - GameWindow: BuildPointLightSnapshot + dispatcher.SetSceneLights each frame. Tests: 17/17 LightManager + 36/36 dispatcher clip-slot/clip-frame green (parallel-array lockstep preserved). Visually gated: the meeting hall now holds steady as the camera approaches (was the popping symptom). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
aa94cedc38
commit
4345e77d62
5 changed files with 473 additions and 50 deletions
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using AcDream.Core.Lighting;
|
||||
using AcDream.Core.Meshing;
|
||||
using AcDream.Core.Rendering;
|
||||
using AcDream.Core.Terrain;
|
||||
|
|
@ -132,6 +133,24 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
private uint _clipSlotSsbo;
|
||||
private uint[] _clipSlotData = new uint[256];
|
||||
|
||||
// Fix B (A7 #3): per-OBJECT light selection (minimize_object_lighting). Two
|
||||
// SSBOs replace the single global nearest-8-to-CAMERA UBO set for point/spot
|
||||
// lights — see mesh_modern.vert binding=4/5. _globalLightsSsbo (binding=4)
|
||||
// holds the per-frame point-light snapshot (LightManager.PointSnapshot);
|
||||
// _instLightSetSsbo (binding=5) holds MaxLightsPerObject int indices per
|
||||
// instance INTO it (-1 = unused), laid out parallel to _instanceSsbo.
|
||||
private uint _globalLightsSsbo;
|
||||
private uint _instLightSetSsbo;
|
||||
private int[] _lightSetData = new int[256 * LightManager.MaxLightsPerObject];
|
||||
private float[] _globalLightData = new float[16 * 16]; // 16 floats (4 vec4) per GlobalLight
|
||||
// This frame's point-light snapshot, handed in by GameWindow before Draw via
|
||||
// SetSceneLights. Null/empty ⇒ only ambient + sun render (all instance sets -1).
|
||||
private IReadOnlyList<LightSource>? _pointSnapshot;
|
||||
// This entity's selected point/spot light set — computed ONCE per entity at
|
||||
// the isNewEntity site (constant across the entity's parts/tuples), exactly
|
||||
// like _currentEntitySlot. -1 = unused slot.
|
||||
private readonly int[] _currentEntityLightSet = new int[LightManager.MaxLightsPerObject];
|
||||
|
||||
// Phase U.3: the SHARED per-cell clip-region SSBO (binding=2), owned by the
|
||||
// GameWindow-level ClipFrame and handed to us via SetClipRegionSsbo. When 0
|
||||
// (not yet wired), we bind our OWN fallback no-clip region buffer below so the
|
||||
|
|
@ -329,8 +348,21 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
_batchSsbo = _gl.GenBuffer();
|
||||
_indirectBuffer = _gl.GenBuffer();
|
||||
_clipSlotSsbo = _gl.GenBuffer(); // Phase U.3 binding=3
|
||||
_globalLightsSsbo = _gl.GenBuffer(); // Fix B binding=4
|
||||
_instLightSetSsbo = _gl.GenBuffer(); // Fix B binding=5
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fix B (A7 #3): hand the dispatcher this frame's GLOBAL point-light snapshot
|
||||
/// (<see cref="LightManager.PointSnapshot"/>). Call once per frame BEFORE
|
||||
/// <see cref="Draw"/>. The dispatcher uploads it to binding=4 and selects each
|
||||
/// object's up-to-8 lights from it (<see cref="LightManager.SelectForObject"/>)
|
||||
/// by the object's bounding sphere — camera-independent. Pass null/empty to
|
||||
/// disable per-object point lights (only ambient + sun render).
|
||||
/// </summary>
|
||||
public void SetSceneLights(IReadOnlyList<LightSource>? pointSnapshot)
|
||||
=> _pointSnapshot = pointSnapshot;
|
||||
|
||||
/// <summary>
|
||||
/// Phase U.3: hand the dispatcher the SHARED per-cell clip-region SSBO
|
||||
/// (binding=2) that <see cref="ClipFrame.UploadShared"/> created. The
|
||||
|
|
@ -888,7 +920,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
camPos = invView.Translation;
|
||||
|
||||
// ── Phase 1: clear groups, walk entities, build groups ──────────────
|
||||
foreach (var grp in _groups.Values) { grp.Matrices.Clear(); grp.Slots.Clear(); }
|
||||
foreach (var grp in _groups.Values) { grp.Matrices.Clear(); grp.Slots.Clear(); grp.LightSets.Clear(); }
|
||||
|
||||
var metaTable = _meshAdapter.MetadataTable;
|
||||
uint anyVao = 0;
|
||||
|
|
@ -1053,6 +1085,11 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
if (_currentEntityCulled)
|
||||
probeCulledEntities++;
|
||||
|
||||
// Fix B: select this entity's up-to-8 point/spot lights ONCE (the set
|
||||
// is constant across the entity's parts/tuples), by the entity's
|
||||
// bounding sphere — camera-INDEPENDENT (minimize_object_lighting).
|
||||
ComputeEntityLightSet(entity);
|
||||
|
||||
// #119 decisive probe: one-shot dump (+ change re-emission) for
|
||||
// ACDREAM_DUMP_ENTITY-targeted entities. Before the culled-continue
|
||||
// so a routed-out entity still reports its state.
|
||||
|
|
@ -1350,6 +1387,13 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
if (_clipSlotData.Length < totalInstances)
|
||||
_clipSlotData = new uint[totalInstances + 256];
|
||||
|
||||
// Fix B: per-instance light-set buffer, MaxLightsPerObject ints per
|
||||
// instance, laid out in the SAME group order / cursor as _instanceData
|
||||
// so instanceLightIdx[instanceIndex*8 + k] (binding=5) tracks
|
||||
// Instances[instanceIndex] (binding=0).
|
||||
if (_lightSetData.Length < totalInstances * LightManager.MaxLightsPerObject)
|
||||
_lightSetData = new int[(totalInstances + 256) * LightManager.MaxLightsPerObject];
|
||||
|
||||
_opaqueDraws.Clear();
|
||||
_translucentDraws.Clear();
|
||||
|
||||
|
|
@ -1375,6 +1419,13 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
// Slots[] is parallel to Matrices[] within the group; write the
|
||||
// slot at the same cursor so binding=3 stays aligned with binding=0.
|
||||
_clipSlotData[cursor] = grp.Slots[i];
|
||||
// Fix B: LightSets[] holds 8 ints per instance, parallel to
|
||||
// Matrices[]; copy this instance's block to the same cursor so
|
||||
// binding=5 stays aligned with binding=0.
|
||||
int lsDst = cursor * LightManager.MaxLightsPerObject;
|
||||
int lsSrc = i * LightManager.MaxLightsPerObject;
|
||||
for (int k = 0; k < LightManager.MaxLightsPerObject; k++)
|
||||
_lightSetData[lsDst + k] = grp.LightSets[lsSrc + k];
|
||||
cursor++;
|
||||
}
|
||||
|
||||
|
|
@ -1460,6 +1511,15 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
fixed (uint* sp = _clipSlotData)
|
||||
UploadSsbo(_clipSlotSsbo, 3, sp, totalInstances * sizeof(uint));
|
||||
|
||||
// Fix B: global point-light buffer (binding=4) + per-instance light-set
|
||||
// buffer (binding=5). The global buffer is this frame's PointSnapshot; the
|
||||
// per-instance buffer holds 8 int indices into it per instance, laid out
|
||||
// parallel to _instanceData in Phase 3. Both bound with ≥1 element so the
|
||||
// shader never reads an unbound SSBO on a no-lights frame.
|
||||
UploadGlobalLights();
|
||||
fixed (int* lp = _lightSetData)
|
||||
UploadSsbo(_instLightSetSsbo, 5, lp, totalInstances * LightManager.MaxLightsPerObject * sizeof(int));
|
||||
|
||||
fixed (DrawElementsIndirectCommand* cp = _indirectCommands)
|
||||
{
|
||||
_gl.BindBuffer(BufferTargetARB.DrawIndirectBuffer, _indirectBuffer);
|
||||
|
|
@ -1743,6 +1803,50 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
_gl.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, binding, ssbo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fix B: pack <see cref="_pointSnapshot"/> into the binding=4 global light
|
||||
/// buffer (one GlobalLight = 4 vec4 = 16 floats, std430 stride 64 bytes,
|
||||
/// matching mesh_modern.vert's <c>GlobalLight</c>). Always uploads ≥1 element
|
||||
/// so the shader never reads an unbound SSBO — on a no-lights frame index 0 is
|
||||
/// a zeroed dummy that no instance set references (all sets are -1).
|
||||
/// </summary>
|
||||
private unsafe void UploadGlobalLights()
|
||||
{
|
||||
var snap = _pointSnapshot;
|
||||
int n = snap?.Count ?? 0;
|
||||
int count = n > 0 ? n : 1; // never zero-size
|
||||
int floatsNeeded = count * 16;
|
||||
if (_globalLightData.Length < floatsNeeded)
|
||||
_globalLightData = new float[floatsNeeded + 16 * 16];
|
||||
Array.Clear(_globalLightData, 0, floatsNeeded);
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
var L = snap![i];
|
||||
int o = i * 16;
|
||||
// posAndKind (xyz world pos, w kind)
|
||||
_globalLightData[o + 0] = L.WorldPosition.X;
|
||||
_globalLightData[o + 1] = L.WorldPosition.Y;
|
||||
_globalLightData[o + 2] = L.WorldPosition.Z;
|
||||
_globalLightData[o + 3] = (int)L.Kind;
|
||||
// dirAndRange (xyz forward, w range = Falloff×1.3)
|
||||
_globalLightData[o + 4] = L.WorldForward.X;
|
||||
_globalLightData[o + 5] = L.WorldForward.Y;
|
||||
_globalLightData[o + 6] = L.WorldForward.Z;
|
||||
_globalLightData[o + 7] = L.Range;
|
||||
// colorAndIntensity (xyz linear colour, w intensity)
|
||||
_globalLightData[o + 8] = L.ColorLinear.X;
|
||||
_globalLightData[o + 9] = L.ColorLinear.Y;
|
||||
_globalLightData[o + 10] = L.ColorLinear.Z;
|
||||
_globalLightData[o + 11] = L.Intensity;
|
||||
// coneAngleEtc (x cone radians; yzw reserved)
|
||||
_globalLightData[o + 12] = L.ConeAngle;
|
||||
}
|
||||
|
||||
fixed (float* gp = _globalLightData)
|
||||
UploadSsbo(_globalLightsSsbo, 4, gp, count * 16 * sizeof(float));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase U.3: bind the per-cell clip-region SSBO to binding=2. Prefers the
|
||||
/// shared <see cref="ClipFrame"/> buffer (set via <see cref="SetClipRegionSsbo"/>);
|
||||
|
|
@ -1936,6 +2040,38 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
}
|
||||
grp.Matrices.Add(model);
|
||||
grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices
|
||||
AppendCurrentLightSet(grp); // Fix B — 8 ints per instance, parallel to Matrices
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fix B: choose the up-to-8 point/spot lights for THIS entity (the result
|
||||
/// reused by every part/instance of it), by the entity's world bounding
|
||||
/// sphere. Camera-independent (<see cref="LightManager.SelectForObject"/>), so
|
||||
/// a static building's torches stay constant as the viewer moves. Fills
|
||||
/// <see cref="_currentEntityLightSet"/>; unused slots are -1. On the no-lights
|
||||
/// path (no snapshot handed in) every slot is -1 ⇒ shader adds no point light.
|
||||
/// </summary>
|
||||
private void ComputeEntityLightSet(WorldEntity entity)
|
||||
{
|
||||
Array.Fill(_currentEntityLightSet, -1);
|
||||
var snap = _pointSnapshot;
|
||||
if (snap is null || snap.Count == 0) return;
|
||||
|
||||
if (entity.AabbDirty) entity.RefreshAabb();
|
||||
Vector3 center = (entity.AabbMin + entity.AabbMax) * 0.5f;
|
||||
float radius = (entity.AabbMax - entity.AabbMin).Length() * 0.5f;
|
||||
LightManager.SelectForObject(snap, center, radius, _currentEntityLightSet);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fix B: append the current entity's 8-slot light set to a group's
|
||||
/// <see cref="InstanceGroup.LightSets"/>, parallel to its Matrices (one
|
||||
/// 8-int block per instance), mirroring <c>grp.Slots.Add</c>.
|
||||
/// </summary>
|
||||
private void AppendCurrentLightSet(InstanceGroup grp)
|
||||
{
|
||||
for (int k = 0; k < LightManager.MaxLightsPerObject; k++)
|
||||
grp.LightSets.Add(_currentEntityLightSet[k]);
|
||||
}
|
||||
|
||||
private void ClassifyBatches(
|
||||
|
|
@ -1993,6 +2129,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
}
|
||||
grp.Matrices.Add(model);
|
||||
grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices
|
||||
AppendCurrentLightSet(grp); // Fix B — 8 ints per instance, parallel to Matrices
|
||||
collector?.Add(new CachedBatch(key, texHandle, restPose));
|
||||
}
|
||||
}
|
||||
|
|
@ -2072,6 +2209,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
_gl.DeleteBuffer(_indirectBuffer);
|
||||
if (_clipSlotSsbo != 0) _gl.DeleteBuffer(_clipSlotSsbo); // Phase U.3
|
||||
if (_fallbackClipRegionSsbo != 0) _gl.DeleteBuffer(_fallbackClipRegionSsbo); // Phase U.3
|
||||
if (_globalLightsSsbo != 0) _gl.DeleteBuffer(_globalLightsSsbo); // Fix B binding=4
|
||||
if (_instLightSetSsbo != 0) _gl.DeleteBuffer(_instLightSetSsbo); // Fix B binding=5
|
||||
if (_gpuQueriesInitialized)
|
||||
{
|
||||
for (int i = 0; i < GpuQueryRingDepth; i++)
|
||||
|
|
@ -2257,5 +2396,13 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
// _clipSlotData at the same cursor it writes Matrices[i] into _instanceData,
|
||||
// so the binding=3 instanceClipSlot[] tracks the binding=0 instance.
|
||||
public readonly List<uint> Slots = new();
|
||||
|
||||
// Fix B (A7 #3): per-instance light SET, MaxLightsPerObject(8) ints per
|
||||
// instance, parallel to Matrices (LightSets[i*8 .. i*8+8) is the selected
|
||||
// light index block for the instance whose matrix is Matrices[i]). At
|
||||
// layout time the dispatcher copies each block into _lightSetData at the
|
||||
// same cursor, so the binding=5 instanceLightIdx[] tracks the binding=0
|
||||
// instance. -1 = unused slot.
|
||||
public readonly List<int> LightSets = new();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue