Daniel Gray

Thoughts, Notes, Ideas, Projects

← Back to home

Cell Shading and Non-Photorealistic Rendering

Both terrain and trees use custom shader modifications via Three.js's onBeforeCompile hook. This allows us to modify the shader code at runtime, injecting custom lighting calculations without creating entirely custom shaders. The technique is inspired by cel-shading (also known as toon shading) used in games and animation.

Overview

The shader analyzes light intensity and quantizes it into discrete levels, creating sharp transitions between light and shadow. This gives everything a stylized, illustrated appearance reminiscent of Chinese ink paintings or Japanese animation.

Implementation

The cell shading is implemented by modifying the shader code at runtime:


float lightIntensity = length(reflectedLight.directDiffuse);

  

float cellLevel;

if (lightIntensity > 0.7) cellLevel = 1.0;

else if (lightIntensity > 0.4) cellLevel = 0.6;

else if (lightIntensity > 0.2) cellLevel = 0.3;

else cellLevel = 0.15;

This creates four distinct lighting levels:

  • Bright: Light intensity > 0.7 → Full brightness (1.0)

  • Medium: Light intensity > 0.4 → 60% brightness

  • Dim: Light intensity > 0.2 → 30% brightness

  • Dark: Light intensity ≤ 0.2 → 15% brightness

Three.js Integration

The shader modification is done using Three.js's onBeforeCompile hook:


material.onBeforeCompile = (shader) => {

shader.fragmentShader = shader.fragmentShader.replace(

'#include <output_fragment>',

`

float lightIntensity = length(reflectedLight.directDiffuse);

float cellLevel;

if (lightIntensity > 0.7) cellLevel = 1.0;

else if (lightIntensity > 0.4) cellLevel = 0.6;

else if (lightIntensity > 0.2) cellLevel = 0.3;

else cellLevel = 0.15;

reflectedLight.directDiffuse *= cellLevel;

#include <output_fragment>

`

);

};

This approach allows us to:

  • Use standard Three.js materials as a base

  • Inject custom lighting calculations

  • Maintain compatibility with Three.js lighting system

  • Avoid writing entire custom shaders from scratch

Visual Style

The cell-shaded aesthetic creates:

  • Sharp transitions between light and shadow areas

  • Discrete lighting levels instead of smooth gradients

  • Stylized appearance that fits the minimalist design

  • Consistent look across terrain, trees, and other objects

This technique is commonly used in non-photorealistic rendering (NPR) to achieve a cartoon or cel-shaded aesthetic.

Performance Considerations

The cell shading implementation is highly performant:

  • Uses simple if/else chains, not complex calculations

  • Runs entirely on the GPU in the fragment shader

  • No additional CPU overhead

  • Minimal impact on frame rate

References

Related Articles

Related Content

The 3D Background

The 3D Background The animated 3D background is a procedurally generated landscape that creates an infinite, dynamically rendered terrain with fractal trees, atmospheric effects, and interactive camer...