WebGL, 2011
WebGL put a 3D API behind a browser canvas with no plugin. This era calls it raw from JavaScript with no library: query the context and its limits, fill buffers, link shader programs, set GL state and issue draw calls, with every gl.* call printed back.
Graphics track · 28 missions · boss mission, written exam and certificate · free, no signup. Everything below runs in the browser terminal on the SERVBG home page.
What you will do
- see the reference scene drawn by this era, as a texture on two triangles
refBefore 2011 a picture like this in a browser meant a plugin. WebGL 1.0 was released on 3 March 2011 and made it fifteen lines of JavaScript against the graphics card.
- query the live context: version, renderer and hardware limits
contextgetContext("webgl2") returns null rather than throwing when the browser refuses, which is why every serious WebGL page starts with a fallback to "webgl" and then to a static image.
- read how a 3D API got into the browser, with dates
timelineVladimir Vukicevic started Canvas 3D at Mozilla in 2006; Khronos took it into a working group in March 2009 with Mozilla, Google, Apple and Opera; the 1.0 spec landed in March 2011.
- walk the fixed stages every draw call passes through
pipelineYou write two stages, the vertex shader and the fragment shader. Primitive assembly, clipping, the perspective divide, the viewport transform and rasterisation are fixed hardware you only configure.
- wipe the framebuffer with gl.clearColor and gl.clear
clear 0.05 0.06 0.09clearColor stores a colour, clear applies it. GL colours are floats from 0.0 to 1.0, a convention inherited from OpenGL in 1992 and unchanged since.
- read and compile the vertex shader
shader vertA vertex shader has one required output: gl_Position, a vec4 in clip space. It runs once per vertex, in parallel, with no access to any other vertex.
- read and compile the fragment shader
shader fragThe precision line is mandatory: GLSL ES gives fragment floats no default precision, so a fragment shader without it fails to compile.
- attach both shaders, link them and check LINK_STATUS
program linkLinking matches the two shaders up. Every varying the fragment shader reads must be written by the vertex shader with the same name and type, or the link fails even though both compiled.
- upload three vertices to the GPU with gl.bufferData
buffer createbufferData writes into whatever is bound to the target, not into the buffer you pass it, because there is no buffer argument. WebGL is a state machine and this is the part that catches everyone once.
- wire the buffer to the shader attribute: size, type, stride, offset
attrib a_pos 2vertexAttribPointer captures the buffer bound at that instant. Bind the wrong one a line earlier and you get garbage geometry with no error raised at all.
- set a value that is constant for the whole draw call
uniform u_color 1 0.5 0Attribute varies per vertex, varying is interpolated per fragment, uniform is one value for the entire draw call. The suffix on uniform3f names the payload, not the uniform.
- issue the first real draw call, gl.drawArrays
draw triangleThe count argument is vertices, not triangles: gl.TRIANGLES consumes them in threes. The triangle is the only filled primitive the hardware knows.
- read one pixel back out of the framebuffer and check the colour
readpixel 160 140readPixels stalls the pipeline: the CPU must wait for every queued GPU command before it can answer, which is why it belongs in tests and screenshots, never in a render loop.
- make a deliberate mistake and decode what gl.getError returns
errorWebGL never throws for a GL mistake. It sets a flag that stays set until something reads it, so getError returns the first error since the last read and then resets to NO_ERROR.
- upload four vertices with position and texture coordinate interleaved
buffer quadInterleaving beats two separate buffers because one vertex is one contiguous 16-byte read: one cache line touched instead of two.
- describe two triangles out of four vertices with an index buffer
indicesdrawElements arrived in OpenGL 1.1 in 1997 so a shared vertex could be uploaded once and referenced many times. UNSIGNED_SHORT indices cap a mesh at 65,536 vertices.
- upload the reference image to the GPU with gl.texImage2D
texture loadUNPACK_FLIP_Y_WEBGL exists because HTML images start at the top-left while texture coordinate v = 0 means the bottom. Without the flip the picture arrives upside down.
- bind the texture to a texture unit and point the sampler at it
samplerA sampler2D uniform holds a texture unit number, so it is set with uniform1i. Passing the texture object instead is the most common WebGL texture bug there is.
- switch the texture from nearest to bilinear sampling
filter linearMIN_FILTER applies when the texture is squeezed, MAG_FILTER when it is stretched. Bilinear filtering has been free in silicon since the 3dfx Voodoo in 1996.
- draw the textured quad with gl.drawElements
draw quadThe second, smaller square is drawn last and sits farther away, yet it paints over the picture. With no depth test, draw order is the only order there is.
- enable the depth test and watch the far square go behind
depth onZ-fighting is the depth buffer running out of precision: two nearly coplanar surfaces land in the same 24-bit bucket and flicker. Moving the near plane out from 0.001 to 0.1 fixes more of it than anything else.
- enable alpha blending with the standard source-over function
blend onSRC_ALPHA with ONE_MINUS_SRC_ALPHA is the source-over operator Porter and Duff defined in 1984. Blended geometry has to be sorted back to front, because it needs whatever is behind it already drawn.
- ask the context what is actually switched on right now
stateThere is no call that returns the whole context state. Renderers keep a shadow copy in JavaScript and only call gl.* when a value really changed, because a function that flips a switch and forgets to flip it back breaks code it never touched.
- shrink the viewport and see NDC squeezed into a corner
resize 160 120The viewport maps normalised device coordinates, always -1 to +1, onto a pixel rectangle. gl.clear ignores it and wipes the whole drawing buffer regardless.
- match the viewport back to the drawing buffer
resize 320 240A canvas has two sizes: the drawing buffer in pixels and the CSS box it is stretched into. Letting them drift apart is why so much WebGL looks blurry, and forgetting the viewport call after a resize is why the rest looks stretched.
- read the render loop and why it is driven by delta time
loopRobert O'Callahan proposed requestAnimationFrame at Mozilla in 2010. It fires once per display refresh and stops completely in a hidden tab, which is exactly why animation is scaled by elapsed seconds and not by frame count.
- see what WebGL 2.0 added in 2017 and how GLSL changed
webgl2The WebGL 2.0 specification was released on 17 January 2017 on top of OpenGL ES 3.0: vertex array objects, instancing, 3D textures and transform feedback all became core, and GLSL ES 3.00 replaced attribute and varying with in and out.
- Boss missionanimate the rotation uniform — textured quad, depth test and blending on
spinOne float and one draw call per frame is the whole idea: geometry uploaded once and transformed by a uniform, instead of the 1990s habit of pushing every vertex across the bus every frame.
Certificate
This track is certifiable. Clear the boss mission in the terminal, then run EXAM WEBGL for the written paper: 20 server-graded questions drawn from our own bank, pass mark 14 of 20. The certificate is issued once both are done, and it carries a verification code.