Skip to content

leslieduan/webgl-tut

Repository files navigation

WebGL Basics - Concept Guide

This document explains the key WebGL concepts used in this project and clarifies common points of confusion.

Table of Contents


Project Overview

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 JavaScript
  • webgl.js - Main WebGL logic and the WebGLBox class
  • shader.js - Vertex and fragment shader source code

WebGL Coordinate System

Clip Space (NDC - Normalized Device Coordinates)

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

Using Geographic Coordinates

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

Shaders are programs that run on the GPU. They're written in GLSL (OpenGL Shading Language).

Vertex Shader (shader.js - vs)

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);
}

Fragment Shader (shader.js - fs)

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 (The Complicated Part)

Creating shaders involves multiple steps (webgl.js lines 3-36):

  1. createShader() - Compiles individual shaders (vertex or fragment)
  2. linkProgram() - Links vertex + fragment shaders together
  3. createWebGLProgram() - Combines the above two steps

Alternative: Libraries like TWGL simplify this:

const programInfo = twgl.createProgramInfo(gl, [
  "vertex-shader",
  "fragment-shader",
]);

Attributes vs Uniforms

This is a key distinction in WebGL!

Attributes

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

Uniforms

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

Analogy

  • Uniform = Classroom rule (same for all students)
  • Attribute = Student names (different for each student)

Location Objects

Common Confusion: When are locations set?

// In constructor (runs when box is instantiated)
this.colorLocation = gl.getUniformLocation(this.webglProgram, "vColor");

Important: colorLocation is NOT null after this line!

What getUniformLocation() Returns

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

Buffer Binding

The Confusing Part: Implicit Connection

// 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);

Why is this confusing?

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 State Machine

WebGL works like a state machine with "slots":

gl.ARRAY_BUFFER (slot)
    ↓
  [buffer] ← Currently plugged in

Steps:

  1. gl.bindBuffer(gl.ARRAY_BUFFER, buffer) - "Plug" buffer into the slot
  2. gl.vertexAttribPointer(...) - "Connect attribute to whatever's in the slot"

Analogy

Think of gl.ARRAY_BUFFER as a USB port:

  • gl.bindBuffer() = Insert USB drive into port
  • gl.vertexAttribPointer() = Tell attribute "read from the USB port"
  • The attribute remembers which buffer it's connected to

Why This Design?

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.


How Data Flows Through WebGL

Complete Flow (Per Draw Call)

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!

Timeline of What's Set When

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 testing

During 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);

Common Pitfalls

1. Missing .js Extension in Imports

// ❌ 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";

2. Canvas in Wrong Place

<!-- ❌ Wrong - canvas in <head> -->
<head>
  <canvas id="canvas"></canvas>
</head>

<!-- ✅ Correct - canvas in <body> -->
<body>
  <canvas id="canvas"></canvas>
</body>

3. Wrong Bind Order

// ❌ 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);

4. Need a Local Server

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

Further Learning


Quick Reference

Key WebGL Functions

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

Coordinate Reference

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

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published