#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; // 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; // 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). vec3 lit = vLit; // 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); FragColor = vec4(rgb, color.a); }