From 95b89e9f741c2e137c318c65a4ecd6d38414e6a1 Mon Sep 17 00:00:00 2001 From: David Thompson Date: Thu, 13 May 2021 21:03:07 -0400 Subject: graphics: pbr: Partially rewrite fragment shader. --- data/shaders/pbr-frag.glsl | 240 +++++++++++++++++++++++++++------------------ 1 file changed, 147 insertions(+), 93 deletions(-) (limited to 'data/shaders') diff --git a/data/shaders/pbr-frag.glsl b/data/shaders/pbr-frag.glsl index b025bb7..47ed573 100644 --- a/data/shaders/pbr-frag.glsl +++ b/data/shaders/pbr-frag.glsl @@ -1,7 +1,5 @@ // -*- mode: c -*- -// Heavily based upon: https://learnopengl.com/PBR/Lighting - struct Material { vec3 baseColorFactor; bool baseColorTextureEnabled; @@ -49,8 +47,10 @@ in vec4 fragColor0; out vec4 fragColor; #endif +#define MAX_LIGHTS 4 + uniform Material material; -uniform Light lights[4]; +uniform Light lights[MAX_LIGHTS]; uniform vec4 ambientLightColor; uniform bool vertexColored; uniform vec3 cameraPosition; @@ -60,46 +60,47 @@ uniform sampler2D normalTexture; uniform sampler2D occlusionTexture; uniform sampler2D emissiveTexture; -const float PI = 3.14159265359; +const float PI = 3.14159265358979323846; +const float GAMMA = 2.2; -vec3 fresnelSchlick(float cosTheta, vec3 F0) -{ - return F0 + (1.0 - F0) * pow(max(1.0 - cosTheta, 0.0), 5.0); +#ifndef GLSL330 +// Compatibility shim for older GLSL versions. +vec2 texture(sampler2D tex, vec2 coord) { + return texture2D(tex, coord); +} +#endif + +float posDot(vec3 v1, vec3 v2) { + return max(dot(v1, v2), 0.0); } -float DistributionGGX(vec3 N, vec3 H, float roughness) +vec3 fresnelSchlick(float cosTheta, vec3 baseColor) { - float a = roughness*roughness; - float a2 = a*a; - float NdotH = max(dot(N, H), 0.0); - float NdotH2 = NdotH*NdotH; + return baseColor + (1.0 - baseColor) * pow(max(1.0 - cosTheta, 0.0), 5.0); +} - float num = a2; - float denom = (NdotH2 * (a2 - 1.0) + 1.0); - denom = PI * denom * denom; +float distributionGGX(vec3 normal, vec3 halfAngle, float roughness) +{ + float a = roughness * roughness * roughness * roughness; + float ndoth = posDot(normal, halfAngle); + float denominator = ndoth * ndoth * (a - 1.0) + 1.0; - return num / denom; + return a / (PI * denominator * denominator); } -float GeometrySchlickGGX(float NdotV, float roughness) +float geometrySchlickGGX(float ndotv, float roughness) { - float r = (roughness + 1.0); - float k = (r*r) / 8.0; + float r = roughness + 1.0; + float k = (r * r) / 8.0; - float num = NdotV; - float denom = NdotV * (1.0 - k) + k; - - return num / denom; + return ndotv / (ndotv * (1.0 - k) + k); } -float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) +float geometrySmith(vec3 normal, vec3 viewDirection, vec3 lightDirection, + float roughness) { - float NdotV = max(dot(N, V), 0.0); - float NdotL = max(dot(N, L), 0.0); - float ggx2 = GeometrySchlickGGX(NdotV, roughness); - float ggx1 = GeometrySchlickGGX(NdotL, roughness); - - return ggx1 * ggx2; + return geometrySchlickGGX(posDot(normal, viewDirection), roughness) * + geometrySchlickGGX(posDot(normal, lightDirection), roughness); } vec4 applyAlpha(vec4 color) { @@ -130,9 +131,9 @@ vec2 texcoord(int i) { } vec4 sRGBtoLinear(vec4 srgb) { - return vec4(pow(srgb.r, 2.2), - pow(srgb.g, 2.2), - pow(srgb.b, 2.2), + return vec4(pow(srgb.r, GAMMA), + pow(srgb.g, GAMMA), + pow(srgb.b, GAMMA), srgb.a); } @@ -210,19 +211,71 @@ vec3 materialNormal() { return normalize(normal); } +vec3 lightDirection(Light light) { + if(light.type == 0 || light.type == 2) { // point and spot lights + return normalize(light.position - fragWorldPos); + } else if(light.type == 1) { // directional light + return normalize(-light.direction); + } + + return vec3(0.0); // should never be reached. +} + +vec3 lightAttenuate(Light light) { + float distance = length(light.position - fragWorldPos); + float attenuation = 1.0 / (distance * distance); + return light.color.rgb * attenuation; +} + +vec3 lightRadiance(Light light, vec3 direction) { + if(light.type == 0) { // point light + return lightAttenuate(light); + } else if(light.type == 1) { // directional light + return light.color.rgb; + } else if(light.type == 2) { // spot light + float theta = dot(direction, normalize(-light.direction)); + // Spot lights only shine light in a specific conical area. + // They have no effect outside of that area. + if(theta > light.cutOff) { + return lightAttenuate(light); + } else { + return vec3(0.0); + } + } + + return vec3(0.0); // should never be reached. +} + void main(void) { + // The unit vector pointing from the fragment position to the viewer + // position. + vec3 viewDirection = normalize(cameraPosition - fragWorldPos); float metallic = materialMetallic(); float roughness = materialRoughness(); - vec3 N = materialNormal(); - vec3 V = normalize(cameraPosition - fragWorldPos); + vec3 normal = materialNormal(); + // The "raw" albedo has an alpha channel which we need to preserve + // so that we can apply the desired alpha blending method at the + // end, but it is completely ignored for lighting calculations. vec4 rawAlbedo = materialAlbedo(); vec3 albedo = rawAlbedo.rgb; + // The ambient occlusion factor affects the degree to which ambient + // lighting has an affect on the fragment. Each element of the + // vector is in the range [0, 1]. vec3 ao = materialOcclusion(); - vec3 F0 = mix(vec3(0.4), albedo, metallic); - - // reflectance equation - vec3 Lo = vec3(0.0); - for(int i = 0; i < 4; ++i) + // The color that the fragment relects at zero incidence. In other + // words, the color reflected when looking directly at the fragment. + // A material that is fully metallic will fully express its albedo + // color. A material that isn't metallic at all will have a gray + // initial color. The grayscale value of 0.04 apparently achieves a + // good result so that's what we do. Any other metallic value is + // somewhere between both colors, as calculated by a linear + // interpolation. + vec3 baseColor = mix(vec3(0.04), albedo, metallic); + // Stores the sum of all lighting operations. + vec3 color = vec3(0.0); + + // Apply direct lighting. + for(int i = 0; i < MAX_LIGHTS; ++i) { Light light = lights[i]; @@ -230,62 +283,63 @@ void main(void) { continue; } - vec3 L; - vec3 radiance; - - // calculate per-light radiance - if(light.type == 0) { // point light - L = normalize(light.position - fragWorldPos); - float distance = length(light.position - fragWorldPos); - float attenuation = 1.0 / (distance * distance); - radiance = light.color.rgb * attenuation; - } else if(light.type == 1) { // directional light - L = normalize(-light.direction); - radiance = light.color.rgb; - } else if(light.type == 2) { // spotlight - L = normalize(light.position - fragWorldPos); - float theta = dot(L, normalize(-light.direction)); - - if(theta > light.cutOff) { - float distance = length(light.position - fragWorldPos); - float attenuation = 1.0 / (distance * distance); - radiance = light.color.rgb * attenuation; - } else { - continue; - } - } - - vec3 H = normalize(V + L); - - // cook-torrance brdf - float NDF = DistributionGGX(N, H, roughness); - float G = GeometrySmith(N, V, L, roughness); - vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0); - - vec3 kS = F; - vec3 kD = vec3(1.0) - kS; - kD *= 1.0 - metallic; - - vec3 numerator = NDF * G * F; - float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0); - vec3 specular = numerator / max(denominator, 0.001); - - // add to outgoing radiance Lo - float NdotL = max(dot(N, L), 0.0); - Lo += (kD * albedo / PI + specular) * radiance * NdotL; + // The unit vector pointing from the fragment position to the + // light position. + vec3 lightDirection = lightDirection(light); + vec3 radiance = lightRadiance(light, lightDirection); + // The half angle vector sits between the view direction and the + // light direction. + vec3 halfwayVector = normalize(viewDirection + lightDirection); + // Apply the normal distribution function. + float normalDistribution = distributionGGX(normal, halfwayVector, roughness); + // Apply the geometry function. + float geo = geometrySmith(normal, viewDirection, lightDirection, roughness); + // Compute the fresnel factor via Schlick's approximation to get + // the specular relection coeffecient. Since this is a microfacet + // lighting model, we need to use the dot product of the halfway + // vector and the view direction to get the cos(theta) value, + // according to: + // https://en.wikipedia.org/wiki/Schlick%27s_approximation + vec3 fresnelFactor = fresnelSchlick(posDot(halfwayVector, viewDirection), baseColor); + vec3 refractionRatio = (vec3(1.0) - fresnelFactor) * (1.0 - metallic); + vec3 specularNumerator = normalDistribution * geo * fresnelFactor; + // The dot product of the surface normal and the light direction + // gives a value in the range [0, 1]. 0 means the normal and light + // direction form a >= 90 degree angle and thus the light should + // have no effect. 1 means the light is directly hitting the + // surface and the lighting should be applied with full intensity. + float specularFactor = posDot(normal, lightDirection); + float specularDenominator = 4.0 * posDot(normal, viewDirection) * specularFactor; + vec3 specular = specularNumerator / max(specularDenominator, 0.001); + + color += (refractionRatio * albedo / PI + specular) * radiance * specularFactor; } - // Add emissive color. - Lo += materialEmissive().rgb; - - // Apply ambient lighting. - vec3 ambient = ambientLightColor.xyz * albedo * ao; - vec3 color = ambient + Lo; - - // Apply HDR. + // The emissive texture says which fragments emit light. There's + // probably something more complicated to do here, but for now we + // just add it to the accumulator to get a decent result. + color += materialEmissive().rgb; + // Apply simple ambient lighting. The affect of the ambient light + // is dampened by the ambient occlusion factor. + // + // TODO: Use image based lighting. + color += ambientLightColor.rgb * albedo * ao; + // Apply Reinhard tone mapping to convert our high dynamic range + // color value to low dynamic range. All of the lighting + // calculations stacked on top of each other is likely to create + // color channel values that exceed the [0, 1] range of OpenGL's + // linear color space, i.e. high dynamic range. If we did nothing, + // the resulting color values would be clamped to that range and the + // final image would be mostly white and washed out. color = color / (color + vec3(1.0)); - color = pow(color, vec3(1.0 / 2.2)); - + // Apply gamma correction. The power of 2.2 is the rough average + // gamma value of most monitors. So, scaling the color by the + // inverse, the power of 1/2.2, the color that people see on the + // monitor is closer to what we intend to present with our original + // linear color value. + color = pow(color, vec3(1.0 / GAMMA)); + // Add alpha channel back in and apply the appropriate blend mode. + // Yay, we're done! vec4 finalColor = applyAlpha(vec4(color, rawAlbedo.a)); #ifdef GLSL330 -- cgit v1.2.3