Why a Library

Why Reach for a Library Instead of Raw WebGL

webgl-helper.js grew to 35 parts and 825 lines before this chapter could texture, light, shadow and pick six boxes, and it still has no text rendering, no glTF loader and no resize handling. Every WebGL project rebuilds that layer, and a library is someone else's tested, documented copy of it. Libraries sit at three heights. Helpers (twgl.js 3,005 , regl 5,583 ) remove boilerplate such as program linking, attribute setup and uniform setters, but you still write shaders and decide every draw. Renderers and engines (PixiJS 313,967 , OGL 4,660 , Three.js 30,815 , Babylon.js 89,603 ) add a scene graph, materials, cameras and loaders, and choose the draws for you. Domain engines (deck.gl 492,297 , Cesium 76,128 , MapLibre 34,367 ) know about maps, globes and data layers. Game engines (Unity 2,152 , Godot 15,706 , PlayCanvas 62,178 ) go further and compile a whole project for the web.

Stay with raw WebGL for a small, fixed effect, for learning, or when kilobytes matter. Reach for a library once you need text, model loading or a later WebGPU path, and pin exact versions in CDN URLs, as this book does.

Six lit covers in raw WebGL with every gl call counted, beside the three heights of library that would write those calls for youHTMLLive
<!doctype html>
<style>
  body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
  .row { display: flex; max-width: 600px; }
  canvas { display: block; width: 50%; }
  .stack { position: relative; width: 50%; }
  .stack div { position: absolute; left: 6%; width: 88%; box-sizing: border-box; padding: 6px 8px; color: #fff; border-radius: 3px; }
  .stack b { display: block; font-size: 12px; }
  p { max-width: 600px; margin: 4px 6px; font: 11px monospace; }
</style>
<div class="row"><canvas id="c" width="300" height="300"></canvas><div class="stack" id="stack"></div></div>
<p id="out"></p>
<script>
const raw = document.getElementById('c').getContext('webgl2');
const counts = {};                                   // wrap the context so every call is tallied
const gl = new Proxy(raw, { get(target, key) {
  const value = target[key];
  if (typeof value !== 'function') return value;
  return (...args) => { counts[key] = (counts[key] || 0) + 1; return value.apply(target, args); };
} });
function shader(type, src) { const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); return s; }
const program = gl.createProgram();
gl.attachShader(program, shader(gl.VERTEX_SHADER, `#version 300 es
layout(location = 0) in vec3 aPosition; layout(location = 1) in vec3 aNormal;
uniform vec2 uOffset; uniform float uTurn; out vec3 vNormal;
void main() { mat3 r = mat3(cos(uTurn), 0, -sin(uTurn), 0, 1, 0, sin(uTurn), 0, cos(uTurn));
  vec3 p = r * (aPosition * vec3(0.13, 0.34, 0.04)); vNormal = r * aNormal;
  gl_Position = vec4(p.x + uOffset.x, p.y * 0.9 + p.z * 0.3 + uOffset.y, -p.z, 1); }`));
gl.attachShader(program, shader(gl.FRAGMENT_SHADER, `#version 300 es
precision mediump float; uniform vec3 uColor; in vec3 vNormal; out vec4 c;
void main() { c = vec4(uColor * (0.35 + 0.65 * max(dot(normalize(vNormal), normalize(vec3(0.4, 0.5, 1))), 0.0)), 1); }`));
gl.linkProgram(program);
const box = [];
for (const [n, a1, b1] of [[[0,0,1],[1,0,0],[0,1,0]], [[0,0,-1],[-1,0,0],[0,1,0]], [[1,0,0],[0,0,-1],[0,1,0]],
                           [[-1,0,0],[0,0,1],[0,1,0]], [[0,1,0],[1,0,0],[0,0,-1]], [[0,-1,0],[1,0,0],[0,0,1]]])
  for (const [a, b] of [[-1,-1],[1,-1],[1,1],[-1,-1],[1,1],[-1,1]]) box.push(...[0, 1, 2].map(i => n[i] + a * a1[i] + b * b1[i]), ...n);
gl.bindVertexArray(gl.createVertexArray());
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(box), gl.STATIC_DRAW);
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 24, 0); gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 24, 12); gl.enableVertexAttribArray(1);
const where = ['uOffset', 'uTurn', 'uColor'].map((name) => gl.getUniformLocation(program, name));
gl.useProgram(program); gl.enable(gl.DEPTH_TEST);
gl.clearColor(0.93, 0.91, 0.87, 1); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
const colors = [[0.12, 0.37, 0.55], [0.36, 0.25, 0.6], [0.88, 0.6, 0.06], [0.25, 0.49, 0.23], [0.71, 0.27, 0.18], [0.16, 0.62, 0.56]];
colors.forEach((color, i) => {
  gl.uniform2f(where[0], -0.62 + (i % 3) * 0.62, i < 3 ? 0.45 : -0.5);
  gl.uniform1f(where[1], 0.5 - (i % 3) * 0.3);
  gl.uniform3fv(where[2], color);
  gl.drawArrays(gl.TRIANGLES, 0, 36);
});
const total = Object.values(counts).reduce((a, b) => a + b, 0);
document.getElementById('out').textContent = `${total} gl calls, ${Object.keys(counts).length} different functions, no text, loaders or resize: ` +
  Object.entries(counts).map(([k, v]) => `${k} ${v}`).join(', ');
// The three heights of library, drawn as a stack (lowest = closest to raw WebGL).
const layers = [['Game engines', 'Unity, Godot, PlayCanvas: compile a whole project', '#2a3d52'],
  ['Domain engines', 'deck.gl, Cesium, MapLibre: maps, globes, data layers', '#5b3f99'],
  ['Renderers and engines', 'PixiJS, OGL, Three.js, Babylon.js: scene graph, materials', '#1e5f8c'],
  ['Helpers', 'twgl.js, regl: less boilerplate, you still draw', '#2a9d8f'],
  ['Raw WebGL 2', 'this canvas: every call by hand', '#b04a2e']];
document.getElementById('stack').innerHTML = layers.map(([name, text, bg], i) =>
  `<div style="top:${4 + i * 19}%;background:${bg};height:17%"><b>${name}</b>${text}</div>`).join('');
</script>