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
-
Cel Shading on Wikipedia - Overview of cel-shading techniques
-
Non-Photorealistic Rendering - General NPR techniques
-
Three.js Material.onBeforeCompile - Official documentation
Related Articles
-
An adventure with 3js 3d Backgrounds - Overview of the animated background system
-
Terrain Generation - How terrain is generated and rendered
-
Fractal Trees - Tree rendering with cell shading