#version 330 core out vec4 FragColor; in vec2 TexCoord; struct DirLight { vec3 direction; vec3 color; float intensity; }; struct PointLight { vec3 position; vec3 color; float intensity; float radius; int shadowTile; }; struct SpotLight { vec3 position; vec3 direction; vec3 color; float intensity; float radius; float innerCos; float outerCos; int shadowTile; }; // The array sizes have to match Light.h #define MAX_DIRECTIONAL_LIGHTS 4 #define MAX_POINT_LIGHTS 16 #define MAX_SPOT_LIGHTS 8 #define SHADOW_ATLAS_COLUMNS 6 #define SHADOW_ATLAS_TILES (SHADOW_ATLAS_COLUMNS * SHADOW_ATLAS_COLUMNS) uniform DirLight dirLight; // the sun, the only light with a shadow map uniform DirLight directionalLights[MAX_DIRECTIONAL_LIGHTS]; uniform int directionalLightCount = 0; uniform PointLight pointLights[MAX_POINT_LIGHTS]; uniform int pointLightCount = 0; uniform SpotLight spotLights[MAX_SPOT_LIGHTS]; uniform int spotLightCount = 0; uniform sampler2DShadow localShadowMap; uniform mat4 shadowTileMatrices[SHADOW_ATLAS_TILES]; uniform vec3 viewPos; uniform mat4 lightSpaceMatrix; uniform sampler2D gPosition; uniform sampler2D gNormal; uniform sampler2D gAlbedo; uniform sampler2D gMaterial; uniform sampler2DShadow shadowMap; uniform samplerCube environmentMap; uniform float envIntensity = 0.8; const float PI = 3.14159265359; // Plain Blinn-Phong: ambient, Lambert diffuse and a half vector highlight. // Roughness sets how tight and strong the highlight is, metallic tints it and takes away the diffuse. // Close enough to the 2.2 curve and much cheaper than pow vec3 ToLinear(vec3 c) { return c * c; } // Light from one direction, L points from the surface toward the light. // diffuse and specular are already scaled for this pixel, see main. vec3 ShadeLight(vec3 L, vec3 radiance, vec3 N, vec3 V, vec3 diffuse, vec3 specular, float power) { float NdotL = max(dot(N, L), 0.0); vec3 H = normalize(V + L); float highlight = pow(max(dot(N, H), 0.0), power); return (diffuse + specular * highlight) * radiance * NdotL; } // Inverse square falloff, windowed so it reaches exactly zero at the radius float PointAttenuation(float distanceSquared, float radius) { float ratio = distanceSquared / (radius * radius); float window = clamp(1.0 - ratio * ratio, 0.0, 1.0); return window * window / max(distanceSquared, 0.01); } // Which of a point light's 6 atlas tiles covers offset, the order is set in RenderLocalShadows int PointShadowFace(vec3 offset) { vec3 size = abs(offset); if (size.x >= size.y && size.x >= size.z) return offset.x > 0.0 ? 0 : 1; if (size.y >= size.z) return offset.y > 0.0 ? 2 : 3; return offset.z > 0.0 ? 4 : 5; } // 1 = lit, 0 = shadowed. Stays inside the tile so the filter can't pick up its neighbours. float LocalShadow(int tile, vec3 worldPos, vec3 N) { vec4 clipPos = shadowTileMatrices[tile] * vec4(worldPos + N * 0.04, 1.0); if (clipPos.w <= 0.0) return 1.0; vec3 projCoords = clipPos.xyz / clipPos.w * 0.5 + 0.5; if (any(lessThan(projCoords, vec3(0.0))) || any(greaterThan(projCoords, vec3(1.0)))) return 1.0; float tileSize = 1.0 / float(SHADOW_ATLAS_COLUMNS); vec2 tileOrigin = vec2(float(tile % SHADOW_ATLAS_COLUMNS), float(tile / SHADOW_ATLAS_COLUMNS)) * tileSize; vec2 texelSize = 1.0 / vec2(textureSize(localShadowMap, 0)); vec2 uv = tileOrigin + projCoords.xy * tileSize; float shadow = 0.0; for (int x = -1; x <= 1; x++) { for (int y = -1; y <= 1; y++) { vec2 sampleUV = clamp(uv + vec2(x, y) * texelSize, tileOrigin + texelSize, tileOrigin + tileSize - texelSize); shadow += texture(localShadowMap, vec3(sampleUV, projCoords.z - 0.0002)); } } return shadow / 9.0; } float ShadowCalculation(vec4 fragPosLightSpace, vec3 N, vec3 L) { vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w; projCoords = projCoords * 0.5 + 0.5; if (projCoords.z > 1.0 || any(lessThan(projCoords, vec3(0.0))) || any(greaterThan(projCoords, vec3(1.0)))) return 1.0; float bias = max(0.004 * (1.0 - dot(N, L)), 0.001); vec2 texelSize = 1.0 / vec2(textureSize(shadowMap, 0)); float shadow = 0.0; for (int x = -1; x <= 1; x++) { for (int y = -1; y <= 1; y++) { vec2 offset = vec2(x, y) * texelSize; shadow += texture(shadowMap, vec3(projCoords.xy + offset, projCoords.z - bias)); } } return shadow / 9.0; } void main() { vec3 normal = texture(gNormal, TexCoord).rgb; // Nothing was written here, keep the skybox underneath if (dot(normal, normal) < 0.01) discard; vec3 FragPos = texture(gPosition, TexCoord).rgb; vec4 albedoSample = texture(gAlbedo, TexCoord); vec4 material = texture(gMaterial, TexCoord); vec3 albedo = albedoSample.rgb; float metal = material.r; float rough = material.g; float ao = material.b; vec3 N = normalize(normal); vec3 V = normalize(viewPos - FragPos); float smoothness = 1.0 - clamp(rough, 0.0, 1.0); float power = exp2(10.0 * smoothness + 1.0); // 2 when fully rough, 2048 when mirror smooth vec3 specularColor = mix(vec3(0.5 * smoothness * smoothness), albedo * (0.25 + 0.75 * smoothness), metal); vec3 diffuseBRDF = albedo * (1.0 - metal) / PI; // the light intensities were tuned with the 1/PI in vec3 specularBRDF = specularColor / PI; vec3 direct = vec3(0.0); // Facing away from the sun has no direct light, so skip the BRDF and the 9 shadow samples vec3 sunL = normalize(-dirLight.direction); if (dot(N, sunL) > 0.0) { float shadow = ShadowCalculation(lightSpaceMatrix * vec4(FragPos, 1.0), N, sunL); direct += ShadeLight(sunL, dirLight.color * dirLight.intensity, N, V, diffuseBRDF, specularBRDF, power) * shadow; } for (int i = 0; i < directionalLightCount; i++) { vec3 L = normalize(-directionalLights[i].direction); vec3 radiance = directionalLights[i].color * directionalLights[i].intensity; direct += ShadeLight(L, radiance, N, V, diffuseBRDF, specularBRDF, power); } for (int i = 0; i < pointLightCount; i++) { vec3 toLight = pointLights[i].position - FragPos; float distanceSquared = dot(toLight, toLight); float radius = pointLights[i].radius; if (distanceSquared >= radius * radius) continue; vec3 radiance = pointLights[i].color * pointLights[i].intensity * PointAttenuation(distanceSquared, radius); vec3 L = toLight / sqrt(distanceSquared); if (dot(N, L) <= 0.0) continue; vec3 lit = ShadeLight(L, radiance, N, V, diffuseBRDF, specularBRDF, power); if (pointLights[i].shadowTile >= 0) lit *= LocalShadow(pointLights[i].shadowTile + PointShadowFace(-toLight), FragPos, N); direct += lit; } for (int i = 0; i < spotLightCount; i++) { vec3 toLight = spotLights[i].position - FragPos; float distanceSquared = dot(toLight, toLight); float radius = spotLights[i].radius; if (distanceSquared >= radius * radius) continue; vec3 L = toLight / sqrt(distanceSquared); float cone = smoothstep(spotLights[i].outerCos, spotLights[i].innerCos, dot(-L, spotLights[i].direction)); if (cone <= 0.0) continue; if (dot(N, L) <= 0.0) continue; vec3 radiance = spotLights[i].color * spotLights[i].intensity * PointAttenuation(distanceSquared, radius) * cone; vec3 lit = ShadeLight(L, radiance, N, V, diffuseBRDF, specularBRDF, power); if (spotLights[i].shadowTile >= 0) lit *= LocalShadow(spotLights[i].shadowTile, FragPos, N); direct += lit; } // Ambient is the sky's blurred colour in the direction the surface faces vec3 ambient = ToLinear(textureLod(environmentMap, N, 7.0).rgb) * envIntensity * albedo * ao; vec3 emission = albedo * material.a * 4.0; FragColor = vec4(direct + ambient + emission, albedoSample.a); }