This document explains the key WebGL concepts used in this project and clarifies common points of confusion.
- Project Overview
- WebGL Coordinate System
- Shaders
- Attributes vs Uniforms
- How Data Flows Through WebGL
- Location Objects
- Buffer Binding
This project draws colored rectangles using WebGL. Each rectangle is made of two triangles (6 vertices total).
Files:
index.html- Contains the canvas element and loads the JavaScriptwebgl.js- Main WebGL logic and theWebGLBoxclassshader.js- Vertex and fragment shader source code
WebGL uses a special coordinate system where all positions are in the range -1 to +1:
y = +1 (top)
|
|
x = -1 ----+---- x = +1
(left) | (right)
|
y = -1 (bottom)
z = -1 (near) to z = +1 (far)
Key Points:
- These are NOT pixels, meters, or degrees
- The screen is always mapped to this -1 to +1 range
(0, 0, 0)is the center of the screen- This is why our boxes use values like
left: -0.5, right: 0.5
If you want to use latitude/longitude, you must convert them to clip space:
function latLongToClipSpace(lat, lon, bounds) {
// Map lat (-90 to 90) and lon (-180 to 180) to clip space (-1 to 1)
const x = ((lon - bounds.minLon) / (bounds.maxLon - bounds.minLon)) * 2 - 1;
const y = ((lat - bounds.minLat) / (bounds.maxLat - bounds.minLat)) * 2 - 1;
return { x, y };
}Important: For real geographic applications, you need map projections (like Mercator) to properly handle the Earth's curvature.
Shaders are programs that run on the GPU. They're written in GLSL (OpenGL Shading Language).
Runs once per vertex. Transforms vertex positions.
attribute vec3 position; // Input: vertex position from JavaScript
void main() {
// Set final position (gl_Position is a built-in output variable)
gl_Position = vec4(position, 1.0);
}Runs once per pixel. Determines pixel colors.
precision mediump float;
uniform vec4 vColor; // Input: color from JavaScript
void main() {
// Set pixel color (gl_FragColor is a built-in output variable)
gl_FragColor = vColor;
}Creating shaders involves multiple steps (webgl.js lines 3-36):
createShader()- Compiles individual shaders (vertex or fragment)linkProgram()- Links vertex + fragment shaders togethercreateWebGLProgram()- Combines the above two steps
Alternative: Libraries like TWGL simplify this:
const programInfo = twgl.createProgramInfo(gl, [
"vertex-shader",
"fragment-shader",
]);This is a key distinction in WebGL!
Different value for each vertex
// webgl.js:69
this.positionLocation = gl.getAttribLocation(this.webglProgram, "position");
// webgl.js:107-109
gl.enableVertexAttribArray(this.positionLocation);
gl.vertexAttribPointer(this.positionLocation, 3, gl.FLOAT, false, 0, 0);- Data comes from a buffer (array of values)
- Each vertex gets different position coordinates
- Used for: positions, normals, texture coordinates, vertex colors
Same value for all vertices in a draw call
// webgl.js:70
this.colorLocation = gl.getUniformLocation(this.webglProgram, "vColor");
// webgl.js:112
gl.uniform4fv(this.colorLocation, settings.color);- Data is set directly (not from a buffer)
- All vertices in one
draw()call share the same value - Used for: colors, transformation matrices, light positions, time
- Uniform = Classroom rule (same for all students)
- Attribute = Student names (different for each student)
// In constructor (runs when box is instantiated)
this.colorLocation = gl.getUniformLocation(this.webglProgram, "vColor");Important: colorLocation is NOT null after this line!
It returns a WebGLUniformLocation object - think of it as a "pointer" or "address" to where the uniform is in the shader program.
// Constructor: Get the "address"
this.colorLocation = gl.getUniformLocation(...);
// → Returns: WebGLUniformLocation { ... }
// Draw method: Send data to that address
gl.uniform4fv(this.colorLocation, [1, 0.4, 0.4, 1]);
// → Sends color values to the location// webgl.js:98-104
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer); // ← Buffer becomes "active"
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
// Connect positionLocation to the currently bound buffer
gl.vertexAttribPointer(this.positionLocation, 3, gl.FLOAT, false, 0, 0);gl.vertexAttribPointer() doesn't take the buffer as a parameter! Instead, it connects the attribute to whatever buffer is currently bound to gl.ARRAY_BUFFER.
WebGL works like a state machine with "slots":
gl.ARRAY_BUFFER (slot)
↓
[buffer] ← Currently plugged in
Steps:
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)- "Plug" buffer into the slotgl.vertexAttribPointer(...)- "Connect attribute to whatever's in the slot"
Think of gl.ARRAY_BUFFER as a USB port:
gl.bindBuffer()= Insert USB drive into portgl.vertexAttribPointer()= Tell attribute "read from the USB port"- The attribute remembers which buffer it's connected to
This is part of OpenGL's original design from the 1990s. Modern WebGL 2 has better alternatives (Vertex Array Objects), but this pattern is still fundamental to understanding WebGL.
JavaScript Data
↓
[1] Create Float32Array with vertex positions
↓
[2] Create buffer & upload to GPU
gl.createBuffer()
gl.bindBuffer()
gl.bufferData()
↓
[3] Connect buffer to attribute location
gl.vertexAttribPointer()
↓
[4] Set uniform values
gl.uniform4fv()
↓
[5] Draw command
gl.drawArrays(gl.TRIANGLES, 0, 6)
↓
GPU runs vertex shader for each vertex (6 times)
- Reads position from buffer via attribute
- Receives color via uniform
- Outputs gl_Position
↓
GPU rasterizes triangles into pixels
↓
GPU runs fragment shader for each pixel
- Receives interpolated values
- Uses vColor uniform
- Outputs gl_FragColor
↓
Pixels appear on screen!
During Instantiation (Constructor):
gl.useProgram(this.webglProgram); // Activate shader program
this.positionLocation = gl.getAttribLocation(...); // Get attribute "address"
this.colorLocation = gl.getUniformLocation(...); // Get uniform "address"
gl.enable(gl.DEPTH_TEST); // Enable depth testingDuring Each draw() Call:
// Create & upload vertex data
const data = new Float32Array([...vertices...]);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
// Connect attribute to buffer
gl.vertexAttribPointer(this.positionLocation, ...);
// Set uniform value
gl.uniform4fv(this.colorLocation, settings.color);
// Draw!
gl.drawArrays(gl.TRIANGLES, 0, 6);// ❌ Wrong (works in Node.js, not in browser)
import { vs, fs } from "./shader";
// ✅ Correct (required for browser ES6 modules)
import { vs, fs } from "./shader.js";<!-- ❌ Wrong - canvas in <head> -->
<head>
<canvas id="canvas"></canvas>
</head>
<!-- ✅ Correct - canvas in <body> -->
<body>
<canvas id="canvas"></canvas>
</body>// ❌ Wrong - attribute connected before buffer is bound
gl.vertexAttribPointer(this.positionLocation, 3, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
// ✅ Correct - bind buffer FIRST
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.vertexAttribPointer(this.positionLocation, 3, gl.FLOAT, false, 0, 0);ES6 modules require a web server. Can't just open file:// in browser.
Solutions:
- Python:
python -m http.server 8000 - Node.js:
npx http-server - VS Code: Use Live Server extension
- WebGL Fundamentals: https://webglfundamentals.org/
- TWGL Library: https://twgljs.org/ (simplifies WebGL boilerplate)
- The Book of Shaders: https://thebookofshaders.com/ (GLSL shaders)
- WebGL2 Fundamentals: https://webgl2fundamentals.org/ (modern WebGL)
| Function | Purpose | When to Call |
|---|---|---|
gl.createShader() |
Compile shader | Setup |
gl.createProgram() |
Link shaders | Setup |
gl.useProgram() |
Activate program | Setup / before draw |
gl.getAttribLocation() |
Get attribute location | Setup |
gl.getUniformLocation() |
Get uniform location | Setup |
gl.createBuffer() |
Create buffer | Per draw (or setup) |
gl.bindBuffer() |
Make buffer active | Before buffer operations |
gl.bufferData() |
Upload data to buffer | After binding |
gl.vertexAttribPointer() |
Connect attribute to buffer | Before draw |
gl.enableVertexAttribArray() |
Enable attribute | Before draw |
gl.uniform*() |
Set uniform value | Before draw |
gl.drawArrays() |
Execute draw | Draw time |
| Value | Position |
|---|---|
| x = -1 | Left edge |
| x = 0 | Horizontal center |
| x = +1 | Right edge |
| y = -1 | Bottom edge |
| y = 0 | Vertical center |
| y = +1 | Top edge |
| z = -1 | Near plane |
| z = 0 | Middle depth |
| z = +1 | Far plane |