How to render outlines in WebGL

Left — boundary outline only. Right — the technique described in this article. Boat model by Google Poly.
Top, a stylized two-tone lighting in ThreeJS inspired by Return of the Obra Dinn. Bottom, the same scene with outlines. Ship model from Museovirasto Museiverket Finnish Heritage Agency on Sketchfab.

Live Demo

Overview of the technique

  1. The depth buffer
  2. The normal buffer
  3. The color buffer (the original scene)

Overview of the rendering pipeline

This effect requires 3 passes. Two render-passes and one post-process.
Each stage in this rendering pipeline visualized.

Implementation

1. Get the depth buffer

  • You need to know how the values are “packed”. Given the limited precision, does the engine just linearly interpolate Z values camera.near to camera.far? Does it do this in reverse? Or use a logarithmic depth buffer?
  • The engine most likely already has some mechanisms for working with depth values that you can re-use. For ThreeJS, you can include #include <packing> in your fragment shader which will allow you to use these helper functions.
  • For just visualizing it for debug purposes, you can collapse your camera’s near/far to cover the bounds of the object so you can more clearly see the image.

2. Create a normal buffer

this.renderScene.overrideMaterial = new THREE.MeshNormalMaterial();renderer.render(this.renderScene, this.renderCamera);                      this.renderScene.overrideMaterial = null;

3. Create the outline post process

float depth = getPixelDepth(0, 0);                           
// Difference between depth of neighboring pixels and current.
float depthDiff = 0.0;
depthDiff += abs(depth - getPixelDepth(1, 0)); depthDiff += abs(depth - getPixelDepth(-1, 0)); depthDiff += abs(depth - getPixelDepth(0, 1)); depthDiff += abs(depth - getPixelDepth(0, -1));
vec3 normal = getPixelNormal(0, 0);
// Difference between normals of neighboring pixels and current float normalDiff = 0.0;
normalDiff += distance(normal, getPixelNormal(1, 0)); normalDiff += distance(normal, getPixelNormal(0, 1)); normalDiff += distance(normal, getPixelNormal(0, 1)); normalDiff += distance(normal, getPixelNormal(0, -1));
float outline = normalDiff + depthDiff;
gl_FragColor = vec4(vec3(outline), 1.0);
  • We can include the diagonals in our neighbor sampling to get a more accurate outline
  • We can sample one or more neighbors further, to get thicker outlines
  • We can multiply normalDiff and depthDiff by a scalar to control their influence on the final outline
  • We can tweak normalDiff and depthDiff so that only really stark differences in depth or normal direction show up as an outline. This is what the “normal bias” and the “depth bias” parameters control.

4. Combine the outlines with your final scene

float outline = normalDiff + depthDiff;
vec4 outlineColor = vec4(1.0, 1.0, 1.0, 1.0);//white outline
gl_FragColor = vec4(mix(sceneColor, outlineColor, outline));
Stylized lighting in ThreeJS inspired by Return of the Obra Dinn. Notice that the outlines change color based on the scene’s lighting.

--

--

--

Graphics programmer working on maps. I love telling stories and it's why I do what I do, from making games, to teaching & writing. https://omarshehata.me/

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

BootstrapVue — Cards

JavaScript-Based GitHub Repos We Can Use

Introduction to TypeScript Data Types

How to Use the setState Callback with React Hooks?

Performant JavaScript — Code Changes

JavaScript Basics — Objects and Inheritance

Material UI — Links and Menus

Introduction to Testing with Jest

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Omar Shehata

Omar Shehata

Graphics programmer working on maps. I love telling stories and it's why I do what I do, from making games, to teaching & writing. https://omarshehata.me/

More from Medium

Camera orbit, zoom-in, zoom-out movements in WebGL

An introduction to WebAssembly

Customizing Materials: The Community Ninja Tale

Practice Rust and TAURI: Make an Image Viewer #4

An example of selecting an image file.