WebGL - Drawing Multiple Things

One of the most common questions after first getting something up in WebGL
is how do I draw mulitple things.

The first thing to realize is that with few exceptions, WebGL is like
having a function someone wrote where instead of passing lots of
parameters to the function you instead have a single function that draws
stuff and 70+ functions that set up the state for that one function. So
for example imagine you had a function that draws a circle. You could
program it like this

WebGL works this second way. Functions like gl.createBuffer,
gl.bufferData, gl.createTexture, and gl.texImage2D let you upload
buffer (vertex) and texture (color, etc..) data to WebGL.
gl.createProgram, gl.createShader, gl.compileProgram, and
gl.linkProgram let you create your GLSL shaders. Nearly all the rest of
the functions of WebGL are setting up these global variables or state
that is used when gl.drawArrays or gl.drawElements is finally called.

Knowing this a typical WebGL program basically follows this structure

At Init time

create all shaders and programs and look up locations

create buffers and upload vertex data

create textures and upload texture data

At Render Time

clear and set the viewport and other global state
(enable depth testing, turn on culling, etc..)

One thing to notice is since we only have a single shader program we only
called gl.useProgram once. If we had different shader programs you'd
need to call gl.useProgram before um... using each program.

This is another place where it's a good idea to simplify. There are
effectively 3 main things to combine.

A shader program (and its uniform and attribute info/setters)

The buffer and attributes for the thing you want to draw

The uniforms needed to draw that thing with the given shader.

So, a simple simplification would be to make an array of things to draw
and in that array put the 3 things togehter

There are a few basic optimizations. If the program we're about to draw
with is the same as the previous program we drew with then there's no need
to call gl.useProgram. Similarly if we're drawing with the same
shape/geometry/vertices we previously drew with there's no need to set
those up again.

So, a very simple optimization might look like this

var lastUsedProgramInfo = null;
var lastUsedBufferInfo = null;
objectsToDraw.forEach(function(object) {
var programInfo = object.programInfo;
var bufferInfo = object.bufferInfo;
var bindBuffers = false;
if (programInfo !== lastUsedProgramInfo) {
lastUsedProgramInfo = programInfo;
gl.useProgram(programInfo.program);
// We have to rebind buffers when changing programs because we
// only bind buffers the program uses. So if 2 programs use the same
// bufferInfo but the 1st one uses only positions then when
// we switch to the 2nd one some of the attributes will not be on.
bindBuffers = true;
}
// Setup all the needed attributes.
if (bindBuffers || bufferInfo != lastUsedBufferInfo) {
lastUsedBufferInfo = bufferInfo;
webglUtils.setBuffersAndAttributes(gl, programInfo, bufferInfo);
}
// Set the uniforms.
webglUtils.setUniforms(programInfo, object.uniforms);
// Draw
gl.drawArrays(gl.TRIANGLES, 0, bufferInfo.numElements);
});

This time let's draw a lot more objects. Instead of just 3 like before let's make
the list of things to draw larger

You could also sort the list by programInfo and/or bufferInfo so that
the optimization kicks in more often. Most game engines do this.
Unfortunately it's not that simple. If everything you're drawing is
opaque and then you can just sort. But, as soon you need to draw
semi-transparent things you'll need to draw them in a specific order.
Most 3D engines handle this by having 2 or more lists of objects to draw.
One list for opaque things. Another list for transparent things. The
opaque list is sorted by program and geometry. The transparent list is
sorted by depth. There might also be separate lists for other things like
overlays or post processing effects.

Here's a sorted example. On my machine I get ~31fps
unsorted and ~37 sorted. That's nearly a 20% increase. But, it's worst
case vs best case and most programs would be doing a lot more so it's
arguably not worth thinking about for all but the most special cases.

It's important to notice that you can't draw just any geometry with just
any shader. For example a shader that requires normals will not function
with geometry that has no normals. Similarly a shader that requires
textures will not work without textures.

This is one of the many reasons it's great to choose a 3D Library like
Three.js because it handles all of this for you.
You create some geometry, you tell three.js how you want it rendered and
it generates shaders at runtime to handle the things you need. Pretty
much all 3D engines do this from Unity3D to Unreal to Source to Crytek.
Some generate them offline but the important thing to realize is they
generate shaders.

Of course the reason you're reading these articles is you want to know
what's going on deep down. That's great and it's fun to write everything
yourself. It's just important to be aware WebGL is super low
level so there's a ton of work for you to do
if you want to do it yourself and that often includes writing a shader
generator since different features often require different shaders.