fix(lighting): #142 — per-instance sun gate for windowed-building interiors
Standing inside (or looking into) a windowed building like the Agent of Arcanum, interior objects (furniture, NPCs, the player) were lit by the directional sun because acdream's sun gate was per-FRAME (keyed on the player being in a sealed cell), not per-DRAW as retail does it. Retail's PView::DrawCells (0x005a4840) runs two stages per frame: outdoor stage → useSunlightSet(1) (0x005a485a): sun ON interior stage → useSunlightSet(0) (0x005a49f3): sun OFF DrawMeshInternal (0x0059f398) then calls minimize_object_lighting only when useSunlight==0, so indoor objects ALWAYS skip the sun regardless of whether the player's cell is windowed or sealed. Fix: add a per-instance uint SSBO (binding=6 instanceIndoor[]) whose value is IndoorObjectReceivesTorches(ParentCellId) — the same predicate AP-43 already uses for the torch gate. In mesh_modern.vert, nest the sun loop inside an additional `if (instanceIndoor[instanceIndex] == 0u)` check inside the existing `if (uLightingMode == 0)` block. Indoor objects get torches (unchanged) but now skip the sun; outdoor objects keep the sun and still get no torches. The ambient regime (UpdateSunFromSky: 0.2 sealed / sky otherwise) is untouched — it was already correct. Mechanically: _currentEntityIndoor set once per entity in ComputeEntityLightSet; appended to InstanceGroup.IndoorFlags in AppendCurrentLightSet; grown/packed/uploaded in the same cursor loop as _clipSlotData and _lightSetData; deleted in Dispose. Mode-1 draws (EnvCellRenderer) never read binding=6 — the sun loop is inside the uLightingMode==0 uniform-control-flow branch. AP-43 divergence register updated: the sun half is now per-draw (no longer a residual). Residual narrowed to the unaudited ebp_2 test in CellManager::ChangePosition (no observed impact). Tests: WbDrawDispatcherIndoorFlagTests pins IndoorObjectReceivesTorches for the spec §5 representative ids: 0xA9B40172 (Agent of Arcanum EnvCell) → 1; 0xA9B40031 (land sub-cell) → 0; 0xA9B4FFFF (landblock) → 0; null (outdoor shell) → 0; plus the boundary cases 0x0100/0x00FF. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
cd024377a0
commit
ef5049fd58
4 changed files with 148 additions and 10 deletions
|
|
@ -96,6 +96,15 @@ layout(std430, binding = 5) readonly buffer InstanceLightSetBuf {
|
|||
int instanceLightIdx[]; // 8 per instance; -1 = unused
|
||||
};
|
||||
|
||||
// #142: per-instance "indoor" flag, 1 per instance, parallel to the binding=0
|
||||
// instance buffer (same instanceIndex). 1 = object parented to an EnvCell (skip the
|
||||
// sun — retail's useSunlight==0 interior stage); 0 = outdoor object (gets the sun).
|
||||
// Read ONLY inside the uniform `uLightingMode == 0` branch below, so the mode-1
|
||||
// (EnvCell shell) path provably never touches it — EnvCellRenderer need not bind it.
|
||||
layout(std430, binding = 6) readonly buffer InstanceIndoorBuf {
|
||||
uint instanceIndoor[];
|
||||
};
|
||||
|
||||
// Core profile: redeclare gl_PerVertex so writing gl_ClipDistance[] is legal
|
||||
// alongside gl_Position. The array is sized 8 to match the CellClip plane budget
|
||||
// and the GL guarantee (GL_MAX_CLIP_DISTANCES >= 8). The host enables
|
||||
|
|
@ -190,14 +199,20 @@ vec3 accumulateLights(vec3 N, vec3 worldPos, int instanceIndex) {
|
|||
// SUN / directional — OBJECT path only (mode 0). retail's EnvCell path
|
||||
// (minimize_envcell_lighting) enables only dynamic lights, NEVER the sun, so
|
||||
// EnvCell walls (mode 1) get no directional sun wash (A7 Fix D D-4).
|
||||
// #142: within mode 0, also skip the sun for indoor objects (ParentCellId is an
|
||||
// EnvCell). This mirrors retail's per-draw-stage useSunlight toggle: the interior
|
||||
// stage runs useSunlightSet(0) (PView::DrawCells 0x005a49f3), so indoor objects
|
||||
// get no sun even in windowed buildings where the player's frame is not sun-killed.
|
||||
if (uLightingMode == 0) {
|
||||
int activeLights = int(uCellAmbient.w);
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
if (i >= activeLights) break;
|
||||
if (int(uLights[i].posAndKind.w) != 0) continue; // directional only
|
||||
vec3 Ldir = -uLights[i].dirAndRange.xyz;
|
||||
float ndl = max(0.0, dot(N, Ldir));
|
||||
lit += uLights[i].colorAndIntensity.xyz * uLights[i].colorAndIntensity.w * ndl;
|
||||
if (instanceIndoor[instanceIndex] == 0u) { // #142: outdoor objects only get the sun
|
||||
int activeLights = int(uCellAmbient.w);
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
if (i >= activeLights) break;
|
||||
if (int(uLights[i].posAndKind.w) != 0) continue; // directional only
|
||||
vec3 Ldir = -uLights[i].dirAndRange.xyz;
|
||||
float ndl = max(0.0, dot(N, Ldir));
|
||||
lit += uLights[i].colorAndIntensity.xyz * uLights[i].colorAndIntensity.w * ndl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -143,6 +143,13 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
private uint _instLightSetSsbo;
|
||||
private int[] _lightSetData = new int[256 * LightManager.MaxLightsPerObject];
|
||||
private float[] _globalLightData = new float[GlobalLightPacker.FloatsPerLight * 16]; // 16 floats (4 vec4) per GlobalLight
|
||||
|
||||
// #142: per-instance "indoor" flag (binding=6), one uint per instance, parallel
|
||||
// to _instanceSsbo. 1 = object parented to an EnvCell (skip the sun in the
|
||||
// shader's uLightingMode==0 branch); 0 = outdoor object (gets the sun).
|
||||
// Mechanically a clone of _clipSlotData / _clipSlotSsbo.
|
||||
private uint _instIndoorSsbo;
|
||||
private uint[] _indoorData = new uint[256];
|
||||
// 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;
|
||||
|
|
@ -151,6 +158,12 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
// like _currentEntitySlot. -1 = unused slot.
|
||||
private readonly int[] _currentEntityLightSet = new int[LightManager.MaxLightsPerObject];
|
||||
|
||||
// #142: per-entity "indoor" flag — set once per entity in ComputeEntityLightSet,
|
||||
// parallel to _currentEntityLightSet. True when IndoorObjectReceivesTorches fires
|
||||
// (ParentCellId is an EnvCell). Appended to InstanceGroup.IndoorFlags in
|
||||
// AppendCurrentLightSet; uploaded as binding=6 instanceIndoor[].
|
||||
private bool _currentEntityIndoor;
|
||||
|
||||
// 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
|
||||
|
|
@ -350,6 +363,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
_clipSlotSsbo = _gl.GenBuffer(); // Phase U.3 binding=3
|
||||
_globalLightsSsbo = _gl.GenBuffer(); // Fix B binding=4
|
||||
_instLightSetSsbo = _gl.GenBuffer(); // Fix B binding=5
|
||||
_instIndoorSsbo = _gl.GenBuffer(); // #142 binding=6
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -923,7 +937,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(); grp.LightSets.Clear(); }
|
||||
foreach (var grp in _groups.Values) { grp.Matrices.Clear(); grp.Slots.Clear(); grp.LightSets.Clear(); grp.IndoorFlags.Clear(); }
|
||||
|
||||
var metaTable = _meshAdapter.MetadataTable;
|
||||
uint anyVao = 0;
|
||||
|
|
@ -1397,6 +1411,11 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
if (_lightSetData.Length < totalInstances * LightManager.MaxLightsPerObject)
|
||||
_lightSetData = new int[(totalInstances + 256) * LightManager.MaxLightsPerObject];
|
||||
|
||||
// #142: per-instance indoor flag buffer, one uint per instance, parallel to
|
||||
// _clipSlotData / _instanceData. Grown on demand like the others.
|
||||
if (_indoorData.Length < totalInstances)
|
||||
_indoorData = new uint[totalInstances + 256];
|
||||
|
||||
_opaqueDraws.Clear();
|
||||
_translucentDraws.Clear();
|
||||
|
||||
|
|
@ -1429,6 +1448,9 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
int lsSrc = i * LightManager.MaxLightsPerObject;
|
||||
for (int k = 0; k < LightManager.MaxLightsPerObject; k++)
|
||||
_lightSetData[lsDst + k] = grp.LightSets[lsSrc + k];
|
||||
// #142: IndoorFlags[] is parallel to Matrices[]; write at the same
|
||||
// cursor so binding=6 instanceIndoor[] tracks binding=0 instances.
|
||||
_indoorData[cursor] = grp.IndoorFlags[i];
|
||||
cursor++;
|
||||
}
|
||||
|
||||
|
|
@ -1514,6 +1536,12 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
fixed (uint* sp = _clipSlotData)
|
||||
UploadSsbo(_clipSlotSsbo, 3, sp, totalInstances * sizeof(uint));
|
||||
|
||||
// #142: per-instance indoor flag buffer (binding=6), one uint per instance,
|
||||
// laid out parallel to _instanceData in Phase 3. Only [0..totalInstances)
|
||||
// is uploaded — stale tail never read (same guarantee as clip-slot above).
|
||||
fixed (uint* dp = _indoorData)
|
||||
UploadSsbo(_instIndoorSsbo, 6, dp, 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
|
||||
|
|
@ -2049,12 +2077,17 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
/// </summary>
|
||||
private void ComputeEntityLightSet(WorldEntity entity)
|
||||
{
|
||||
// #142: set the indoor flag first so it's available even when the early-return
|
||||
// fires below. Both the torch selection and the sun gate use the same predicate,
|
||||
// so they can't disagree — one call, one truth.
|
||||
_currentEntityIndoor = IndoorObjectReceivesTorches(entity.ParentCellId);
|
||||
|
||||
Array.Fill(_currentEntityLightSet, -1);
|
||||
var snap = _pointSnapshot;
|
||||
if (snap is null || snap.Count == 0) return;
|
||||
|
||||
// Retail useSunlight gate: outdoor objects receive no per-object torches.
|
||||
if (!IndoorObjectReceivesTorches(entity.ParentCellId)) return;
|
||||
if (!_currentEntityIndoor) return; // #142: reuse the cached flag (was: IndoorObjectReceivesTorches(...))
|
||||
|
||||
if (entity.AabbDirty) entity.RefreshAabb();
|
||||
Vector3 center = (entity.AabbMin + entity.AabbMax) * 0.5f;
|
||||
|
|
@ -2085,6 +2118,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
{
|
||||
for (int k = 0; k < LightManager.MaxLightsPerObject; k++)
|
||||
grp.LightSets.Add(_currentEntityLightSet[k]);
|
||||
grp.IndoorFlags.Add(_currentEntityIndoor ? 1u : 0u); // #142, parallel to the light block
|
||||
}
|
||||
|
||||
private void ClassifyBatches(
|
||||
|
|
@ -2224,6 +2258,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
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 (_instIndoorSsbo != 0) _gl.DeleteBuffer(_instIndoorSsbo); // #142 binding=6
|
||||
if (_gpuQueriesInitialized)
|
||||
{
|
||||
for (int i = 0; i < GpuQueryRingDepth; i++)
|
||||
|
|
@ -2417,5 +2452,11 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
// same cursor, so the binding=5 instanceLightIdx[] tracks the binding=0
|
||||
// instance. -1 = unused slot.
|
||||
public readonly List<int> LightSets = new();
|
||||
|
||||
// #142: per-instance "indoor" flag, parallel to Matrices. IndoorFlags[i] is
|
||||
// 1 when the instance's entity is parented to an EnvCell (skip the sun); 0
|
||||
// for outdoor objects (gets the sun). Written into _indoorData at the same
|
||||
// cursor as Matrices, so binding=6 instanceIndoor[] tracks binding=0.
|
||||
public readonly List<uint> IndoorFlags = new();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue