#version 430 core in vec2 vTex; in vec3 vWorldNormal; in vec3 vWorldPos; out vec4 fragColor; // One 2D texture per draw call — same binding point as mesh.frag so the // C# side can use the same TextureCache without a texture-array pipeline. uniform sampler2D uDiffuse; // Translucency kind — matches TranslucencyKind C# enum (same as mesh.frag): // 0 = Opaque — depth write+test, no blend; shader never discards // 1 = ClipMap — alpha-key discard at 0.5 (doors, windows, vegetation) // 2 = AlphaBlend — GL blending handles compositing; do NOT discard // 3 = Additive — GL additive blending; do NOT discard // 4 = InvAlpha — GL inverted-alpha blending; do NOT discard uniform int uTranslucencyKind; // Phase G.1+G.2: shared scene-lighting UBO (see mesh.frag for layout docs). 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); } void main() { vec4 color = texture(uDiffuse, vTex); // Alpha cutout only for clip-map surfaces (doors, windows, vegetation). if (uTranslucencyKind == 1 && color.a < 0.5) discard; vec3 N = normalize(vWorldNormal); vec3 lit = accumulateLights(N, vWorldPos); // Lightning flash — additive scene bump. 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); }