summaryrefslogtreecommitdiff
path: root/data/shaders/pbr-frag.glsl
diff options
context:
space:
mode:
authorDavid Thompson <dthompson2@worcester.edu>2021-05-13 21:03:07 -0400
committerDavid Thompson <dthompson2@worcester.edu>2021-05-13 21:03:07 -0400
commit95b89e9f741c2e137c318c65a4ecd6d38414e6a1 (patch)
treed73821365420b87ac25149ddf5fec762fc7b4121 /data/shaders/pbr-frag.glsl
parentc98cce478e902e8569dd59dc1d5748ec85cc6a69 (diff)
graphics: pbr: Partially rewrite fragment shader.
Diffstat (limited to 'data/shaders/pbr-frag.glsl')
-rw-r--r--data/shaders/pbr-frag.glsl240
1 files changed, 147 insertions, 93 deletions
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