merge: bring main into claude/hopeful-maxwell-214a12 (LayoutDesc importer branch)

main was 65 commits ahead of this branch's fork point. Only conflict was the
divergence register: both sides appended an 'AP-32' row. Resolved by keeping
main's AP-32..AP-36 (cell-shell lift, look-in cells, alpha deferral, dungeon
streaming, point lights) and renumbering the importer's row to AP-37; AP header
count -> 37. GameWindow.cs auto-merged cleanly. Verified: AcDream.App builds
0/0; AcDream.App.Tests 354 passed / 1 skipped / 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-15 16:19:15 +02:00
commit 5ac9d8c19c
53 changed files with 6691 additions and 439 deletions

View file

@ -46,10 +46,12 @@ layout(std140, binding = 1) uniform SceneLighting {
vec4 uCameraAndTime;
};
// Retail hard-cutoff lighting equation (r13 §10.2). No distance
// attenuation inside Range; hard edge at Range; spotlights use a
// binary cos-cone test. This is deliberate — the retail "bubble of
// light" look relies on crisp boundaries.
// Retail per-vertex point-light ramp (calc_point_light 0x0059c8b0): the
// contribution scales by (1 - dist/falloff_eff) — a LINEAR fade to exactly
// 0 at the edge, NOT a hard-cutoff bubble. (The prior "no attenuation inside
// 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 lit = uCellAmbient.xyz;
int activeLights = int(uCellAmbient.w);
@ -73,7 +75,9 @@ vec3 accumulateLights(vec3 N, vec3 worldPos) {
if (d < range && range > 1e-3) {
vec3 Ldir = toL / max(d, 1e-4);
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) {
// Spotlight: hard-edged cos-cone test.
float cos_edge = cos(uLights[i].coneAngleEtc.x * 0.5);

View file

@ -49,7 +49,15 @@ vec3 accumulateLights(vec3 N, vec3 worldPos) {
if (d < range && range > 1e-3) {
vec3 Ldir = toL / max(d, 1e-4);
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) {
float cos_edge = cos(uLights[i].coneAngleEtc.x * 0.5);
float cos_l = dot(-Ldir, uLights[i].dirAndRange.xyz);