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 vNormal and 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 GLSL mix(x, y, a) function evaluates \(x \cdot (1 - a) + y \cdot a\), which directly handles the interpolation between kCool and kWarm.
  • Parameter Tuning: The default values for Alpha (\(0.25\)) and Beta (\(0.5\)) prioritize warmth on lit sides while maintaining visibility in shadowed regions. Adjusting CoolBase and WarmBase allows for stylistic customization depending on the art direction.