#version 300 es precision mediump float; in highp vec3 vNormal; in highp vec3 vWorldPos; in vec4 vColor; in float vEmissive; in float vReceiveShadow; in float vFresnel; #include "_graphics_uniforms.glsl" #include "_fog.glsl" uniform highp sampler2DArrayShadow u_shadowMap; out vec4 fragColor; vec3 srgbToLinear(vec3 c) { return pow(c, vec3(2.2)); } highp vec3 cascadeCoord(int c, highp vec3 worldPos, vec3 n) { // the normal offset is authored in texels — acne scales with texel size, so the push must too highp vec3 samplePos = worldPos + n * (u_shadowParams.z * u_shadowCascadeParams[c].x); highp vec4 lp = u_lightVP[c] * vec4(samplePos, 1.0); return lp.xyz * 0.5 + 0.5; // no perspective divide — the light VP is orthographic (w == 1) } // Poisson disk PCF: an irregular tap pattern turns the shadow-map staircase into unstructured // noise the eye forgives, and a per-pixel rotation of the disk keeps the pattern itself from // showing. PCF_RADIUS (in shadow texels) is the softness knob. const float PCF_RADIUS = 2.5; const int PCF_TAPS = 12; const vec2 PCF_POISSON[12] = vec2[]( vec2(-0.326212, -0.405805), vec2(-0.840144, -0.073580), vec2(-0.695914, 0.457137), vec2(-0.203345, 0.620716), vec2( 0.962340, -0.194983), vec2( 0.473434, -0.480026), vec2( 0.519456, 0.767022), vec2( 0.185461, -0.893124), vec2( 0.507431, 0.064425), vec2( 0.896420, 0.412458), vec2(-0.321940, -0.932615), vec2(-0.791559, -0.597705) ); float cascadePcf(int c, highp vec3 sc) { highp float bias01 = u_shadowCascadeParams[c].y; highp vec2 texel = u_shadowCascadeParams[c].zw; // interleaved gradient noise -> rotation angle; highp: the hash feeds on pixel coords up to // thousands, and a mediump fract() up there quantizes to garbage highp float angle = 6.2831853 * fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715)))); highp vec2 rot = vec2(cos(angle), sin(angle)); float sum = 0.0; for (int i = 0; i < PCF_TAPS; ++i) { vec2 p = PCF_POISSON[i] * PCF_RADIUS; highp vec2 o = vec2((p.x * rot.x) - (p.y * rot.y), (p.x * rot.y) + (p.y * rot.x)) * texel; sum += texture(u_shadowMap, vec4(sc.xy + o, float(c), sc.z - bias01)); } return sum / float(PCF_TAPS); } void main() { // vertex colors are authored sRGB; the light/ambient uniforms arrive pre-linearized (converted on upload) vec3 albedo = srgbToLinear(vColor.rgb); vec3 lightColor = u_lightColor.rgb; vec3 skyColor = u_ambientSky.rgb; vec3 groundColor = u_ambientGround.rgb; vec3 n = normalize(vNormal); vec3 v = normalize(u_cameraPos - vWorldPos); vec3 h = normalize(u_lightDir + v); float ndl = max(dot(n, u_lightDir), 0.0); float hemi = n.y * 0.5 + 0.5; vec3 ambient = mix(groundColor * u_ambientGround.a, skyColor * u_ambientSky.a, hemi); // shadow: cascades run near to far — take the first (densest) volume containing the fragment; // outside all of them = past the shadow distance, fully lit float shadow = 1.0; const float MARGIN = 0.01; // containment inset — the seam cross-fade must reach 0 here, not at the true edge for (int c = 0; c < 3; ++c) { highp vec3 sc = cascadeCoord(c, vWorldPos, n); if (any(lessThan(sc.xy, vec2(MARGIN))) || any(greaterThan(sc.xy, vec2(1.0 - MARGIN))) || sc.z > 1.0) continue; shadow = cascadePcf(c, sc); // seam blending: near this cascade's volume edge, cross-fade into the next cascade — // PCF softness is texel-proportional, so a hard switch shows as a visible line. // past the LAST cascade the fade target is 1.0, which doubles as the shadow-distance fade-out float edge = min(min(sc.x, 1.0 - sc.x), min(sc.y, 1.0 - sc.y)); const float BLEND = 0.1; if (edge < BLEND) { float next = 1.0; if (c < 2) next = cascadePcf(c + 1, cascadeCoord(c + 1, vWorldPos, n)); shadow = mix(next, shadow, (edge - MARGIN) / (BLEND - MARGIN)); } break; } shadow = mix(1.0, shadow, u_shadowParams.y * vReceiveShadow); // strength 0 = shadows off; receiveShadow 0 = this instance ignores them ambient *= mix(1.0, shadow, u_shadowParams.w); // darkness: shadow also cuts the ambient vec3 lit = ambient + lightColor * (u_lightColor.a * ndl * shadow); // Blinn-Phong — highlight only on the lit side, tinted by the light color float spec = (ndl > 0.0) ? pow(max(dot(n, h), 0.0), u_specParams.x) * u_specParams.y * shadow : 0.0; // emissive: self-illumination in the primitive's own color — bloom catches it past the threshold. // output stays LINEAR — the HDR scene target stores it as-is, brightness past 1.0 survives vec3 outColor = albedo * (lit + vEmissive) + lightColor * spec; // fresnel rim: limb glow in the primitive's own hue — atmosphere on planets, halo on additive shells if (vFresnel > 0.0) { float rim = pow(1.0 - max(dot(n, v), 0.0), 2.5); outColor += albedo * (rim * vFresnel); } outColor = mix(outColor, u_fogColor.rgb, fogFactor(vWorldPos)); fragColor = vec4(outColor, vColor.a); }