Command Palette

Search for a command to run...

Hectal
Course outline
Topic 14.2Advanced ~20 min · step 2 of 6

WebGL Is a State Machine

In one line

WebGL calls don't take "what to draw with" as arguments. They change global settings (the bound buffer, the current program, attribute pointers), and every later call uses whatever is set. Forget one setting and the next draw silently uses the last one. VAOs bundle the per-object settings.

0/6 · 0%

Think of it like this

A shared office printer. Whoever printed last left it on A3, landscape. You press Print without checking and your letter comes out wrong. No error: the printer did what its settings said.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

state machine
A system whose behaviour depends on current settings that earlier commands changed.
bind
Make an object the current one for a role (ARRAY_BUFFER, TEXTURE_2D), so later calls act on it.
VAO
Vertex array object: saved attribute settings that one bindVertexArray restores.
uniform
A value that is the same for every vertex and pixel of a draw (a colour, a matrix); it lives in a program.
INVALID_OPERATION
Error 1282: the call isn't allowed in the current state.

Step by step

01Run the two passes

The first example draws twice: once with vertexAttribPointer missing for the second buffer, once complete. Missing: the left pixel is blue, the right empty.

02Ask WebGL what it believes

getParameter(ARRAY_BUFFER_BINDING) and getVertexAttrib(0, VERTEX_ATTRIB_ARRAY_BUFFER_BINDING) show what is bound and what attribute 0 reads. Querying state is how you debug these bugs.

03Count the calls

The second example wraps a few methods to count them, then draws three shapes without and with VAOs.

04Unbind after setup

While a VAO is bound, every attribute call changes that VAO. bindVertexArray(null) after setup stops one object's setup from editing another's.

Try it yourself

  1. 1

    Point, then bind

    In the first example's draw(), move the second vertexAttribPointer line above gl.bindBuffer(gl.ARRAY_BUFFER, right). Both passes print left blue | right empty, and the last line says attribute 0 reads 'right': false: the order of two calls is the whole bug.

  2. 2

    Add a fourth shape

    In the second example, change [0, 1, 2] to [0, 1, 2, 3]. Without VAOs: 33 calls (8 more: 2 binds, 2 enables, 2 pointers, a uniform, a draw). With VAOs: 14 calls (3 more).

Code & diagrams

Two triangles and one forgotten line New tab

Both passes run in one go; the first forgets `vertexAttribPointer` for the second buffer.

idle

Console

Run the scene to see its console.

Expected console output

forgot vertexAttribPointer: left blue | right empty
with it:                   left orange | right blue
bound ARRAY_BUFFER is 'right': true
attribute 0 reads 'right': true
Calls per frame, without and with VAOs New tab

Counts, not timings: the same on every machine.

idle

Console

Run the scene to see its console.

Expected console output

without VAOs -> 25 calls: useProgram 1, bindBuffer 6, enableVertexAttribArray 6, vertexAttribPointer 6, uniform4f 3, drawArrays 3
with VAOs    -> 11 calls: useProgram 1, bindVertexArray 4, uniform4f 3, drawArrays 3
errors: none

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Select the program too late

Delete the top-level gl.useProgram(program); and add it at the end of draw() instead.

terminal
$ Press Run
── what you'll see ──
forgot vertexAttribPointer: left empty | right empty
with it: left orange | right blue
The first pass draws nothing; the second works, because the program is now left current from the first.

Myth vs fact

Myth

A WebGL bug always shows up as an error.

Fact

Many state bugs are legal calls with the wrong settings. getError() returns NO_ERROR for the forgotten pointer above.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    getError() and getParameter() make the browser wait for the GPU process. Use them while debugging, never every frame; three.js tracks state instead of querying it.

  • ▸

    VAOs are core in WebGL 2 (in WebGL 1 they were the OES_vertex_array_object extension). three.js keeps one per geometry and program pair (WebGLBindingStates), so a normal frame needs one bindVertexArray per mesh (Topic 14.5).

Remember this

  1. 1

    The problem this solves: wrong pictures with no error are the hardest WebGL bugs. They come from state left over by earlier code. Knowing which calls set state and which use it is how you find them, and why three.js tracks every setting itself.

  2. 2

    Set, then use. bindBuffer(ARRAY_BUFFER, b) draws nothing; it changes a setting. vertexAttribPointer reads that setting when you call it and stores "attribute 0 reads buffer b". Binding another buffer later doesn't change attribute 0. In the first example the blue triangle's buffer is bound but never pointed to, so it is drawn with the orange triangle's corners.

  3. 3

    Uniforms belong to the current program. uniform4f sets a value in whatever useProgram last selected; with no program current, it fails with INVALID_OPERATION.

  4. 4

    VAOs. A vertex array object records, per attribute, which buffer it reads, the layout and whether it is enabled, plus the index buffer. bindVertexArray(vao) restores all of it. For 3 shapes with 2 attributes each: 25 calls per frame without VAOs, 11 with them.

  5. 5

    Why three.js caches state. three.js keeps its own copy of the GL state (WebGLState) and skips calls that set what is already set. Topic 14.5 shows what breaks when your code changes the real state behind its back.

Explain it without notes

01

What does it mean that WebGL is a state machine, and what kind of bug does it cause?

02

What does a VAO store, and why does it make drawing faster and safer?

Practice

01

Rewrite the first example's draw() with one VAO per triangle.

02

Write logState(gl), printing the bound array buffer, the current program and attribute 0's buffer.

Done when you can

  • You can explain bind-then-use with an example

  • You can find a forgotten-pointer bug by querying state

  • You set up VAOs once and unbind after setup

  • You select the program before setting its uniforms