From 193f9be3e18cd824316d4f44b8199cb3f86c4a65 Mon Sep 17 00:00:00 2001 From: David Thompson Date: Fri, 14 May 2021 21:24:00 -0400 Subject: graphics: pbr: Improve fragment shader. Properly process normal maps, among other small changes. --- data/shaders/pbr-frag.glsl | 169 ++++++++++++++++++++++++++++++--------------- 1 file changed, 115 insertions(+), 54 deletions(-) diff --git a/data/shaders/pbr-frag.glsl b/data/shaders/pbr-frag.glsl index 47ed573..36545d3 100644 --- a/data/shaders/pbr-frag.glsl +++ b/data/shaders/pbr-frag.glsl @@ -123,18 +123,19 @@ vec4 applyAlpha(vec4 color) { } vec2 texcoord(int i) { - if(i == 0) { - return fragTexcoord0; - } else { - return fragTexcoord1; - } + return i == 0 ? fragTexcoord0: fragTexcoord1; } vec4 sRGBtoLinear(vec4 srgb) { - return vec4(pow(srgb.r, GAMMA), - pow(srgb.g, GAMMA), - pow(srgb.b, GAMMA), - srgb.a); + return vec4(pow(srgb.rgb, vec3(GAMMA)), srgb.a); +} + +vec3 gammaCorrect(vec3 color) { + return pow(color, vec3(1.0 / GAMMA)); +} + +vec3 toneMap(vec3 color) { + return color / (color + vec3(1.0)); } float materialMetallic() { @@ -177,16 +178,16 @@ vec4 materialAlbedo() { return color; } -vec4 materialEmissive() { - vec4 color = vec4(0.0); +vec3 materialEmissive() { + vec3 color = vec3(0.0); if(material.emissiveTextureEnabled) { vec4 texColor = texture(emissiveTexture, texcoord(material.emissiveTexcoord)); - color = sRGBtoLinear(texColor); + color = sRGBtoLinear(texColor).rgb; } - return color * vec4(material.emissiveFactor, 1.0); + return color * material.emissiveFactor; } vec3 materialOcclusion() { @@ -199,16 +200,25 @@ vec3 materialOcclusion() { } vec3 materialNormal() { - vec3 normal; - if(material.normalTextureEnabled) { - normal = texture(normalTexture, - texcoord(material.normalTexcoord)).rgb * 2.0 - 1.0; + // See: http://www.thetenthplanet.de/archives/1180 + vec2 t = texcoord(material.normalTexcoord); + vec3 tangentNormal = texture(normalTexture, t).xyz * 2.0 - 1.0; + + vec3 q1 = dFdx(fragWorldPos); + vec3 q2 = dFdy(fragWorldPos); + vec2 st1 = dFdx(fragTexcoord0); + vec2 st2 = dFdy(fragTexcoord0); + + vec3 N = normalize(fragNormal); + vec3 T = normalize(q1 * st2.t - q2 * st1.t); + vec3 B = -normalize(cross(N, T)); + mat3 TBN = mat3(T, B, N); + + return normalize(TBN * tangentNormal); } else { - normal = fragNormal; + return fragNormal; } - - return normalize(normal); } vec3 lightDirection(Light light) { @@ -237,7 +247,9 @@ vec3 lightRadiance(Light light, vec3 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); + // Feather out the light as it approaches the edge of its cone. + float intensity = (theta - light.cutOff) / (1.0 - light.cutOff); + return lightAttenuate(light) * intensity; } else { return vec3(0.0); } @@ -246,6 +258,9 @@ vec3 lightRadiance(Light light, vec3 direction) { return vec3(0.0); // should never be reached. } +// Useful resources I learned a lot from: +// https://learnopengl.com/PBR/Theory +// http://graphicrants.blogspot.com/2013/08/specular-brdf-reference.html void main(void) { // The unit vector pointing from the fragment position to the viewer // position. @@ -261,7 +276,7 @@ void main(void) { // 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 ambientOcclusion = materialOcclusion(); // 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 @@ -271,7 +286,8 @@ void main(void) { // 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. + // We will accumulate the results of all lights (ambient, emissive, + // and user-specified direct lights) into this color vector. vec3 color = vec3(0.0); // Apply direct lighting. @@ -285,45 +301,90 @@ void main(void) { // 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; + vec3 direction = lightDirection(light); // 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 + // gives a value in the range [0, 1] that tells us how directly + // light is hitting the fragment. 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); + // surface and the light should be applied with full intensity. + float incidenceFactor = posDot(normal, direction); + // The intensity of the light. + vec3 radiance = lightRadiance(light, direction); + + // Skip all the expensive math below if the light will have no + // effect on the result. + if(incidenceFactor == 0.0 || radiance == vec3(0.0)) { + continue; + } - color += (refractionRatio * albedo / PI + specular) * radiance * specularFactor; + // The halfway vector is named as such because it is halfway + // between the view direction and the light direction. It is a + // significant vector for microfacet lighting calculations. + vec3 halfwayVector = normalize(viewDirection + direction); + // Apply the Trowbridge-Reitz GGX normal distribution function. + // This function approximates the percentage of microfacets that + // are aligned with the halfway vector. Smooth objects have a lot + // of aligned microfacets over a small area, thus the viewer sees + // a bright spot. Rough objects have few aligned microfacets over + // a larger area, thus it appears dull. + float normalDistribution = distributionGGX(normal, halfwayVector, roughness); + // Apply the geometry distribution function using Smith's method. + // This function approximates how much light is reflected based on + // the fragment's roughness. Rougher objects reflect less light + // because the microfacets of the surface obstruct or overshadow + // the reflected light rays. + float geoFactor = geometrySmith(normal, viewDirection, direction, roughness); + // Compute the fresnel factor via Schlick's approximation to get + // the specular reflection coeffecient, i.e. the percentage of + // light that is reflected. 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 + // + // I also found this article useful: + // http://psgraphics.blogspot.com/2020/03/fresnel-equations-schlick-approximation.html + vec3 reflectionFactor = fresnelSchlick(posDot(halfwayVector, direction), baseColor); + // Refracted light is the leftover light that hasn't been + // reflected. One additional complicating factor is that metallic + // surfaces don't refract light, so we must scale the refraction + // factor based on how metallic the fragment is. + vec3 refractionFactor = (vec3(1.0) - reflectionFactor) * (1.0 - metallic); + // The dot product of the surface normal and the view direction + // gives a value in the range [0, 1] that tells us how directly + // the viewer is looking at the fragment. The effect of specular + // lighting changes based on the angle from the viewer to the + // surface. + float viewFactor = posDot(normal, viewDirection); + // Specular and diffuse lighting are processed together using the + // Cook-Torrance bidirectional reflectance distribution function + // (BRDF.) + // + // http://www.codinglabs.net/article_physically_based_rendering_cook_torrance.aspx + vec3 cookTorranceNumerator = normalDistribution * reflectionFactor * geoFactor; + float cookTorranceDenominator = 4.0 * viewFactor * incidenceFactor; + // We need to be careful to avoid a division by zero error, such + // as when the specular factor is 0, so we clamp the denominator + // to some very small value to safeguard ourselves. + vec3 specular = cookTorranceNumerator / max(cookTorranceDenominator, 0.0001); + // Apply Lambertian reflectance to get the diffuse lighting + // factor. https://en.wikipedia.org/wiki/Lambertian_reflectance + vec3 diffuse = refractionFactor * albedo / PI; + // The final light value is the combination of diffuse and + // specular light scaled by the intensity of the light source and + // the angle of incidence (how directly the light hit the + // fragment.) + color += (diffuse + specular) * radiance * incidenceFactor; } - // 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; + // The emissive texture says which fragments emit light. We simply + // add this light value to the color accumulator. + color += materialEmissive(); // 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; + color += ambientLightColor.rgb * albedo * ambientOcclusion; // 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 @@ -331,13 +392,13 @@ void main(void) { // 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 = toneMap(color); // 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)); + color = gammaCorrect(color); // Add alpha channel back in and apply the appropriate blend mode. // Yay, we're done! vec4 finalColor = applyAlpha(vec4(color, rawAlbedo.a)); -- cgit v1.2.3