summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Thompson <dthompson2@worcester.edu>2021-05-14 21:24:00 -0400
committerDavid Thompson <dthompson2@worcester.edu>2021-05-14 21:31:53 -0400
commit193f9be3e18cd824316d4f44b8199cb3f86c4a65 (patch)
treecaf151889105be458a8b53cb1bd2032503444fa0
parent95b89e9f741c2e137c318c65a4ecd6d38414e6a1 (diff)
graphics: pbr: Improve fragment shader.
Properly process normal maps, among other small changes.
-rw-r--r--data/shaders/pbr-frag.glsl169
1 files 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));