How to render outlines in WebGL
This article describes how to visualize outlines for a WebGL scene as a post process, with example implementations for ThreeJS & PlayCanvas.
Note: I’ve written a follow up that builds on this article with an improved technique that solves many of the artifacts present here. See Better outline rendering using surface IDs with WebGL.
There are a few common approaches that produce boundary-only outlines as shown on the left of the above picture.
- Drawing objects twice, such that the backfaces make up the outline, described here.
- A post process using the depth buffer, implemented in ThreeJS here.
- Similar post process implemented in PlayCanvas here.
Rendering the full outlines of a scene is particularly useful when you need to clearly see the geometry and structure of your scene. For example, the stylized aesthetic of Return of the Obra Dinn would be very hard to navigate without clear outlines.
The technique I describe here is similar to the post process shaders linked above, with the addition of a “normal buffer” in the outline pass that is used to find those inner edges.
Live Demo
Below is a link to a live demo of this technique implemented in ThreeJS. You can drag and drop any glTF model (as a single .glb/glTF file) to see the outline effect on your own test models:
https://threejs-outlines-postprocess.glitch.me/
You can also find the source code on GitHub: https://github.com/OmarShehata/webgl-outlines.
Overview of the technique
Our outline shader needs 3 inputs:
- The depth buffer
- The normal buffer
- The color buffer (the original scene)
Given these 3 inputs we will compute the difference between the current pixel’s depth value and its neighbors. A large depth difference tells us there’s a distance gap (this will typically give you the outer boundary of an object but not fine details on its surface).
We will do the same with the normal buffer. A difference in normal direction means a sharp corner. This is what gives us the finer details.
We then combine those differences to form the final outline, and combine that with the color buffer to add the outlines to the scene.
Tip: The live demo has a scaling factor for each of the normal & depth. You can scale that to 0 to see the influence of each on the final set of outlines.
Overview of the rendering pipeline
Here is how we’re going to set up our effect:
Render pass 1 captures the color of all objects in the scene in “Scene Buffer”.
It also outputs the depth of every pixel in a separate “Depth Buffer”.
Render pass 2 re-renders all objects in the scene with a normal material that colors it using the object’s view-normal at every pixel. This is written
to the “Normal Buffer”.
Finally, Outline pass is a post process, taking the 3 buffers and rendering onto a fullscreen quad.
This can be further optimized by modifying the engine to combine the normal and depth buffers into one “NormalDepth”, similar to how Unity does it, to avoid the need for the 2nd render pass.
A final step not shown in the diagram is an FXAA pass, which we need because we’re rendering the scene onto an off-screen buffer, which disables the browser’s native antialiasing.