Code quality review caught four issues: - Unnecessary GL_ARB_bindless_texture extension in mesh_modern.vert (vert doesn't use bindless types). Removed; only the frag needs it. - SSBO binding=1 (BatchBuffer) and UBO binding=1 (SceneLighting) are in distinct GL namespaces — added a comment in the vert documenting this so Task 10's bind site doesn't get confused. - Misleading "0=opaque, 1=transparent" comment expanded to spell out the full Decision 2 two-pass alpha-test logic and what each discard threshold protects against. - BatchData.flags field is reserved; documented that N.5's dispatcher owns all blend state, with a hook for future shader-side additive. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
102 lines
3.4 KiB
GLSL
102 lines
3.4 KiB
GLSL
#version 430 core
|
|
#extension GL_ARB_bindless_texture : require
|
|
|
|
in vec3 vNormal;
|
|
in vec2 vTexCoord;
|
|
in vec3 vWorldPos;
|
|
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;
|
|
};
|
|
|
|
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));
|
|
float atten = 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;
|
|
}
|
|
|
|
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).
|
|
if (uRenderPass == 0) {
|
|
if (color.a < 0.95) discard; // opaque pass
|
|
} else {
|
|
if (color.a >= 0.95) discard; // transparent pass
|
|
if (color.a < 0.05) discard; // skip totally-empty
|
|
}
|
|
|
|
vec3 N = normalize(vNormal);
|
|
vec3 lit = accumulateLights(N, vWorldPos);
|
|
|
|
// 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);
|
|
}
|