Command Palette

Search for a command to run...

Hectal
Course outline
Topic 14.1Advanced ~20 min · step 1 of 6

A Triangle in Raw WebGL 2

In one line

Raw WebGL 2 draws in five steps: get a context, compile two shaders and link them into a program, put vertices in a buffer, describe that buffer to the shader, then draw. Every step can fail, and nothing throws: you must ask.

0/6 · 0%

Think of it like this

Ordering a poster at a print shop. You hand over the design (the shaders), the measurements (the vertex buffer) and a note saying which number means what (the attribute layout), then say "print". three.js is the assistant who does all of that for you; this topic is you at the counter.

Words you'll meet

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

context
The object a canvas gives for one API (webgl2, webgpu, 2d); every WebGL call is a method on it.
drawing buffer
The canvas's real pixels (canvas.width × canvas.height), separate from its size on the page.
shader
A small program that runs on the GPU: the vertex shader places corners, the fragment shader colours pixels.
program
A vertex and a fragment shader linked together; one draw uses one program.
attribute
A per-vertex input of the vertex shader (here position), fed from a buffer.
info log
The text the compiler or linker writes when something is wrong; nothing throws, you read it yourself.

Step by step

01Read the status bar

Run the first example. The status bar says WebGL 2 · raw and 1 draw calls: no three.js, no scene tree. That one drawArrays is the whole picture.

02Follow the numbered comments

Context, program, buffer, layout, draw: about 25 calls for one flat triangle, and every check is your job.

03Read pixels back

readPixels copies pixels into a Uint8Array: 255, 107, 44, 255 in the middle, the stage colour 11, 17, 24, 255 in the corner.

04Break the shaders on purpose

The second example compiles four broken fragment shaders and links two mismatched programs. The text comes from ANGLE, the layer Chrome, Edge and Firefox on Windows use to run WebGL, so it reads the same on most machines.

Try it yourself

  1. 1

    Move a corner

    In the first example, change the last vertex from 0, 0.7 to 0, -0.2. The triangle becomes a sliver near the bottom, and the middle pixel reads 11, 17, 24, 255: the middle is no longer inside it.

  2. 2

    Draw two vertices

    Change drawArrays(gl.TRIANGLES, 0, 3) to 0, 2. Nothing is drawn and errors: none: 2 vertices make zero triangles, which is legal.

  3. 3

    Find the one-character fix

    In the second example, change float a = 1; to float a = 1.0;: that shader now prints compiled. GLSL ES never turns an int into a float for you, except inside constructors such as vec4(tint, 1), which is why the first statement on that line was fine.

Code & diagrams

Try it: interactive

The pipeline your five steps feed

Step through: the vertex shader is step 1 (clip coordinates), the fragment shader colours each pixel. Every call in the example sets up one of these stages.

A triangle in raw WebGL 2 New tab

No three.js: the viewport shows `WebGL 2 · raw`. The colour is one constant, so the pixels match on every GPU.

idle

Console

Run the scene to see its console.

Expected console output

attribute 'position' is at location 0
middle: 255, 107, 44, 255
corner: 11, 17, 24, 255
errors: none
Compile errors and link errors, as WebGL reports them New tab

ANGLE checks shaders before the driver sees them. Safari, and Firefox on Linux and macOS, word these differently.

idle

Console

Run the scene to see its console.

Expected console output

typo -> FAILED
ERROR: 0:5: 'colr' : undeclared identifier
ERROR: 0:5: 'assign' : l-value required (can't modify a const)
ERROR: 0:5: '=' : dimension mismatch
ERROR: 0:5: 'assign' : cannot convert from 'highp 4-component vector of float' to 'const highp float'
int to float -> FAILED
ERROR: 0:5: '=' : cannot convert from 'const int' to 'highp float'
no #version -> FAILED
ERROR: 0:2: 'in' : storage qualifier supported in GLSL ES 3.00 and above only
ERROR: 0:3: 'out' : storage qualifier supported in GLSL ES 3.00 and above only
no precision -> FAILED
ERROR: 0:2: '' : No precision specified for (float)
ERROR: 0:3: '' : No precision specified for (float)
  vertex -> compiled
  fragment -> compiled
type mismatch link -> FAILED
Types of varying 'tint' differ between VERTEX and FRAGMENT shaders.
FRAGMENT varying tint does not match any VERTEX varying
  vertex -> compiled
  fragment -> compiled
missing output link -> FAILED
FRAGMENT varying tint does not match any VERTEX varying

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

Name a variable `location`

In the first example, rename positionLoc to location (all four places).

terminal
$ Press Run
── what you'll see ──
Uncaught SyntaxError: Identifier 'location' has already been declared
Nothing runs, not even the first line.

Break #2

Skip the checks

Remove both checks (COMPILE_STATUS in compile() and LINK_STATUS), and type colr instead of colour in the fragment shader.

terminal
$ Press Run
── what you'll see ──
attribute 'position' is at location -1
middle: 11, 17, 24, 255
corner: 11, 17, 24, 255
errors: 1281
No triangle, no message. Remove only the compile check and the link check catches it, with the less helpful `Error: Fragment shader is not compiled.`

Myth vs fact

Myth

WebGL 2 is outdated now that WebGPU exists.

Fact

WebGL 2 runs in every current browser and is what three.js's WebGLRenderer uses; most 3D on the web still runs on it.

Pro corner

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

  • ▸

    getShaderInfoLog waits for the GPU process. three.js only checks when renderer.debug.checkShaderErrors is true (the default); apps with many shaders sometimes turn it off in production.

  • ▸

    readPixels makes the CPU wait for the GPU: fine in checks, wrong every frame. Some drivers log GPU stall due to ReadPixels in the console (it varies by machine).

  • ▸

    Linking is where drivers do the expensive work. With the KHR_parallel_shader_compile extension you poll COMPLETION_STATUS_KHR instead of blocking on LINK_STATUS; three.js's renderer.compileAsync() uses it.

Remember this

  1. 1

    The problem this solves: when a three.js scene breaks deep inside the renderer, you can't read the trace without knowing the calls underneath. And when three.js can't do a small special job, you talk to the GPU yourself. Both start with one triangle.

  2. 2

    1. The context. canvas.getContext("webgl2") returns the object every call goes through, or null (Topic 1.5). A canvas has two sizes: its CSS size and its drawing buffer (canvas.width × canvas.height, real pixels). Set the buffer to CSS size × devicePixelRatio, or the picture is blurry on sharp screens.

  3. 3

    2. The program. A shader is a small GPU program, written for WebGL 2 in GLSL ES 3.00 (first line #version 300 es). The vertex shader places each corner (gl_Position); the fragment shader colours each pixel. compileShader compiles each; linkProgram joins them and checks that what one sends, the other expects. Neither throws: ask getShaderParameter(shader, COMPILE_STATUS) and read getShaderInfoLog().

  4. 4

    3 and 4. Buffer and layout. createBuffer, bindBuffer(ARRAY_BUFFER, buffer) and bufferData copy six floats (x, y per corner, already in clip space, Topic 1.4) to the GPU. vertexAttribPointer(location, 2, FLOAT, false, 0, 0) tells the shader's position input to read 2 floats per vertex from the buffer bound now. A VAO records these settings (Topic 14.2).

  5. 5

    5. Draw. viewport, clear, useProgram, then drawArrays(TRIANGLES, 0, 3) sends 3 vertices down the pipeline from Phase 1. The middle pixel reads back as exactly 255, 107, 44: Lumen orange.

Explain it without notes

01

Walk through the five steps of drawing a triangle in raw WebGL 2, naming one call in each.

02

Why does a broken shader not throw, and how do you find what is wrong?

Practice

01

Make the triangle Lumen blue #2a9df4 and print the middle pixel.

02

Compile a fragment shader with no out at all. What does the compiler say?

Done when you can

  • You can draw a triangle in raw WebGL 2 and name the five steps

  • You check COMPILE_STATUS and LINK_STATUS and read the info logs

  • You size the drawing buffer by devicePixelRatio

  • You can read a pixel back with readPixels