fix(render): A7 point-light shape — per-vertex Gouraud + faithful calc_point_light (wrap + norm)

The torch/point-light look was wrong two ways, both now fixed against the
named retail decomp (calc_point_light 0x0059c8b0) via our verified
LightBake.PointContribution port:

1. Per-PIXEL → per-VERTEX. accumulateLights moved from mesh_modern.frag to
   mesh_modern.vert so point lights Gouraud-interpolate across each triangle
   the way retail's fixed-function T&L does. The per-pixel eval made a tight,
   hard-edged "spotlight" pool on flat walls; per-vertex is a soft, broad
   gradient. frag now just consumes the interpolated vLit (+ fog + flash).

2. Simplified ramp → faithful calc_point_light shape. The live point/spot
   branch was max(0,N·L) × linear(1−d/range) × cap — missing two terms our
   LightBake.cs port already has:
     • half-Lambert WRAP (1/1.5)·(N·D + 0.5·d), D un-normalised — a face
       angled away from a torch still catches light (retail's soft terminator)
       instead of snapping to black.
     • distance-cube NORM branch norm = distsq>1 ? distsq·d : d — inverse-
       square-ish soft far halo + punchy near field, vs the flat linear ramp.
   Per-channel no-blowout cap (min(scale·color, color)) retained.

The per-channel cap was also added to the legacy mesh.frag for consistency.

A read-only retail-vs-acdream lighting audit (11-agent workflow) confirmed
these two as the cause of the "better but a bit off" look and cleared the
ambient/sun/terrain/color-space chain as already faithful. Remaining
confirmed divergences (per-object light selection; dungeon static vertex
bake) are filed as the next fixes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-15 22:27:27 +02:00
parent d2b8a51426
commit aa94cedc38
3 changed files with 93 additions and 41 deletions

View file

@ -4,6 +4,7 @@
in vec3 vNormal;
in vec2 vTexCoord;
in vec3 vWorldPos;
in vec3 vLit; // A7: per-vertex Gouraud lighting (ambient + capped lights), from mesh_modern.vert
in flat uvec2 vTextureHandle;
in flat uint vTextureLayer;
@ -31,44 +32,11 @@ layout(std140, binding = 1) uniform SceneLighting {
vec4 uCameraAndTime;
};
vec3 accumulateLights(vec3 N, vec3 worldPos) {
vec3 lit = uCellAmbient.xyz;
int activeLights = int(uCellAmbient.w);
for (int i = 0; i < 8; ++i) {
if (i >= activeLights) break;
int kind = int(uLights[i].posAndKind.w);
vec3 Lcol = uLights[i].colorAndIntensity.xyz * uLights[i].colorAndIntensity.w;
if (kind == 0) {
vec3 Ldir = -uLights[i].dirAndRange.xyz;
float ndl = max(0.0, dot(N, Ldir));
lit += Lcol * ndl;
} else {
vec3 toL = uLights[i].posAndKind.xyz - worldPos;
float d = length(toL);
float range = uLights[i].dirAndRange.w;
if (d < range && range > 1e-3) {
vec3 Ldir = toL / max(d, 1e-4);
float ndl = max(0.0, dot(N, Ldir));
// 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) {
float cos_edge = cos(uLights[i].coneAngleEtc.x * 0.5);
float cos_l = dot(-Ldir, uLights[i].dirAndRange.xyz);
atten *= (cos_l > cos_edge) ? 1.0 : 0.0;
}
lit += Lcol * ndl * atten;
}
}
}
return lit;
}
// A7 (2026-06-15): per-vertex lighting moved to mesh_modern.vert (Gouraud) to match
// retail's fixed-function per-vertex T&L — a per-pixel evaluation made a hard "spotlight"
// pool. The SceneLighting UBO above is still declared here for fog (uFogParams/uFogColor/
// uCameraAndTime) + the lightning-flash bump; its uLights[]/uCellAmbient are now consumed
// in the vertex shader. The std140 layout must stay identical to the vert + the CPU upload.
vec3 applyFog(vec3 lit, vec3 worldPos) {
int mode = int(uFogParams.w);
@ -114,8 +82,8 @@ void main() {
if (color.a < 0.05) discard;
}
vec3 N = normalize(vNormal);
vec3 lit = accumulateLights(N, vWorldPos);
// Per-vertex Gouraud lighting from the vertex shader (ambient + capped lights).
vec3 lit = vLit;
// Lightning flash — additive scene bump (matches mesh_instanced.frag).
lit += uFogParams.z * vec3(0.6, 0.6, 0.75);