fix(A7): port retail calc_point_light (1-dist/falloff) ramp — kill the "spotlight" hard edge (#133)
The dungeon/house/outdoor lights read as hard-edged blown discs ("spotlights")
because our point/spot shader used `atten = 1.0` flat inside a hard `d < range`
cutoff. The mesh.frag comment claimed this was retail-faithful ("no attenuation
inside Range... the bubble-of-light look relies on crisp boundaries", citing
r13 10.2) — that was a misread and the literal cause of the symptom.
Verified against the decomp (not guessed): calc_point_light (0x0059c8b0, the
PER-VERTEX point-light path that lights static walls) scales each light's
contribution by (1 - dist/falloff_eff) — a LINEAR ramp that fades to exactly 0
at the edge, eliminating the hard disc. falloff_eff = Falloff * static_light_factor,
and static_light_factor = 1.3 (0x00820e24), NOT the 1.5 config_hardware_light
rangeAdjust (that 1.5 is the D3D-dynamic path for moving objects, a different
path). The Ghidra port (acclient.c:808639) is more garbled — BN pseudo-C is the
oracle here; the exact normalization factor + a half-Lambert wrap (0.5*dist+N*L)
are x87-obscured (same artifact class as GetPowerBarLevel) and left unported.
Changes:
- mesh_modern.frag + mesh.frag: replace flat atten with clamp(1 - d/range, 0, 1);
Range now carries falloff_eff so the ramp fades to 0 at the cutoff. Fix the
false "no attenuation / crisp bubble" comment in mesh.frag.
- LightInfoLoader: Range = Falloff * 1.3 (static_light_factor), was * 1.5.
- LightManager: correct the stale class doc comment (Tick is now nearest-8
allocation-free partial-select with NO viewer-range slack filter).
- divergence register: AP-16 updated (slack filter removed), AP-35 added
(per-pixel vs per-vertex Gouraud; dropped half-Lambert wrap + normalization).
- test: LightingHookSinkTests Range 8*1.3 = 10.4.
Build + 20 lighting tests green. Visual gate pending (game-wide lighting change:
dungeon torches, house candles, outdoor braziers).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5872bcf075
commit
007e287309
6 changed files with 44 additions and 26 deletions
|
|
@ -92,7 +92,7 @@ accepted-divergence entries (#96, #49, #50).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Documented approximation (AP) — 34 rows
|
## 3. Documented approximation (AP) — 35 rows
|
||||||
|
|
||||||
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
|
|
@ -111,7 +111,7 @@ accepted-divergence entries (#96, #49, #50).
|
||||||
| AP-13 | `ComputeDamage` is a simplified retail damage formula (no augmentations/ratings) — verified DEAD CODE as of 2026-06-04, M2 scaffolding | `src/AcDream.Core/Combat/CombatModel.cs:184` | Not on the critical path; stubbed from r02 §5 + ACE CombatManager for the future M2 predictive display | If wired into the M2 attack-bar estimate as-is, predicted numbers diverge whenever augs/ratings apply | r02 §5; ACE CombatManager |
|
| AP-13 | `ComputeDamage` is a simplified retail damage formula (no augmentations/ratings) — verified DEAD CODE as of 2026-06-04, M2 scaffolding | `src/AcDream.Core/Combat/CombatModel.cs:184` | Not on the critical path; stubbed from r02 §5 + ACE CombatManager for the future M2 predictive display | If wired into the M2 attack-bar estimate as-is, predicted numbers diverge whenever augs/ratings apply | r02 §5; ACE CombatManager |
|
||||||
| AP-14 | Encumbrance multiplier is a rough piecewise-linear stand-in (1.0→50%, ~0.7@100%, 0.1@300%) for retail's exact curve | `src/AcDream.Core/Items/ItemInstance.cs:187` | Hand-fit segments capture the curve's shape for scaffolding | Client-side burden-scaled effects (speed prediction) differ from retail at most burden ratios when loaded | r06 §6 (retail encumbered multiplier curve) |
|
| AP-14 | Encumbrance multiplier is a rough piecewise-linear stand-in (1.0→50%, ~0.7@100%, 0.1@300%) for retail's exact curve | `src/AcDream.Core/Items/ItemInstance.cs:187` | Hand-fit segments capture the curve's shape for scaffolding | Client-side burden-scaled effects (speed prediction) differ from retail at most burden ratios when loaded | r06 §6 (retail encumbered multiplier curve) |
|
||||||
| AP-15 | WeenieError translation table covers only ~30 common codes (from ACE enum docs, not retail string_table.bin); unknown codes render raw hex | `src/AcDream.Core/Chat/WeenieErrorMessages.cs:26` | Untranslated codes are rare, fall back losslessly, 30-second add when reported | Server messages outside the table show as raw hex instead of the retail sentence | retail string_table.bin; ACE WeenieError*.cs |
|
| AP-15 | WeenieError translation table covers only ~30 common codes (from ACE enum docs, not retail string_table.bin); unknown codes render raw hex | `src/AcDream.Core/Chat/WeenieErrorMessages.cs:26` | Untranslated codes are rare, fall back losslessly, 30-second add when reported | Server messages outside the table show as raw hex instead of the retail sentence | retail string_table.bin; ACE WeenieError*.cs |
|
||||||
| AP-16 | Global nearest-8 viewer-distance light selection with 10% range slack (own r13 design); retail bound D3D lights per object/cell | `src/AcDream.Core/Lighting/LightManager.cs:10` | Honors retail's 8-hardware-light constraint while fitting a global-uniform shader; 1.1 slack is anti-pop hysteresis | With >7 nearby lights, different objects are lit than retail would light (retail's per-object pick can light a far object by ITS nearest lights); pop thresholds differ | r13 §12.2 (acdream design); retail D3D 8-light constraint |
|
| AP-16 | Global nearest-8 viewer-distance light selection (own r13 design); retail bound D3D lights per object/cell. NO viewer-range candidacy filter — each light's range cutoff is applied per-surface in the shader (the earlier `Range²×1.1` slack filter was removed; it dropped torches the viewer stood outside, the #133 "lighting off" report) | `src/AcDream.Core/Lighting/LightManager.cs:10` | Honors retail's 8-hardware-light constraint while fitting a global-uniform shader; nearest-8 is an allocation-free partial-select (no per-frame list/sort) | With >7 nearby lights, different objects are lit than retail would light (retail's per-object pick can light a far object by ITS nearest lights) | r13 §12.2 (acdream design); retail D3D 8-light constraint |
|
||||||
| AP-17 | Spell metadata from third-party CSV (3,956 rows, bad rows silently skipped), not the portal.dat SpellTable; Family feeds stacking decisions | `src/AcDream.Core/Spells/SpellTable.cs:10` | The dat spell-table port (obfuscated/encrypted aspects) wasn't done; CSV closed #11 fast and unblocked #6 stacking | Any CSV↔dat drift (wrong Family, missing rows) silently produces wrong buff-stacking winners and wrong panel info | portal.dat SpellTable 0x0E00000E |
|
| AP-17 | Spell metadata from third-party CSV (3,956 rows, bad rows silently skipped), not the portal.dat SpellTable; Family feeds stacking decisions | `src/AcDream.Core/Spells/SpellTable.cs:10` | The dat spell-table port (obfuscated/encrypted aspects) wasn't done; CSV closed #11 fast and unblocked #6 stacking | Any CSV↔dat drift (wrong Family, missing rows) silently produces wrong buff-stacking winners and wrong panel info | portal.dat SpellTable 0x0E00000E |
|
||||||
| AP-18 | Radar/indicator RGBA hand-tuned from screenshots; dispatch order ports `GetBlipColor` exactly but the real `RGBAColor_Radar*` static data is unrecovered | `src/AcDream.Core/Ui/RadarBlipColors.cs:33` | Color constants live in retail static data not yet extracted; comment invites tightening when recovered | Blip/indicator hues differ subtly from retail color cues | `gmRadarUI::GetBlipColor` 0x004d76f0; RGBAColor_Radar* (unrecovered) |
|
| AP-18 | Radar/indicator RGBA hand-tuned from screenshots; dispatch order ports `GetBlipColor` exactly but the real `RGBAColor_Radar*` static data is unrecovered | `src/AcDream.Core/Ui/RadarBlipColors.cs:33` | Color constants live in retail static data not yet extracted; comment invites tightening when recovered | Blip/indicator hues differ subtly from retail color cues | `gmRadarUI::GetBlipColor` 0x004d76f0; RGBAColor_Radar* (unrecovered) |
|
||||||
| AP-19 | `PortalSideEpsilon` 0.01 (≈1 cm) instead of retail F_EPSILON ≈ 0.0002 — a documented render-root-lag tolerance, NOT a retail constant. DO-NOT-RETRY: T2 (BR-4) tried the retail value; CornerFloodReplay refuted it | `src/AcDream.App/Rendering/PortalVisibilityBuilder.cs:49` | Retail's tight epsilon only works with eye-exact swept curr_cell tracking; our viewer cell lags the eye by up to ~1 cm at pressed corners. Tighten after the #108-membership family + cdstW near-clip pin land | A 1 cm misclassification band at portal planes can flood or cull a portal the eye hasn't crossed — one-frame leaks / grey flashes at knife-edge doorway/corner positions | F_EPSILON @0x007c8c70; `PView::InitCell` 0x005a4b70 |
|
| AP-19 | `PortalSideEpsilon` 0.01 (≈1 cm) instead of retail F_EPSILON ≈ 0.0002 — a documented render-root-lag tolerance, NOT a retail constant. DO-NOT-RETRY: T2 (BR-4) tried the retail value; CornerFloodReplay refuted it | `src/AcDream.App/Rendering/PortalVisibilityBuilder.cs:49` | Retail's tight epsilon only works with eye-exact swept curr_cell tracking; our viewer cell lags the eye by up to ~1 cm at pressed corners. Tighten after the #108-membership family + cdstW near-clip pin land | A 1 cm misclassification band at portal planes can flood or cull a portal the eye hasn't crossed — one-frame leaks / grey flashes at knife-edge doorway/corner positions | F_EPSILON @0x007c8c70; `PView::InitCell` 0x005a4b70 |
|
||||||
|
|
@ -130,6 +130,7 @@ accepted-divergence entries (#96, #49, #50).
|
||||||
| AP-32 | Cell shells DRAW +0.02 m above the dat EnvCell origin (`ShellDrawLiftZ`, z-fight vs coplanar terrain); retail draws at the origin verbatim. Split invariant: PHYSICS + visibility graph UNLIFTED (f35cb8b, **#119**-residual), every DRAW-space consumer of portal/cell geometry LIFTED (OutsideView color gate via `Build(drawLiftZ)`, seal/punch fans — **#130**) | `src/AcDream.App/Rendering/GameWindow.cs:5604` (const at `PortalVisibilityBuilder.ShellDrawLiftZ`) | Shell floors coplanar with terrain z-fight in our z-buffered frame; the 2 cm lift is the documented stand-in | A new draw-space consumer of portal/cell polygons that forgets the lift re-opens a 2 cm seam at horizontal aperture edges (the #130 top-edge strip, ~7 px at 2.4 m); a visibility consumer that picks up the LIFTED transform re-opens the #119-residual horizontal-portal side-cull | retail draws cell geometry at the dat EnvCell origin (no lift) |
|
| AP-32 | Cell shells DRAW +0.02 m above the dat EnvCell origin (`ShellDrawLiftZ`, z-fight vs coplanar terrain); retail draws at the origin verbatim. Split invariant: PHYSICS + visibility graph UNLIFTED (f35cb8b, **#119**-residual), every DRAW-space consumer of portal/cell geometry LIFTED (OutsideView color gate via `Build(drawLiftZ)`, seal/punch fans — **#130**) | `src/AcDream.App/Rendering/GameWindow.cs:5604` (const at `PortalVisibilityBuilder.ShellDrawLiftZ`) | Shell floors coplanar with terrain z-fight in our z-buffered frame; the 2 cm lift is the documented stand-in | A new draw-space consumer of portal/cell polygons that forgets the lift re-opens a 2 cm seam at horizontal aperture edges (the #130 top-edge strip, ~7 px at 2.4 m); a visibility consumer that picks up the LIFTED transform re-opens the #119-residual horizontal-portal side-cull | retail draws cell geometry at the dat EnvCell origin (no lift) |
|
||||||
| AP-33 | Interior-root look-in cells (**#124** sub-pass) draw their statics + DYNAMICS + emitters WHOLE — no per-part/per-object viewcone check; retail viewconeCheck's each vs the installed view (the **#131** portal closure: a server object in a look-in cell drew nowhere — dynamics-last culls cells absent from the main cone, and post-seal it z-fails anyway) | `src/AcDream.App/Rendering/RetailPViewRenderer.cs` (`DrawBuildingLookIns`) | The main viewcone has no entries for look-in cells; over-include is the safe direction (z-correct, repainted outside apertures by the root's shells); look-in cell counts are small (~1-3 cells) | A few wasted draws on content outside the doorway region (repainted); no under-draw direction remains | `viewconeCheck` 0x0054c250; nested `DrawCells` objects pc:432878 |
|
| AP-33 | Interior-root look-in cells (**#124** sub-pass) draw their statics + DYNAMICS + emitters WHOLE — no per-part/per-object viewcone check; retail viewconeCheck's each vs the installed view (the **#131** portal closure: a server object in a look-in cell drew nowhere — dynamics-last culls cells absent from the main cone, and post-seal it z-fails anyway) | `src/AcDream.App/Rendering/RetailPViewRenderer.cs` (`DrawBuildingLookIns`) | The main viewcone has no entries for look-in cells; over-include is the safe direction (z-correct, repainted outside apertures by the root's shells); look-in cell counts are small (~1-3 cells) | A few wasted draws on content outside the doorway region (repainted); no under-draw direction remains | `viewconeCheck` 0x0054c250; nested `DrawCells` objects pc:432878 |
|
||||||
| AP-34 | Landscape-stage alpha deferral is a TWO-PHASE slice split (statics-early / dynamics+particles+weather-late around the **#124** look-ins) + outdoor-root attached scene emitters moved to the post-frame pass, not retail's single deferred alpha flush. Residual: building exteriors' / outside-stage dynamics' own translucent MESH batches still draw within their stage draw call (before later stage content) | `src/AcDream.App/Rendering/RetailPViewRenderer.cs` (`DrawLandscapeThroughOutsideView` late loop) + `GameWindow` post-frame Scene pass | The MDI dispatcher draws translucency inside each Draw call; a faithful FlushAlphaList port needs a global deferred alpha list across all landscape draws — the split covers the user-visible cases (#131 portal swirl, #132 candle flame indoors + outdoors) | Translucent landscape content drawn early and screen-overlapped by content drawn later in the stage gets overpainted (no depth self-protection) — the portal-swirl/candle-flame class re-appears in the residual configurations | `D3DPolyRender::FlushAlphaList` (DrawCells pc:432722) |
|
| AP-34 | Landscape-stage alpha deferral is a TWO-PHASE slice split (statics-early / dynamics+particles+weather-late around the **#124** look-ins) + outdoor-root attached scene emitters moved to the post-frame pass, not retail's single deferred alpha flush. Residual: building exteriors' / outside-stage dynamics' own translucent MESH batches still draw within their stage draw call (before later stage content) | `src/AcDream.App/Rendering/RetailPViewRenderer.cs` (`DrawLandscapeThroughOutsideView` late loop) + `GameWindow` post-frame Scene pass | The MDI dispatcher draws translucency inside each Draw call; a faithful FlushAlphaList port needs a global deferred alpha list across all landscape draws — the split covers the user-visible cases (#131 portal swirl, #132 candle flame indoors + outdoors) | Translucent landscape content drawn early and screen-overlapped by content drawn later in the stage gets overpainted (no depth self-protection) — the portal-swirl/candle-flame class re-appears in the residual configurations | `D3DPolyRender::FlushAlphaList` (DrawCells pc:432722) |
|
||||||
|
| AP-35 | Point/spot lights use a single PER-PIXEL accumulation that ports `calc_point_light`'s `(1 − dist/falloff_eff)` LINEAR ramp (falloff_eff = Falloff × static_light_factor 1.3) + standard Lambert `N·L`; retail's path is PER-VERTEX Gouraud and additionally applies a half-Lambert wrap (`0.5·dist + N·L_vec`, lights surfaces down to `N·L ≥ −0.5`) and an x87-obscured normalization factor, neither ported | `src/AcDream.App/Rendering/Shaders/mesh_modern.frag:52` (+ `mesh.frag`; `LightInfoLoader.cs:81` folds 1.3 into Range) | The linear ramp is the user-visible fix (kills the hard-disc "spotlight" edge, #133 A7); the dropped wrap/normalization only re-shade the gradient slightly, and per-pixel vs per-vertex Gouraud chiefly differs on coarse geometry. Half-Lambert wrap + factor are an x87-decompile refinement (same artifact class as GetPowerBarLevel AP-24) | Surfaces facing slightly away from a torch (`−0.5 ≤ N·L < 0`) stay dark where retail's wrap lights them faintly; near-light gradient shading differs subtly from retail's per-vertex bake | `calc_point_light` 0x0059c8b0 (line 0x0059c9a2 ramp; 0x0059c925 wrap); static_light_factor 0x00820e24 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,10 +46,12 @@ layout(std140, binding = 1) uniform SceneLighting {
|
||||||
vec4 uCameraAndTime;
|
vec4 uCameraAndTime;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Retail hard-cutoff lighting equation (r13 §10.2). No distance
|
// Retail per-vertex point-light ramp (calc_point_light 0x0059c8b0): the
|
||||||
// attenuation inside Range; hard edge at Range; spotlights use a
|
// contribution scales by (1 - dist/falloff_eff) — a LINEAR fade to exactly
|
||||||
// binary cos-cone test. This is deliberate — the retail "bubble of
|
// 0 at the edge, NOT a hard-cutoff bubble. (The prior "no attenuation inside
|
||||||
// light" look relies on crisp boundaries.
|
// Range / crisp boundaries" note was a misread; it is the literal cause of
|
||||||
|
// the #133 "spotlight" look. falloff_eff = Falloff * static_light_factor 1.3
|
||||||
|
// is folded into Range by LightInfoLoader.) Spots add a binary cos-cone test.
|
||||||
vec3 accumulateLights(vec3 N, vec3 worldPos) {
|
vec3 accumulateLights(vec3 N, vec3 worldPos) {
|
||||||
vec3 lit = uCellAmbient.xyz;
|
vec3 lit = uCellAmbient.xyz;
|
||||||
int activeLights = int(uCellAmbient.w);
|
int activeLights = int(uCellAmbient.w);
|
||||||
|
|
@ -73,7 +75,9 @@ vec3 accumulateLights(vec3 N, vec3 worldPos) {
|
||||||
if (d < range && range > 1e-3) {
|
if (d < range && range > 1e-3) {
|
||||||
vec3 Ldir = toL / max(d, 1e-4);
|
vec3 Ldir = toL / max(d, 1e-4);
|
||||||
float ndl = max(0.0, dot(N, Ldir));
|
float ndl = max(0.0, dot(N, Ldir));
|
||||||
float atten = 1.0; // retail: no attenuation inside Range
|
// calc_point_light (1 - dist/falloff_eff) linear ramp; Range already
|
||||||
|
// carries falloff_eff (Falloff * 1.3), so it fades to 0 at the cutoff.
|
||||||
|
float atten = clamp(1.0 - d / max(range, 1e-3), 0.0, 1.0);
|
||||||
if (kind == 2) {
|
if (kind == 2) {
|
||||||
// Spotlight: hard-edged cos-cone test.
|
// Spotlight: hard-edged cos-cone test.
|
||||||
float cos_edge = cos(uLights[i].coneAngleEtc.x * 0.5);
|
float cos_edge = cos(uLights[i].coneAngleEtc.x * 0.5);
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,15 @@ vec3 accumulateLights(vec3 N, vec3 worldPos) {
|
||||||
if (d < range && range > 1e-3) {
|
if (d < range && range > 1e-3) {
|
||||||
vec3 Ldir = toL / max(d, 1e-4);
|
vec3 Ldir = toL / max(d, 1e-4);
|
||||||
float ndl = max(0.0, dot(N, Ldir));
|
float ndl = max(0.0, dot(N, Ldir));
|
||||||
float atten = 1.0;
|
// Retail per-vertex point-light ramp (calc_point_light 0x0059c8b0,
|
||||||
|
// line 0x0059c9a2): contribution scales by (1 - dist/falloff_eff), a
|
||||||
|
// LINEAR fade to exactly 0 at the edge. That is what makes a torch a
|
||||||
|
// smooth glow that blends into the ambient instead of a flat disc with
|
||||||
|
// a hard edge — the dungeon/house/outdoor "spotlight" look (#133 A7).
|
||||||
|
// falloff_eff = Falloff * static_light_factor (1.3, 0x00820e24) is folded
|
||||||
|
// into the shader Range (dirAndRange.w) by LightInfoLoader, so the ramp
|
||||||
|
// denominator is just Range and fades to 0 exactly at the cutoff.
|
||||||
|
float atten = clamp(1.0 - d / max(range, 1e-3), 0.0, 1.0);
|
||||||
if (kind == 2) {
|
if (kind == 2) {
|
||||||
float cos_edge = cos(uLights[i].coneAngleEtc.x * 0.5);
|
float cos_edge = cos(uLights[i].coneAngleEtc.x * 0.5);
|
||||||
float cos_l = dot(-Ldir, uLights[i].dirAndRange.xyz);
|
float cos_l = dot(-Ldir, uLights[i].dirAndRange.xyz);
|
||||||
|
|
|
||||||
|
|
@ -79,12 +79,15 @@ public static class LightInfoLoader
|
||||||
(info.Color?.Green ?? 255) / 255f,
|
(info.Color?.Green ?? 255) / 255f,
|
||||||
(info.Color?.Blue ?? 255) / 255f),
|
(info.Color?.Blue ?? 255) / 255f),
|
||||||
Intensity = info.Intensity,
|
Intensity = info.Intensity,
|
||||||
// Retail PrimD3DRender::config_hardware_light (0x0059ad30) sets the
|
// falloff_eff for the per-vertex point-light burn-in (calc_point_light
|
||||||
// hardware light Range = Falloff * rangeAdjust, where rangeAdjust is
|
// 0x0059c8b0) is Falloff * static_light_factor, where static_light_factor
|
||||||
// the fixed global 1.5 (0x00820cc4). Our prior Range = Falloff reached
|
// is the fixed global 1.3 (0x00820e24). That is the path that lights
|
||||||
// only 2/3 of retail's distance → tight torch bubbles (the dungeon
|
// STATIC walls — what the dungeon/house "spotlight" report (#133 A7) is
|
||||||
// "candles/spotlights" report, #133 A7). Match retail's reach.
|
// about — so we match it, not the D3D-dynamic config_hardware_light
|
||||||
Range = info.Falloff * 1.5f,
|
// rangeAdjust (1.5, a different path for moving objects). The shader ramp
|
||||||
|
// (1 - dist/Range) fades to exactly 0 at this Range, eliminating the hard
|
||||||
|
// disc edge that read as a spotlight.
|
||||||
|
Range = info.Falloff * 1.3f,
|
||||||
ConeAngle = info.ConeAngle,
|
ConeAngle = info.ConeAngle,
|
||||||
OwnerId = ownerId,
|
OwnerId = ownerId,
|
||||||
IsLit = true,
|
IsLit = true,
|
||||||
|
|
|
||||||
|
|
@ -11,23 +11,25 @@ namespace AcDream.Core.Lighting;
|
||||||
/// §12.2).
|
/// §12.2).
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Active-light selection algorithm (r13 §12.2 "Tick" steps):
|
/// Active-light selection algorithm (r13 §12.2), as implemented by
|
||||||
|
/// <see cref="Tick"/>:
|
||||||
/// <list type="number">
|
/// <list type="number">
|
||||||
/// <item><description>
|
/// <item><description>
|
||||||
/// Recompute <c>DistSq</c> from viewer to every registered
|
/// Reserve slot 0 for the sun (directional, infinite range) when present.
|
||||||
/// point/spot light.
|
|
||||||
/// </description></item>
|
/// </description></item>
|
||||||
/// <item><description>
|
/// <item><description>
|
||||||
/// Drop lights outside <c>Range² * 1.1</c> (10% slack prevents
|
/// For every registered lit point/spot light, recompute <c>DistSq</c>
|
||||||
/// pop as we walk across the boundary).
|
/// from the viewer and keep the nearest <c>(MaxActiveLights − sunSlot)</c>
|
||||||
/// </description></item>
|
/// directly in the active window via an allocation-free insertion
|
||||||
/// <item><description>
|
/// partial-select (no per-frame list/sort).
|
||||||
/// Rank remaining lights by <c>DistSq</c> ascending. Pick top 7.
|
|
||||||
/// </description></item>
|
|
||||||
/// <item><description>
|
|
||||||
/// Reserve slot 0 for the sun (directional, infinite range).
|
|
||||||
/// </description></item>
|
/// </description></item>
|
||||||
/// </list>
|
/// </list>
|
||||||
|
/// There is deliberately NO viewer-range candidacy filter: each light's
|
||||||
|
/// own range cutoff is applied PER SURFACE in the shader
|
||||||
|
/// (<c>mesh_modern.frag</c>: <c>d < range</c>), so a torch the viewer
|
||||||
|
/// stands outside the range of must still light the wall it sits on. The
|
||||||
|
/// earlier <c>Range² × 1.1</c> slack filter wrongly dropped exactly those
|
||||||
|
/// lights (the #133 "lighting off" report).
|
||||||
/// </para>
|
/// </para>
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ public sealed class LightInfoLoaderTests
|
||||||
var light = result[0];
|
var light = result[0];
|
||||||
Assert.Equal(LightKind.Point, light.Kind);
|
Assert.Equal(LightKind.Point, light.Kind);
|
||||||
Assert.Equal(77u, light.OwnerId);
|
Assert.Equal(77u, light.OwnerId);
|
||||||
Assert.Equal(12f, light.Range); // Falloff 8 × retail rangeAdjust 1.5 (config_hardware_light)
|
Assert.Equal(10.4f, light.Range, 3); // Falloff 8 × static_light_factor 1.3 (calc_point_light 0x00820e24)
|
||||||
Assert.Equal(0.8f, light.Intensity);
|
Assert.Equal(0.8f, light.Intensity);
|
||||||
Assert.Equal(new Vector3(101, 202, 303), light.WorldPosition);
|
Assert.Equal(new Vector3(101, 202, 303), light.WorldPosition);
|
||||||
Assert.InRange(light.ColorLinear.X, 0.99f, 1.01f);
|
Assert.InRange(light.ColorLinear.X, 0.99f, 1.01f);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue