feat(lighting): SelectForCell (all dynamic lights per EnvCell) + #176 handoff — residual is a runtime z-fight
cdb trace of LIVE retail (tools/cdb/issue176-floor-light.cdb, binary<->PDB MATCH) PROVED retail applies ALL its dynamic lights — 4 intensity-100 magenta portal lights (d3dIdx 3-6, falloff 6) + the viewer fill (d3dIdx 1, 2.25) — as D3D hardware lights to EVERY Facility Hub cell, every frame, stable. So the faceted purple wedges on the floor are retail-FAITHFUL. acdream did a per-cell SelectForObject sphere-overlap 8-cap for cells, so the portal set could differ/flip per cell. - LightManager.SelectForCell (retail minimize_envcell_lighting 0x0054c170): ALL dynamic lights applied unconditionally (shader range cutoff zeroes non-reaching = D3D hardware range), then nearest static torches fill remaining slots. Wired into EnvCellRenderer.GetCellLightSet. Objects keep SelectForObject (minimize_object_lighting). Pins: SelectForCell_AppliesAllDynamicLights_EvenOutOfReach + _SameDynamicSet_ForCellsFarApart_NoFlap. - Apparatus: [light-detail] gains owner/cell/dyn (pinned the culprit = 2 portal weenies 0x000F4247/48 in 0x8A020118/19, intensity=100 magenta); CellVertexNormals_SmoothOrFaceted_Dump (corridor floor uses SMOOTH per-vertex dat normals, not flat); tools/cdb/issue176-floor-light.cdb. #176 RESIDUAL is NOT this fix. It's a RUNTIME draw z-fight in the seam floor. Eliminated (evidence): NOT lighting (per-light cap + this both no-change), NOT membership (render cell 0x8A020164 stable 100% of 188k frames / 526 angles, res=None), NOT dat geometry (coplanar sweep empty at z=-6 floor incl. cell 0164). NEXT = RenderDoc pixel-history. Full handoff + DO-NOT-RETRY: docs/research/2026-07-06-176-seam-floor-zfight-handoff.md. Suites green: Core 2599 + 2 skip. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
92bb27c03b
commit
8cb3176daa
8 changed files with 366 additions and 11 deletions
|
|
@ -379,4 +379,75 @@ public sealed class LightManager
|
|||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-CELL light selection — retail <c>minimize_envcell_lighting</c> (0x0054c170).
|
||||
/// Unlike <see cref="SelectForObject"/> (per-object sphere-overlap cull), retail enables
|
||||
/// the ENTIRE dynamic subset for EVERY EnvCell it draws (verified by a live cdb trace of
|
||||
/// <c>config_hardware_light</c>: the same 4 intensity-100 portal lights are applied to
|
||||
/// every Facility Hub cell, every frame). So here: ALL dynamic lights are added
|
||||
/// unconditionally (the shader's per-light range cutoff zeroes ones that don't reach —
|
||||
/// same as D3D's hardware range), THEN remaining slots fill with the nearest STATIC lights
|
||||
/// that reach the cell sphere. This is what makes a cell's floor lighting STABLE as the
|
||||
/// portal flood shifts — a per-cell sphere-overlap cull of the dynamics is what made the
|
||||
/// floor lighting FLAP (#176). Objects keep <see cref="SelectForObject"/>
|
||||
/// (retail minimize_object_lighting).
|
||||
/// </summary>
|
||||
public static int SelectForCell(
|
||||
IReadOnlyList<LightSource> snapshot,
|
||||
Vector3 center,
|
||||
float radius,
|
||||
Span<int> outIndices)
|
||||
{
|
||||
int cap = Math.Min(outIndices.Length, MaxLightsPerObject);
|
||||
if (cap <= 0) return 0;
|
||||
|
||||
int count = 0;
|
||||
|
||||
// 1) ALL dynamic lights, unconditionally (retail applies the whole dynamic subset to
|
||||
// every cell — stable regardless of the cell's relation to each light).
|
||||
for (int li = 0; li < snapshot.Count && count < cap; li++)
|
||||
if (snapshot[li].IsDynamic)
|
||||
outIndices[count++] = li;
|
||||
|
||||
// 2) Fill remaining slots with the nearest STATIC lights that reach the cell sphere,
|
||||
// insertion-sorted among the static slots only (dynamic slots [0..staticStart) are fixed).
|
||||
int staticStart = count;
|
||||
Span<float> keptDistSq = stackalloc float[MaxLightsPerObject];
|
||||
for (int li = 0; li < snapshot.Count; li++)
|
||||
{
|
||||
var light = snapshot[li];
|
||||
if (light.IsDynamic) continue; // dynamics already added
|
||||
float reach = light.Range + radius;
|
||||
float dsq = (light.WorldPosition - center).LengthSquared();
|
||||
if (dsq >= reach * reach) continue;
|
||||
|
||||
if (count < cap)
|
||||
{
|
||||
int j = count;
|
||||
while (j > staticStart && keptDistSq[j - 1] > dsq)
|
||||
{
|
||||
keptDistSq[j] = keptDistSq[j - 1];
|
||||
outIndices[j] = outIndices[j - 1];
|
||||
j--;
|
||||
}
|
||||
keptDistSq[j] = dsq;
|
||||
outIndices[j] = li;
|
||||
count++;
|
||||
}
|
||||
else if (staticStart < cap && dsq < keptDistSq[cap - 1])
|
||||
{
|
||||
int j = cap - 1;
|
||||
while (j > staticStart && keptDistSq[j - 1] > dsq)
|
||||
{
|
||||
keptDistSq[j] = keptDistSq[j - 1];
|
||||
outIndices[j] = outIndices[j - 1];
|
||||
j--;
|
||||
}
|
||||
keptDistSq[j] = dsq;
|
||||
outIndices[j] = li;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -471,9 +471,10 @@ public static class RenderingDiagnostics
|
|||
|
||||
float dist = ls.DistSq >= 0f ? MathF.Sqrt(ls.DistSq) : 0f;
|
||||
Console.WriteLine(string.Format(ci,
|
||||
"[light-detail] kind={0} range={1:0.###} intensity={2:0.###} cone={3:0.####} color=({4:0.###},{5:0.###},{6:0.###}) distToViewer={7:0.###}",
|
||||
"[light-detail] kind={0} range={1:0.###} intensity={2:0.###} cone={3:0.####} color=({4:0.###},{5:0.###},{6:0.###}) distToViewer={7:0.###} owner=0x{8:X8} cell=0x{9:X8} dyn={10}",
|
||||
ls.Kind, ls.Range, ls.Intensity, ls.ConeAngle,
|
||||
ls.ColorLinear.X, ls.ColorLinear.Y, ls.ColorLinear.Z, dist));
|
||||
ls.ColorLinear.X, ls.ColorLinear.Y, ls.ColorLinear.Z, dist,
|
||||
ls.OwnerId, ls.CellId, ls.IsDynamic ? 1 : 0));
|
||||
shown++;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue