one box takes 122 lines * Line counts vary by coding style I wrote and ran one lit box in WebGL alone. You write almost the whole rendering pipeline yourself. webgl-cube.js (122 lines incl. 7 blank, nothing omitted) const canvas = document.querySelector('canvas') canvas.width = innerWidth canvas.height = innerHeight const gl = canvas.getContext('webgl') // Vertex shader: transform coordinates const vsSource = ` attribute vec3 aPosition; attribute vec3 aNormal; uniform mat4 uModel; uniform mat4 uView; uniform mat4 uProjection; varying vec3 vNormal; void main() { vNormal = mat3(uModel) * aNormal; gl_Position = uProjection * uView * uModel * vec4(aPosition, 1.0); }` // Fragment shader: decide pixel colors (lighting) const fsSource = ` precision mediump float; uniform vec3 uColor; uniform vec3 uLightDir; uniform float uAmbient; varying vec3 vNormal; void main() { float diffuse = max(dot(normalize(vNormal), normalize(uLightDir)), 0.0); vec3 color = uColor * (uAmbient + diffuse); gl_FragColor = vec4(color, 1.0); }` function compile(type, source) { const shader = gl.createShader(type) gl.shaderSource(shader, source) gl.compileShader(shader) if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { throw new Error(gl.getShaderInfoLog(shader)) } return shader } const program = gl.createProgram() gl.attachShader(program, compile(gl.VERTEX_SHADER, vsSource)) gl.attachShader(program, compile(gl.FRAGMENT_SHADER, fsSource)) gl.linkProgram(program) if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { throw new Error(gl.getProgramInfoLog(program)) } gl.useProgram(program) // Cube: 6 faces x 4 vertices. Normals differ per face, so vertices can't be const s = 0.1 const positions = new Float32Array([ -s, -s, s, s, -s, s, s, s, s, -s, s, s, // front -s, -s, -s, -s, s, -s, s, s, -s, s, -s, -s, // back -s, s, -s, -s, s, s, s, s, s, s, s, -s, // top -s, -s, -s, s, -s, -s, s, -s, s, -s, -s, s, // bottom s, -s, -s, s, s, -s, s, s, s, s, -s, s, // right -s, -s, -s, -s, -s, s, -s, s, s, -s, s, -s, // left ]) const normals = new Float32Array([ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0,-1, 0, 0,-1, 0, 0,-1, 0, 0,-1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0,-1, 0, 0,-1, 0, 0,-1, 0, 0,-1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, ]) const indices = new Uint16Array([ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23, ]) function attribute(name, data) { const buffer = gl.createBuffer() gl.bindBuffer(gl.ARRAY_BUFFER, buffer) gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW) const location = gl.getAttribLocation(program, name) gl.enableVertexAttribArray(location) gl.vertexAttribPointer(location, 3, gl.FLOAT, false, 0, 0) } attribute('aPosition', positions) attribute('aNormal', normals) const indexBuffer = gl.createBuffer() gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer) gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW) // Matrices: compute perspective and position yourself function perspective(fovy, aspect, near, far) { const f = 1 / Math.tan(fovy / 2) const nf = 1 / (near - far) return new Float32Array([ f / aspect, 0, 0, 0, 0, f, 0, 0, 0, 0, (far + near) * nf, -1, 0, 0, 2 * far * near * nf, 0, ]) } function translation(x, y, z) { return new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1]) } const uModel = gl.getUniformLocation(program, 'uModel') const uView = gl.getUniformLocation(program, 'uView') const uProjection = gl.getUniformLocation(program, 'uProjection') gl.uniform3fv(gl.getUniformLocation(program, 'uColor'), [0.26, 0.72, 0.51]) gl.uniform3fv(gl.getUniformLocation(program, 'uLightDir'), [5, 8, 5]) gl.uniform1f(gl.getUniformLocation(program, 'uAmbient'), 0.3) gl.uniformMatrix4fv(uView, false, translation(0, -1.2, 0)) gl.uniformMatrix4fv(uProjection, false, perspective(75 * Math.PI / 180, canvas.width / canvas.height, 0.1, 100)) gl.enable(gl.DEPTH_TEST) // Render loop: clear the screen, then draw function render() { gl.viewport(0, 0, canvas.width, canvas.height) gl.clearColor(0, 0, 0, 1) gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) gl.uniformMatrix4fv(uModel, false, translation(0, 1.2, -1.4)) gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0) requestAnimationFrame(render) } requestAnimationFrame(render) Colors = pipeline stages (lines) 36 Prepare vertices 33 Transform coordinates 15 Colors and lighting 17 Send shaders to the GPU 14 Set up canvas, draw Only rasterization is done automatically by the GPU What this code draws localhost:5173 Just one lit green box floating in space 31 / 62