Gooch Warm-to-Cool Shading in GLSL
This article provides a concise guide to implementing the Gooch non-photorealistic shading model in OpenGL Shading Language (GLSL). Originally developed for technical illustration, the Gooch model replaces standard intensity-based diffuse shading with a shift between warm and cool color tones to preserve shape detail across varying lighting conditions. Below, you will find the mathematical breakdown of the model followed by a complete, optimized GLSL fragment shader implementation.
Mathematical Foundation
The standard Lambertian diffuse model scales surface color directly by the dot product of the surface normal (\(\mathbf{N}\)) and light direction (\(\mathbf{L}\)): \(\mathbf{N} \cdot \mathbf{L}\). This leaves shadowed regions completely dark and obscures surface details.
The Gooch model modifies this by mapping \(\mathbf{N} \cdot \mathbf{L} \in [-1, 1]\) into a normalized range \(t \in [0, 1]\):
\[t = \frac{1.0 + \mathbf{N} \cdot \mathbf{L}}{2.0}\]
Instead of fading to black, the surface blends between a defined "cool" color and a "warm" color based on \(t\):
- Cool Tone (\(k_{\text{cool}}\)): \(k_{\text{blue}} + \alpha \cdot k_{\text{surface}}\)
- Warm Tone (\(k_{\text{warm}}\)): \(k_{\text{yellow}} + \beta \cdot k_{\text{surface}}\)
Here, \(k_{\text{surface}}\) is the object's base diffuse color, \(k_{\text{blue}}\) and \(k_{\text{yellow}}\) are user-defined cool and warm constants, and \(\alpha\) and \(\beta\) determine the strength of the surface color contribution.
The final diffuse color \(I_{\text{diffuse}}\) is interpolated linearly:
\[I_{\text{diffuse}} = (1.0 - t) \cdot k_{\text{cool}} + t \cdot k_{\text{warm}}\]
A standard Blinn-Phong specular term is typically added on top to indicate glossy highlights.
GLSL Fragment Shader Implementation
Below is a complete GLSL fragment shader implementing the Gooch shading model.
#version 330 core
in vec3 vNormal;
in vec3 vFragPos;
out vec4 FragColor;
// Lighting and Camera Uniforms
uniform vec3 uLightPos;
uniform vec3 uViewPos;
// Surface Properties
uniform vec3 uSurfaceColor; // Object's base color (kd)
// Gooch Parameters
const vec3 CoolBase = vec3(0.0, 0.0, 0.55); // Base cool color (typically cyan/blue)
const vec3 WarmBase = vec3(0.3, 0.3, 0.0); // Base warm color (typically yellow/orange)
const float Alpha = 0.25; // Base color contribution to cool tone
const float Beta = 0.5; // Base color contribution to warm tone
void main()
{
// Normalize interpolated vectors
vec3 N = normalize(vNormal);
vec3 L = normalize(uLightPos - vFragPos);
vec3 V = normalize(uViewPos - vFragPos);
// 1. Remap N dot L from [-1, 1] to [0, 1]
float NdotL = dot(N, L);
float t = (NdotL + 1.0) * 0.5;
// 2. Compute warm and cool tone colors
vec3 kCool = CoolBase + Alpha * uSurfaceColor;
vec3 kWarm = WarmBase + Beta * uSurfaceColor;
// 3. Interpolate diffuse color based on t
vec3 goochDiffuse = mix(kCool, kWarm, t);
// 4. Compute Specular Highlight (Blinn-Phong)
vec3 H = normalize(L + V);
float NdotH = max(dot(N, H), 0.0);
float specularStrength = pow(NdotH, 32.0);
vec3 specularColor = vec3(1.0) * specularStrength;
// 5. Combine Diffuse and Specular
vec3 finalColor = goochDiffuse + specularColor;
FragColor = vec4(finalColor, 1.0);
}Key Implementation Details
- Vector Normalization: Both
vNormaland the calculated light and view directions must be normalized in the fragment shader to avoid distortion artifacts caused by linear interpolation across polygons. - The
mix()Function: The GLSLmix(x, y, a)function evaluates \(x \cdot (1 - a) + y \cdot a\), which directly handles the interpolation betweenkCoolandkWarm. - Parameter Tuning: The default values for
Alpha(\(0.25\)) andBeta(\(0.5\)) prioritize warmth on lit sides while maintaining visibility in shadowed regions. AdjustingCoolBaseandWarmBaseallows for stylistic customization depending on the art direction.