#version 430 core in vec2 vTex; in vec3 vWorldNormal; in vec3 vWorldPos; out vec4 fragColor; uniform sampler2D uDiffuse; // Phase 9.1: translucency kind — matches TranslucencyKind C# enum. // 0 = Opaque — depth write+test, no blend; shader never discards // 1 = ClipMap — alpha-key discard (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 (binding = 1). // // Layout mirrors SceneLightingUbo in C#: // struct Light { // vec4 posAndKind; xyz = world pos, w = kind (0=dir,1=point,2=spot) // vec4 dirAndRange; xyz = forward, w = range (metres, hard cutoff) // vec4 colorAndIntensity; xyz = RGB linear, w = intensity // vec4 coneAngleEtc; x = cone (rad), yzw = reserved // }; // layout(std140, binding = 1) uniform SceneLighting { // Light uLights[8]; // vec4 uCellAmbient; xyz = ambient RGB, w = active count // vec4 uFogParams; x = start, y = end, z = flash, w = mode // vec4 uFogColor; xyz = color // vec4 uCameraAndTime; xyz = camera pos, w = day fraction // }; // ───────────────────────────────────────────────────────────── 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; }; // Retail hard-cutoff lighting equation (r13 §10.2). No distance // attenuation inside Range; hard edge at Range; spotlights use a // binary cos-cone test. This is deliberate — the retail "bubble of // light" look relies on crisp boundaries. 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) { // Directional: "forward" is the light's direction vector // pointing INTO the scene. N·(-forward) = light-facing. vec3 Ldir = -uLights[i].dirAndRange.xyz; float ndl = max(0.0, dot(N, Ldir)); lit += Lcol * ndl; } else { // Point / spot: falloff is a HARD bubble at Range. 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; // retail: no attenuation inside Range if (kind == 2) { // Spotlight: hard-edged cos-cone test. 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; } // Linear fog (r12 §5.1): mode 1 = LINEAR, 0 = off, others reserved. 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 sampled = texture(uDiffuse, vTex); // Alpha cutout only for clip-map surfaces (doors, windows, vegetation). if (uTranslucencyKind == 1 && sampled.a < 0.5) discard; vec3 N = normalize(vWorldNormal); vec3 lit = accumulateLights(N, vWorldPos); // Lightning flash (r12 §9) — additive cold-white pulse layered on top // of diffuse lighting. float flash = uFogParams.z; lit += flash * vec3(0.6, 0.6, 0.75); // Clamp per-channel to 1.0 — matches retail (r13 §13.1). lit = min(lit, vec3(1.0)); vec3 rgb = sampled.rgb * lit; // Atmospheric fog — applied after lighting. rgb = applyFog(rgb, vWorldPos); fragColor = vec4(rgb, sampled.a); }