WebGL: overlaying a wireframe with 2D canvas

Published

I've been experimenting with WebGL, just for fun. I found the Mozilla Developer Network introductory tutorial very helpful. But I wanted to debug some problems with the shapes I was creating and thought showing a wireframe overlay would help. Unfortunately that seems to be non-trivial with GL.

Here's a screenshot of what I ended up with though. This shows the outline of each triangle making up an object, with a bounding box thrown in for good measure.


    3D display of three objects, a cube, icosohedron, and cylinder.
    They are overlayed with lines showing the vertices of the triangles used to draw them.

Actually drawing single pixel lines with WebGL or OpenGL is surprisingly hard. I've seen discussions of ways to do it with clever shader techniques, but that seems a bit backwards when I'm trying to debug simple geometry problems. Learning to write fancy shaders seems more advanced than my novice tinkering.

Using a 2D canvas

The solution I came up with is perhaps a bit of a hack, but works well in some cases. It's easy to draw two-dimensional lines with a 2D canvas, so just use the 2D API for drawing the wireframe.

It turns out that you can't use a single <canvas> element for drawing with both WebGL and the original 2D canvas API. The second call to the .getContext() method fails if you do that.

What I'm doing instead with the example below is:

  1. Creating two <canvas> elements
  2. Using CSS absolute positioning to put the two elements in the same place, overlapping
  3. Making the first an opaque WebGL canvas and drawing my 3D scene in that
  4. Making the second a transparent 2D canvas and using that to draw the outlines of the same triangles

The HTML ends up looking like this:

<figure id=canvas-wrapper>
  <canvas id=webgl-canvas>(fallback text)</canvas>
  <canvas id=wireframe-canvas>(fallback text)</canvas>
</figure>

The CSS to put one on top of the other is quite simple. Apart from making sure that they end up the same size, this is all you need:

#canvas-wrapper {
    position: relative;
}
#wireframe-canvas {
    position: absolute;
    top: 0;
}

JavaScript code

Here's how I initialize the canvases:

const $gl_canvas = document.getElementById("webgl-canvas");
const $wireframe_canvas = document.getElementById("wireframe-canvas");

let gl, wire;

function init_canvases () {
    // Opaque 3D canvas in the background.
    gl = $gl_canvas.getContext("webgl", { alpha: false });

    // Transparent 2D canvas in the foreground.
    wire = $wireframe_canvas.getContext("2d", { alpha: true });

    // Basic OpenGL settings.
    gl.enable(gl.CULL_FACE);
    gl.cullFace(gl.BACK);
    gl.enable(gl.DEPTH_TEST);
    gl.depthFunc(gl.LEQUAL);
}

When drawing, you need to first clear both images, and make sure that the 2D one is cleared to transparent:

// Clear the wireframe canvas to it's default transparent.
$wireframe_canvas.width = $wireframe_canvas.width;

// Clear the WebGL canvas to a solid color.
gl.clearColor(0.2, 0.0, 0.0, 1.0);
gl.clearDepth(1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

To draw the wireframe I'm using the same projection and model/view matrices as I feed to the vertex shader. To calculate the 2D coordinates I then have to duplicate the work of the vertex shader to project the triangles. Unless of course there's some way to get at the output of the vertex shader directly, but if there is it's not something that I've yet discovered.

So what I do for each vertex v is apply the transformation then scale to the canvas size:

const trans = mat4.create();
mat4.multiply(trans, projection_matrix, model_view_matrix);

// For each vertex:
vec3.transformMat4(v, v, trans);
const x = (v[0] *  0.5 + 0.5) * $wireframe_canvas.width;
const y = (v[1] * -0.5 + 0.5) * $wireframe_canvas.height;

Example

If you have JavaScript enabled then this embedded example should work, or there's a separate stand-alone example page. Use the checkbox to change whether the wireframe is drawn.

You need JavaScript enabled and support for the <canvas> element for this to work.

You need JavaScript enabled and support for the <canvas> element for this to work.