uniform sampler2D Texture; uniform sampler2D uShadowMap; uniform vec3 uLightDir; varying vec2 texCoord; varying vec4 fragPosLightSpace; varying vec3 fragNormal; float computeShadow(vec4 lightSpacePos, vec3 normal) { vec3 projCoords = lightSpacePos.xyz / lightSpacePos.w; projCoords = projCoords * 0.5 + 0.5; if (projCoords.x < 0.0 || projCoords.x > 1.0 || projCoords.y < 0.0 || projCoords.y > 1.0 || projCoords.z > 1.0) { return 0.0; } float currentDepth = projCoords.z; // Slope-dependent bias: large for grazing angles, tiny for surfaces facing the light float bias = 0.0004; if (dot(normal, normal) > 0.001) { float cosTheta = dot(normalize(normal), -normalize(uLightDir)); bias = max(0.002 * (1.0 - cosTheta), 0.0002); } // 3x3 PCF (percentage-closer filtering) for softer shadows float shadow = 0.0; float texelSize = 1.0 / 2048.0; for (int x = -1; x <= 1; x++) { for (int y = -1; y <= 1; y++) { float pcfDepth = texture2D(uShadowMap, projCoords.xy + vec2(float(x), float(y)) * texelSize).r; shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0; } } shadow /= 9.0; return shadow; } void main() { vec4 color = texture2D(Texture, texCoord); float ambient = 0.4; float diffuseStrength = 0.6; // Compute diffuse term; if normals are missing (zero-length) treat as fully lit float diffuse = 1.0; vec3 n = fragNormal; if (dot(n, n) > 0.001) { n = normalize(n); diffuse = max(dot(n, -normalize(uLightDir)), 0.0); } float shadow = computeShadow(fragPosLightSpace, fragNormal); // Shadow dims only the diffuse contribution; ambient is always present float lighting = ambient + diffuseStrength * diffuse * (1.0 - shadow); color.rgb *= lighting; gl_FragColor = color; }