Port the retail high/low material-lighting cadence for successful world clicks, keep it instance-scoped in the modern renderer, and restore authored lighting after 0.8 seconds. Correct the selection oracle and pin timing plus per-frame buffer lifecycle with tests. Co-authored-by: Codex <codex@openai.com>
114 lines
5.1 KiB
GLSL
114 lines
5.1 KiB
GLSL
#version 430 core
|
||
#extension GL_ARB_bindless_texture : require
|
||
|
||
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;
|
||
in flat float vOpacityMultiplier; // #188
|
||
in flat vec2 vSelectionLighting; // x=luminosity, y=diffuse
|
||
|
||
// uRenderPass values (Phase N.5 Decision 2 — two-pass alpha-test):
|
||
// 0 = opaque pass — discard fragments with alpha < 0.95
|
||
// (lets the depth write succeed for solid pixels)
|
||
// 1 = translucent pass — covers AlphaBlend / Additive / InvAlpha;
|
||
// discard alpha >= 0.95 (already drawn opaque) and
|
||
// alpha < 0.05 (skip empty fragments — large
|
||
// transparent overdraw cost otherwise)
|
||
uniform int uRenderPass;
|
||
uniform int uLightDebug; // #176 stripe hunt (see mesh_modern.vert) — mode 3 handled here
|
||
|
||
// SceneLighting UBO — IDENTICAL layout to mesh_instanced.frag binding=1.
|
||
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;
|
||
};
|
||
|
||
// 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);
|
||
if (mode == 0) return lit;
|
||
float d = length(worldPos - uCameraAndTime.xyz);
|
||
float fogStart = uFogParams.x;
|
||
float fogEnd = uFogParams.y;
|
||
float span = max(1e-3, fogEnd - fogStart);
|
||
float fog = clamp((d - fogStart) / span, 0.0, 1.0);
|
||
return mix(lit, uFogColor.xyz, fog);
|
||
}
|
||
|
||
out vec4 FragColor;
|
||
|
||
void main() {
|
||
sampler2DArray tex = sampler2DArray(vTextureHandle);
|
||
vec4 color = texture(tex, vec3(vTexCoord, float(vTextureLayer)));
|
||
|
||
// Two-pass alpha-test (N.5 Decision 2).
|
||
// A.5 T20: opaque pass writes alpha as-sampled so GL_SAMPLE_ALPHA_TO_COVERAGE
|
||
// derives the MSAA sample mask from it — ClipMap foliage edges become smooth.
|
||
// Discard only fully-transparent (α < 0.05); the GPU handles coverage masking.
|
||
if (uRenderPass == 0) {
|
||
if (color.a < 0.05) discard; // opaque pass — kill truly empty only (A2C)
|
||
} else {
|
||
// Transparent pass.
|
||
//
|
||
// Phase Post-A.5 (ISSUE #52, 2026-05-10): do NOT discard α≥0.95 here.
|
||
// Native AC transparent-flagged surfaces routinely include
|
||
// effectively-opaque pixels — e.g. the Holtburg lifestone crystal core
|
||
// (surface 0x080011DE) which the spawn manifest classifies as
|
||
// transparent (batch.IsTransparent=True) but whose decoded texture
|
||
// alpha lands ≥0.95 across the visible surface. Those pixels still
|
||
// compose correctly under (SrcAlpha, 1-SrcAlpha) alpha-blending, so
|
||
// discarding them here threw away the whole crystal. The original
|
||
// N.5 §2 rationale (high-α fragments belong in the opaque pass) does
|
||
// not apply when the SURFACE is dat-flagged transparent — those
|
||
// pixels can't reach the opaque pass at all.
|
||
//
|
||
// Keep the α<0.05 short-circuit as a fragment-cost optimization
|
||
// (skip fully-empty pixels — saves blend bandwidth on alpha-keyed
|
||
// sprites with large transparent margins).
|
||
if (color.a < 0.05) discard;
|
||
}
|
||
|
||
// Per-vertex Gouraud lighting from the vertex shader (ambient + capped
|
||
// lights), passed through retail CMaterial's two click-pulse replacements:
|
||
// emissive/luminosity + diffuse * scene lighting. Normal is (0,1).
|
||
vec3 lit = vec3(vSelectionLighting.x) + vSelectionLighting.y * vLit;
|
||
|
||
// #176 stripe-hunt mode 3: show the raw per-vertex light field (texture
|
||
// ignored). Stripes visible HERE = a vertex-lighting artifact; absent =
|
||
// the pattern comes from texture/per-pixel machinery. Throwaway diagnostic.
|
||
if (uLightDebug == 3) {
|
||
FragColor = vec4(min(lit, vec3(1.0)), 1.0);
|
||
return;
|
||
}
|
||
|
||
// Lightning flash — additive scene bump (matches mesh_instanced.frag).
|
||
lit += uFogParams.z * vec3(0.6, 0.6, 0.75);
|
||
|
||
// Retail clamp per-channel to 1.0 (r13 §13.1).
|
||
lit = min(lit, vec3(1.0));
|
||
|
||
vec3 rgb = color.rgb * lit;
|
||
rgb = applyFog(rgb, vWorldPos);
|
||
// #188: multiply the FINAL alpha only — the discard thresholds above stay
|
||
// keyed on the raw sampled color.a, so the last few frames of a fade
|
||
// (multiplier crossing under 0.05) still ramp smoothly toward zero rather
|
||
// than popping invisible early against the discard cutoff.
|
||
FragColor = vec4(rgb, color.a * vOpacityMultiplier);
|
||
}
|