Course outline
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.
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
Move a corner
In the first example, change the last vertex from
0, 0.7to0, -0.2. The triangle becomes a sliver near the bottom, and the middle pixel reads11, 17, 24, 255: the middle is no longer inside it. - 2
Draw two vertices
Change
drawArrays(gl.TRIANGLES, 0, 3)to0, 2. Nothing is drawn anderrors: none: 2 vertices make zero triangles, which is legal. - 3
Find the one-character fix
In the second example, change
float a = 1;tofloat a = 1.0;: that shader now printscompiled. GLSL ES never turns anintinto afloatfor you, except inside constructors such asvec4(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.
No three.js: the viewport shows `WebGL 2 · raw`. The colour is one constant, so the pixels match on every GPU.
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: noneANGLE checks shaders before the driver sees them. Safari, and Firefox on Linux and macOS, word these differently.
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 varyingBreak 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).
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.
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.
- ▸
getShaderInfoLogwaits for the GPU process. three.js only checks whenrenderer.debug.checkShaderErrorsis true (the default); apps with many shaders sometimes turn it off in production. - ▸
readPixelsmakes the CPU wait for the GPU: fine in checks, wrong every frame. Some drivers logGPU stall due to ReadPixelsin the console (it varies by machine). - ▸
Linking is where drivers do the expensive work. With the
KHR_parallel_shader_compileextension you pollCOMPLETION_STATUS_KHRinstead of blocking onLINK_STATUS; three.js'srenderer.compileAsync()uses it.
Remember this
- 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
1. The context.
canvas.getContext("webgl2")returns the object every call goes through, ornull(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
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.compileShadercompiles each;linkProgramjoins them and checks that what one sends, the other expects. Neither throws: askgetShaderParameter(shader, COMPILE_STATUS)and readgetShaderInfoLog(). - 4
3 and 4. Buffer and layout.
createBuffer,bindBuffer(ARRAY_BUFFER, buffer)andbufferDatacopy 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'spositioninput to read 2 floats per vertex from the buffer bound now. A VAO records these settings (Topic 14.2). - 5
5. Draw.
viewport,clear,useProgram, thendrawArrays(TRIANGLES, 0, 3)sends 3 vertices down the pipeline from Phase 1. The middle pixel reads back as exactly255, 107, 44: Lumen orange.
Explain it without notes
Walk through the five steps of drawing a triangle in raw WebGL 2, naming one call in each.
Why does a broken shader not throw, and how do you find what is wrong?
Practice
Make the triangle Lumen blue #2a9df4 and print the middle pixel.
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