// -*- mode: c -*- struct Material { vec3 baseColorFactor; bool baseColorTextureEnabled; int baseColorTexcoord; float metallicFactor; float roughnessFactor; bool metallicRoughnessTextureEnabled; int metallicRoughnessTexcoord; vec3 normalFactor; bool normalTextureEnabled; int normalTexcoord; bool occlusionTextureEnabled; int occlusionTexcoord; vec3 emissiveFactor; bool emissiveTextureEnabled; int emissiveTexcoord; int alphaMode; float alphaCutoff; }; struct Light { bool enabled; int type; vec3 position; vec3 direction; vec4 color; float cutOff; }; #ifdef GLSL120 attribute vec3 fragWorldPos; attribute vec3 fragNormal; attribute vec2 fragTexcoord0; attribute vec2 fragTexcoord1 attribute vec4 fragColor0; #else in vec3 fragWorldPos; in vec3 fragNormal; in vec2 fragTexcoord0; in vec2 fragTexcoord1; in vec4 fragColor0; #endif #ifdef GLSL330 out vec4 fragColor; #endif #define MAX_LIGHTS 4 uniform Material material; uniform Light lights[MAX_LIGHTS]; uniform vec4 ambientLightColor; uniform bool vertexColored; uniform vec3 cameraPosition; uniform sampler2D baseColorTexture; uniform sampler2D metallicRoughnessTexture; uniform sampler2D normalTexture; uniform sampler2D occlusionTexture; uniform sampler2D emissiveTexture; const float PI = 3.14159265358979323846; const float GAMMA = 2.2; #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); } vec3 fresnelSchlick(float cosTheta, vec3 baseColor) { return baseColor + (1.0 - baseColor) * pow(max(1.0 - cosTheta, 0.0), 5.0); } 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 a / (PI * denominator * denominator); } float geometrySchlickGGX(float ndotv, float roughness) { float r = roughness + 1.0; float k = (r * r) / 8.0; return ndotv / (ndotv * (1.0 - k) + k); } float geometrySmith(vec3 normal, vec3 viewDirection, vec3 lightDirection, float roughness) { return geometrySchlickGGX(posDot(normal, viewDirection), roughness) * geometrySchlickGGX(posDot(normal, lightDirection), roughness); } vec4 applyAlpha(vec4 color) { // Apply alpha mode. if(material.alphaMode == 0) { // opaque return vec4(color.rgb, 1.0); } else if(material.alphaMode == 1) { // mask if(color.a >= material.alphaCutoff) { return vec4(color.rgb, 1.0); } else { discard; } } else if(material.alphaMode == 2) { // blend if(color.a <= 0.005) { discard; } else { return color; } } } vec2 texcoord(int i) { if(i == 0) { return fragTexcoord0; } else { return fragTexcoord1; } } vec4 sRGBtoLinear(vec4 srgb) { return vec4(pow(srgb.r, GAMMA), pow(srgb.g, GAMMA), pow(srgb.b, GAMMA), srgb.a); } float materialMetallic() { float m = material.metallicFactor; if(material.metallicRoughnessTextureEnabled) { m *= texture(metallicRoughnessTexture, texcoord(material.metallicRoughnessTexcoord)).b; } return m; } float materialRoughness() { float r = material.roughnessFactor; if(material.metallicRoughnessTextureEnabled) { r *= texture(metallicRoughnessTexture, texcoord(material.metallicRoughnessTexcoord)).g; } return r; } vec4 materialAlbedo() { vec4 color = vec4(0.0, 0.0, 1.0, 1.0); if(material.baseColorTextureEnabled) { vec4 texColor = texture(baseColorTexture, texcoord(material.baseColorTexcoord)); color = sRGBtoLinear(texColor); } color *= vec4(material.baseColorFactor, 1.0); if(vertexColored) { color *= fragColor0; } return color; } vec4 materialEmissive() { vec4 color = vec4(0.0); if(material.emissiveTextureEnabled) { vec4 texColor = texture(emissiveTexture, texcoord(material.emissiveTexcoord)); color = sRGBtoLinear(texColor); } return color * vec4(material.emissiveFactor, 1.0); } vec3 materialOcclusion() { if(material.occlusionTextureEnabled) { return vec3(texture(occlusionTexture, texcoord(material.occlusionTexcoord)).r); } else { return vec3(1.0); } } vec3 materialNormal() { vec3 normal; if(material.normalTextureEnabled) { normal = texture(normalTexture, texcoord(material.normalTexcoord)).rgb * 2.0 - 1.0; } else { normal = fragNormal; } 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 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(); // 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]; if(!light.enabled) { continue; } // 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; } // 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)); // 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 fragColor = finalColor; #else gl_FragColor = finalColor; #endif }