Toon shading

툰 셰이딩

Shading in a few sharp color bands instead of a smooth gradient, for a cartoon/anime look.

Also known as: Cel shadingMeshToonMaterialGradient mapInverted hull outline
···
js
import * as THREE from 'three';
function makeToonGradient() {
  const size = 4;
  const c = document.createElement('canvas');
  c.width = size; c.height = 1;
  const ctx = c.getContext('2d');
  const shades = ['#241d33', '#5b4a8a', '#9c86d6', '#efe6ff'];
  shades.forEach((color, i) => { ctx.fillStyle = color; ctx.fillRect(i, 0, 1, 1); });
  const tex = new THREE.CanvasTexture(c);
  tex.minFilter = THREE.NearestFilter;
  tex.magFilter = THREE.NearestFilter;
  tex.needsUpdate = true;
  return tex;
}
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0c0c16);
const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100);
camera.position.set(0, 0.6, 5.2);

const dLight = new THREE.DirectionalLight(0xffffff, 1.6);
dLight.position.set(3, 4, 4);
scene.add(dLight);
scene.add(new THREE.AmbientLight(0x1a1a2c, 1));

const gradientMap = makeToonGradient();
const geo = new THREE.SphereGeometry(1.3, 64, 64);
const mesh = new THREE.Mesh(geo, new THREE.MeshToonMaterial({ color: 0xff8ab8, gradientMap }));
scene.add(mesh);
const outline = new THREE.Mesh(geo, new THREE.MeshBasicMaterial({ color: 0x0c0c16, side: THREE.BackSide }));
outline.scale.setScalar(1.06);
scene.add(outline);

function resize() { renderer.setSize(innerWidth, innerHeight); camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); }
addEventListener('resize', resize); resize();
renderer.setAnimationLoop((t) => {
  mesh.rotation.y = t * 0.0004;
  mesh.rotation.x = Math.sin(t * 0.00025) * 0.5;
  outline.rotation.copy(mesh.rotation);
  renderer.render(scene, camera);
});

Ordinary lighting math (diffuse = max(dot(N,L), 0)) produces a value that changes smoothly with angle. MeshToonMaterial re-looks-up that value in a small 1D texture called gradientMap — if the texture is stepped (say, 4 bands of gray), the lighting snaps between those steps too, giving the sharp banding that reads as cel animation. Setting the texture’s minFilter/magFilter to NearestFilter matters: otherwise the steps get interpolated and the bands blur together.

Outlines need a separate technique. The simplest is the "inverted hull": duplicate the same geometry, scale it up slightly (1.03–1.08×), and draw it behind the original mesh with a flat-color material set to side: THREE.BackSide. Only the back faces of this enlarged shell are visible, and they poke out just past the original mesh’s silhouette, reading as a black outline.

The demo shades a sphere with MeshToonMaterial and a 4-step gradient, then layers a black inverted hull behind it for a cartoon-character look.

When to use

Cartoon/anime-style characters, non-photorealistic game art.