merge: A7 lighting (Fix A point-light shape + Fix B per-object selection) into main
Brings the worktree branch claude/thirsty-goldberg-51bb9b into main: -aa94cedper-vertex Gouraud + faithful calc_point_light (wrap + norm) -4345e77per-OBJECT point-light selection (minimize_object_lighting) Auto-merged cleanly against the D.2b retail-UI line (only GameWindow.cs overlapped, resolved by git). Merged tree builds green; 35/35 Core lighting tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
37911ed510
7 changed files with 517 additions and 42 deletions
|
|
@ -84,7 +84,10 @@ vec3 accumulateLights(vec3 N, vec3 worldPos) {
|
|||
float cos_l = dot(-Ldir, uLights[i].dirAndRange.xyz);
|
||||
atten *= (cos_l > cos_edge) ? 1.0 : 0.0;
|
||||
}
|
||||
lit += Lcol * ndl * atten;
|
||||
// Retail per-channel "no-blowout" cap (calc_point_light 0x0059c8b0): a single
|
||||
// point/spot light can't push a channel past its own colour, regardless of
|
||||
// intensity (~100) — kills the close-torch overblow (#93). See mesh_modern.frag.
|
||||
lit += min(Lcol * ndl * atten, uLights[i].colorAndIntensity.xyz);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -69,6 +69,33 @@ layout(std430, binding = 3) readonly buffer ClipSlotBuf {
|
|||
uint instanceClipSlot[];
|
||||
};
|
||||
|
||||
// === Fix B (A7 #3): per-OBJECT light selection — minimize_object_lighting =====
|
||||
// retail picks up-to-8 point/spot lights PER OBJECT by the object's own position
|
||||
// (minimize_object_lighting 0x0054d480), so a torch always lights the wall it
|
||||
// sits on, camera-INDEPENDENTLY. The previous single global nearest-8-to-CAMERA
|
||||
// UBO set (LightManager.Tick) made a wall brighten as the camera approached
|
||||
// (its torches swapping into the global top-8). Two SSBOs replace that for
|
||||
// point/spot lights (the SUN + ambient still come from the SceneLighting UBO):
|
||||
//
|
||||
// binding=4 — GLOBAL point/spot light array, uploaded once per frame from
|
||||
// LightManager.PointSnapshot. The index of a light here is stable for the frame.
|
||||
// binding=5 — per-instance light SET: MaxLightsPerObject(8) int indices per
|
||||
// instance INTO gLights[] (-1 = unused slot), parallel to the binding=0
|
||||
// instance buffer and indexed by the SAME instanceIndex. WbDrawDispatcher fills
|
||||
// it once per entity (the set is constant across the entity's parts/tuples).
|
||||
struct GlobalLight {
|
||||
vec4 posAndKind;
|
||||
vec4 dirAndRange;
|
||||
vec4 colorAndIntensity;
|
||||
vec4 coneAngleEtc;
|
||||
};
|
||||
layout(std430, binding = 4) readonly buffer GlobalLightBuf {
|
||||
GlobalLight gLights[];
|
||||
};
|
||||
layout(std430, binding = 5) readonly buffer InstanceLightSetBuf {
|
||||
int instanceLightIdx[]; // 8 per instance; -1 = unused
|
||||
};
|
||||
|
||||
// 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
|
||||
|
|
@ -96,9 +123,95 @@ uniform mat4 uViewProjection;
|
|||
// uDrawIDOffset pattern in BaseObjectRenderManager.cs line 845.
|
||||
uniform int uDrawIDOffset;
|
||||
|
||||
// SceneLighting UBO — binding=1 in the UBO namespace (GL keeps the SSBO and UBO
|
||||
// binding tables separate, so this coexists with the binding=1 BatchBuffer SSBO
|
||||
// above). IDENTICAL std140 layout to mesh_modern.frag.
|
||||
//
|
||||
// A7 (2026-06-15): lighting moved from the FRAGMENT shader to HERE (per-VERTEX) so
|
||||
// torch/point lights Gouraud-interpolate across each triangle the way retail's
|
||||
// fixed-function T&L does (D3D DrawEnvCell vertex bake + minimize_object_lighting for
|
||||
// objects). A per-PIXEL evaluation made a tight bright "spotlight" pool on flat walls;
|
||||
// per-vertex spreads it into a soft, broad gradient with no hard edge.
|
||||
struct Light {
|
||||
vec4 posAndKind;
|
||||
vec4 dirAndRange;
|
||||
vec4 colorAndIntensity;
|
||||
vec4 coneAngleEtc;
|
||||
};
|
||||
layout(std140, binding = 1) uniform SceneLighting {
|
||||
Light uLights[8];
|
||||
vec4 uCellAmbient;
|
||||
vec4 uFogParams;
|
||||
vec4 uFogColor;
|
||||
vec4 uCameraAndTime;
|
||||
};
|
||||
|
||||
// Faithful calc_point_light (0x0059c8b0) contribution from ONE point/spot light —
|
||||
// the wrap + norm shape, factored out so the per-object SSBO loop shares it. D =
|
||||
// light − vertex, used UN-normalised (length = dist); N is the unit vertex normal.
|
||||
// Returns the RGB to ADD, already per-channel capped to the light's own colour.
|
||||
vec3 pointContribution(vec3 N, vec3 worldPos, GlobalLight L) {
|
||||
int kind = int(L.posAndKind.w);
|
||||
vec3 toL = L.posAndKind.xyz - worldPos; // D (un-normalised)
|
||||
float distsq = dot(toL, toL);
|
||||
float d = sqrt(distsq);
|
||||
float range = L.dirAndRange.w; // falloff_eff = Falloff × 1.3
|
||||
if (d >= range || range <= 1e-4) return vec3(0.0);
|
||||
// Half-Lambert WRAP: (1/1.5)·(N·D + 0.5·d). N·D = d·cosθ (D un-normalised); the
|
||||
// +0.5·d bias lets a face angled AWAY from the torch still catch light (retail's
|
||||
// soft terminator). wrap≤0 = fully shadowed. TwoLpr=1.5, WrapBias=0.5.
|
||||
float wrap = (1.0 / 1.5) * (dot(N, toL) + 0.5 * d);
|
||||
if (wrap <= 0.0) return vec3(0.0);
|
||||
// NORM branch (distance-cube): >1 m → distsq·d ≈ inverse-square soft far halo;
|
||||
// <1 m → just d (dodge the near singularity). "Punchy near, soft far."
|
||||
float norm = (distsq > 1.0) ? (distsq * d) : d;
|
||||
float intensity = L.colorAndIntensity.w;
|
||||
float scale = (1.0 - d / range) * intensity * (wrap / norm);
|
||||
if (kind == 2) {
|
||||
// Spotlight: hard-edged cos-cone gate layered on the point ramp.
|
||||
vec3 Ldir = toL / max(d, 1e-4);
|
||||
float cos_edge = cos(L.coneAngleEtc.x * 0.5);
|
||||
float cos_l = dot(-Ldir, L.dirAndRange.xyz);
|
||||
if (cos_l <= cos_edge) scale = 0.0;
|
||||
}
|
||||
// Per-channel no-blowout cap to the light's OWN colour (un-intensity-scaled):
|
||||
// a single light can't push a channel past its colour. Summed lit clamped in frag.
|
||||
vec3 baseCol = L.colorAndIntensity.xyz;
|
||||
return min(scale * baseCol, baseCol);
|
||||
}
|
||||
|
||||
vec3 accumulateLights(vec3 N, vec3 worldPos, int instanceIndex) {
|
||||
vec3 lit = uCellAmbient.xyz;
|
||||
|
||||
// SUN / directional — from the SceneLighting UBO (global; the audit cleared
|
||||
// the ambient + sun chain as already faithful). Any point/spot entries still
|
||||
// present in the UBO from LightManager.Tick are IGNORED here — point lights
|
||||
// now come per-object from the SSBO below, so there's no double-count.
|
||||
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; // forward points INTO the scene
|
||||
float ndl = max(0.0, dot(N, Ldir));
|
||||
lit += uLights[i].colorAndIntensity.xyz * uLights[i].colorAndIntensity.w * ndl;
|
||||
}
|
||||
|
||||
// POINT / SPOT — THIS object's selected set (minimize_object_lighting): 8 int
|
||||
// slots per instance into the global light buffer, -1 = unused. Camera-
|
||||
// independent, so a wall's torches light it the same regardless of viewer pos.
|
||||
int base = instanceIndex * 8;
|
||||
for (int k = 0; k < 8; ++k) {
|
||||
int gi = instanceLightIdx[base + k];
|
||||
if (gi < 0) continue;
|
||||
lit += pointContribution(N, worldPos, gLights[gi]);
|
||||
}
|
||||
return lit;
|
||||
}
|
||||
|
||||
out vec3 vNormal;
|
||||
out vec2 vTexCoord;
|
||||
out vec3 vWorldPos;
|
||||
out vec3 vLit; // A7: per-vertex Gouraud lighting (ambient + capped lights)
|
||||
out flat uvec2 vTextureHandle;
|
||||
out flat uint vTextureLayer;
|
||||
|
||||
|
|
@ -123,6 +236,7 @@ void main() {
|
|||
|
||||
vWorldPos = worldPos.xyz;
|
||||
vNormal = normalize(mat3(model) * aNormal);
|
||||
vLit = accumulateLights(vNormal, vWorldPos, instanceIndex); // A7: per-vertex Gouraud (per-object lights)
|
||||
vTexCoord = aTexCoord;
|
||||
|
||||
BatchData b = Batches[uDrawIDOffset + gl_DrawIDARB];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue