Skip to content

Grok WebGL2 and GLSL through spaced repetition and hands-on projects. A zero-to-hero guide covering the programmable geometry pipeline, state management, and 3D shader logic.

Notifications You must be signed in to change notification settings

GregStanton/webgl2-glsl-primer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

66 Commits
ย 
ย 

Repository files navigation

WebGL2 & GLSL primer:
A zero-to-hero, spaced-repetition guide

Status: Complete
Author: Greg Stanton

This primer takes the reader from zero (no knowledge of WebGL2 or GLSL) to hero (confidence in everything from low-level state management to procedural graphics production). If you want to engineer advanced 2D or 3D graphics, or understand how graphics are implemented under the hood, then this guide is for you.

๐Ÿง  Method

Fundamentals of WebGL2 and GLSL are introduced in a natural order, chunking concepts and syntax into atomic Q&A cards. But this is not a typical FAQ or a cheat sheet; this a sequence of guided lessons meant to be internalized, in order, with concepts that build cumulatively from the ground up. Once a card has been learned, it can be directly incorporated into spaced-repetition learning software like Anki, which leverages a scientifically-backed algorithm to ensure you remember what you learn, efficiently and permanently. Because of the small chunks, you can also make significant progress with just a few minutes of effort per day. To provide practice applying the ideas as soon as theyโ€™re introduced, hands-on projects are integrated throughout, with solution code.

๐Ÿ—บ๏ธ Scope

The material focuses on the programmable geometry pipelineโ€”creating form and color through code, logic, and mathematics. It covers the irreducible minimum required to build a 3D engine from scratch. While it establishes the foundation for all graphics tasks, it does not cover external asset management (like texture image loading), focusing instead on the state machine and the vertex/fragment logic essential for procedural graphics and tools like the RMF Engine designed by this primer's author. It's self contained, with intuitive explanations of the mathematical prerequisites. Recommendations on leveraging the covered skills are provided at the end, including an annotated list of links to high-quality projects and advanced resources.

Background

Before diving into the programmable geometry pipeline, we'll make sure we know the lay of the land, and that we have the prerequisite concepts and skills in place.

๐Ÿž๏ธ The landscape: Browser-based, hardware-accelerated graphics

While this primer focuses on WebGL2 (a.k.a. WebGL 2.0) and GLSL, it's helpful to understand how these technologies fit within the broader landscape, which includes two related Web APIs, each capable of both 2D and 3D graphics. The computational demands of 3D graphics means that these APIs are designed to leverage GPUs, efficient hardware that's shipped in virtually all devices manufactured since the late 2000s (phones, tablets, laptops, desktops).

  • WebGL and GLSL
    • Current lingua franca for hardware-accelerated graphics on the web
    • Wide employer demand, indicating extensive current usage
    • Mature support in all major browsers (since 2014 for 1.0, 2022 for 2.0), indicating a large ecosystem
  • WebGPU and WGSL
    • Emerging tech meant as an eventual WebGL replacement, for graphics, computation, and native-feeling apps (e.g., via Electron)
    • Nascent employer demand focused on innovation and adaptation of WebGL code
    • Initial support in all major browsers (as of mid-2025), with production deployments under development but impeded by significant inconsistencies, limitations, and bugs across browser implementations, operating systems, and hardware platforms

Common characteristics:
Both use the same graphics concepts, meaning that learning one makes it easier to learn the other. Both are significantly lower-level technologies relative to typical JavaScript development, as they offer direct interaction with the underlying GPU hardware.

Coexistence likely for 5โ€“10 years or more:
Libraries and applications with dual-backend architectures will likely be typical for at least five to ten years, and possibly longer, similar to the gradual rollout of WebGL and the coexistence of WebGL 1.0 and 2.0.

Updates to this section are welcome:
As the landscape evolves, pull requests that incorporate major updates to this background section will be welcome.

๐Ÿ“• Prerequisite topics

Prerequisites include both programming and math.

Programming: Knowledge of HTML and JavaScript is assumed.

Mathematics: Simple, concise explanations are provided for the topics below.

  • 3D primitives, including triangle strips and triangle fans
  • Matrix representations of geometric transformations
  • Homogeneous coordinates in projective geometry
  • Transforms in the standard 3D rendering pipeline

๐Ÿ“– Prerequisite explanations

This section explains the mathematical prerequisites at the level of detail we will need, with references for anyone desiring additional detail.

3D primitives (drawing modes)

The image below is sufficient for understanding WebGL drawing modes (shape โ€œkindsโ€ in p5.js):

A diagram illustrating the meaning of each drawing mode available in WebGL, including the following: `gl.POINTS`, `gl.LINES`, `gl.LINE_STRIP`, `gl.LINE_LOOP`, `gl.TRIANGLES`, `gl.TRIANGLE_STRIP`, `gl.TRIANGLE_FAN`.

Attribution: โ€œAvailable WebGL shapesโ€ appears in A Brief Introduction to WebGL, by Martรญn Lamas.

Matrix transformations and homogeneous coordinates

Below, we explain the two concepts from higher-level math that we'll need. Anyone who is unfamiliar with vectors, or who desires more detailed explanations of the two concepts explained here, may consult an overview of the relevant math concepts in the online book Introduction to Computer Graphics, by David J. Eck.

  1. Matrix representations: Although we won't be multiplying matrices manually, it will still be helpful to have a procedural understanding of matrix multiplication, which is covered in this ten-minute YouTube video. It will also be helpful for you to know that matrix multiplication represents geometric transformations. For example, if you want to rotate the point $(1, 1)$ around the origin in the plane, you can accomplish this by multiplying a vector with components [1, 1] by a rotation matrix. It's not necessary to know how to define matrices for rotations or other transformations; it's enough to know that multiplying by a matrix accomplishes a transformation. Specific APIs for programming matrix operations are not assumed.

  2. Homogeneous coordinates: Homogeneous coordinates are to projective geometry what Cartesian coordinates are to Euclidean geometry. This is a very cool idea that allows us to represent not just linear transformations (like scaling, rotating, shearing) via matrix multiplication, but also affine transformations (like translations), and even perspective projections (which make distant objects appear smaller). This is accomplished by including one extra coordinate, allowing us to represent both points (which can be translated) and directions (which cannot). It works as follows. When w is 0, translation vectors are annihilated (multiplied by zero), so [x, y, z, 0] represents the direction [x, y, z]. When w is 1, the translation vectors are preserved (multiplied by 1), so [x, y, z, 1] represents the point [x, y, z]. When w is a general nonzero value, the vector [x, y, z, w] represents the 3D point [x / w, y / w, z / w].

Overview of coordinate systems

Itโ€™s enough to understand the significance of each source and target space, from local to screen space, and to know the sequence of transformations between them. The diagram below contains the essentials.

A diagram showing the standard sequence of 3D graphics transforms, from local to world space (via the model matrix), from world to view space (via the view matrix), from view space to clip space (via the projection matrix), and from clip space to screen space (via the viewport transform).

Attribution: coordinate_systems.png by Joey de Vries appears in Coordinate Systems and is licensed under CC BY 4.0.

The basic role of each space is indicated by the diagram. An example will clarify this further, so let's imagine we are drawing a model car in 3D.

  • Local space: It's easiest if we can design one tire, centered at the origin. This is local space.
  • World space: Then we move to the space where the car is, so we can attach the wheel in four places. This is world space.
  • View space: To view our car, we move into the viewer's space, with the viewer's eye (or camera) being the origin. This is view space.
  • Clip space: To show what the viewer sees, we need to clip the space, leaving only what's in front of them. This is clip space.
  • Screen space: Finally, we need to display what the viewer sees on an actual 2D screen (in a viewport). This is screen space.

While this explanation is sufficient for our purposes, additional details may be found in Projection and viewing in Eck, or Coordinate Systems in the online book Learn OpenGL, by Joey de Vries.

Normalized device coordinates

We'll be directly dealing with normalized device coordinates early on. WebGL automatically converts clip-space coordinates to normalized-device coordinates, prior to applying the viewport transform. By making coordinates range between -1 and 1, it becomes simple to stretch them to match the dimensions of the viewport.

A cubic space, with a coordinate system whose origin is at the center of the cube. A horizontal axis points right, a vertical axis points up, and a depth axis points away. Values along each axis range between -1 and 1.

Attribution: Image of NDC space (referred to as โ€œclipspaceโ€ in original source) appears in WebGL model view projection - Web APIs | MDN and is licensed under CC BY SA 4.0.

๐Ÿ’ป Programming tips

You may find the following knowledge and experience helpful:

  • The notion of a statically typed language: It's enough to know that in some languages, it's necessary to explicitly declare variable typesโ€” like bool for a Boolean (true or false), or int for an integer ($\ldots, -2, -1, 0, 1, 2, \ldots$), or float for real numbers (decimal numbers). This may include syntax like const int myInteger = 10; instead of const myInteger = 10;
  • Experience creating graphics with a high-level library like p5.js: You can dive directly into WebGL2 & GLSL if you like to start with the nitty gritty. However, if you like to start by practicing the high-level concepts, such as making a 3D shape out of triangles without lots of low-level detail, then starting with a library like p5.js is a great option. In that case, this introduction to coding with p5.js from The Coding Train is an excellent choice.

๐Ÿ“ Anki tip: Learning lists

The cards in these notes sometimes have a full list as an answer. Lists tend to be more cognitively demanding and can disrupt mental flow. To mitigate this effect, the list cards include hints to make them easier, but you can customize the approach to your own background using the following list-learning principles:

  • Create cards explaining how each list item connects conceptually to the next item (essentially creating a mental linked list, a form of elaborative encoding from cognitive science)
  • Include a hint explaining how to chunk a longer list into only 3โ€“4 items (chunks can be conceptual, or arbitrary, as with phone numbers)
  • Add a single acronym or a mnemonic phrase, especially for ordered lists with seemingly arbitrary names (e.g., in biology, "Do kings play chess on fine green silk?" is a mnemonic for domain, kingdom, phylum, class, order, family, genus, species)
  • Implement lists using cloze deletion, especially if the above techniques prove difficult, in software like Anki (e.g., create cards where all items are revealed except for one)

Hints should be included on the back of a card (along with the answer), rather than the front. They serve as a reminder of how to internalize the idea, in case you fail to retrieve it from memory. If you rely on a hint on the front of a card, you may have trouble remembering it without that extra cue.

Introduction The Official WebGL Logo OpenGL Shading Language logo (Unofficial)

As with all sections of this primer, the current introductory section is self contained. Since the concepts covered here are foundational, sources are provided. Anyone hungry for additional context on subsequent sections will be well served by the MDN WebGL Reference, the OpenGL ES 3.0 Specification, and The OpenGL ESยฎ Shading Language 3.00.6.

Image attributions: โ„ข/ยฎKhronos Group, Public domain; Jim McKeeth, CC BY-SA 4.0; both via Wikimedia Commons

Shader basics

We begin by defining basic terms, and the core software units that process our geometry.

Q: What are the geometric primitives in WebGL?

A: Points, lines, and triangles. (Typically, itโ€™s all triangles.)

Source: WebGL2 Fundamentals, WebGL2 Points, Lines, and Triangles, Geometric primitive - Wikipedia

Q: What mathematical concept unifies points (0D), lines (1D), and triangles (2D)?

A: Each of these is a simplex, the simplest n-dimensional shape in its respective dimension.

Note: This term is not commonly used in graphics, but it's fundamental in mathematics, which is excellent at unifying seemingly disparate ideas.

Source: Simplex - Wikipedia

Q: In computer graphics, what is a vertex?

A: As in geometry, a vertex is one of a set of points that defines a shape (e.g. the three corners of a triangle). A vertex may have additional attributes for rendering (drawing), such as a color.

Source: Vertex (computer graphics) - Wikipedia, Vertex (geometry) - Wikipedia

Q: In computer graphics, what is a pixel?

A: Itโ€™s the smallest visual element on a screen. (Itโ€™s also known as a โ€œpicture element,โ€ analogous to a chemical element in the periodic table). Itโ€™s usually a tiny square.

Source: Pixel - Wikipedia, Chemical element - Wikipedia

Q: In computer graphics, what is a fragment?

A: It's a potential pixel. (For example, if part of a line is behind an opaque triangle, the obscured fragments of that line will be discarded, and won't end up coloring a pixel on the screen.)

Source: Fragment (computer graphics) - Wikipedia

Q: What two pieces of code comprise a WebGL program? Name them.

A: A vertex shader and a fragment shader.

Source: WebGL2 Fundamentals

Q: What does a vertex shader do?

A: It computes a vertex position.

Source: WebGL2 Fundamentals

Q: What does a fragment shader do?

A: It computes a fragment color.

Source: WebGL2 Fundamentals

Q: Conceptually, the execution of a vertex or fragment shader behaves like what standard programming structure? (A loop, an object, or a function?)

A: A function.

Note: While WebGL wraps this code in a shader object, the code itself defines a single function.

Source: WebGL2 Fundamentals

Q: If a shader only computes one vertex position or one fragment color, why does a WebGL program consist of only two shaders?

A: The vertex shader is executed for every single vertex, and the fragment shader is executed for every single fragment.

Source: WebGL2 Fundamentals

Software and hardware

Now we zoom out, to understand the context in which our shaders are situated.

Q: Is WebGL a language or an API?

A: It's an API. (It allows certain graphics features to be accessed through JavaScript.)

Source: WebGL - Wikipedia

Q: What software design pattern best describes the behavior of WebGL?

A: A state machine. (You set a state, and it persists until changed.)

Q: What is a state machine?

A: A mathematical model of computation defined by a list of states, initial values for those states, and the inputs that trigger each transition.

Source: Finite-state machine - Wikipedia

Q: In WebGL2, what language is used specifically to code vertex shaders and fragment shaders?

A: GLSL (OpenGL Shading Language)

Note: More precisely, WebGL2 uses GLSL ES 3.00.

Source: WebGL2 Fundamentals, OpenGL Shading Language - Wikipedia

Q: In both WebGL and OpenGL, what does "GL" stand for?

A: Graphics Library

Source: WebGL - Wikipedia, OpenGL - Wikipedia

Q: Must variable and function declarations have a declared type in GLSL?

A: Yes.

Source: WebGL2 Fundamentals, Type system - Wikipedia, The OpenGL ESยฎ Shading Language 3.00.6 (page 22)

Q: What general-purpose language is the syntax of GLSL patterned after?

A: C

Source: WebGL2 Fundamentals, OpenGL Shading Language - Wikipedia

Q: What hardware component does WebGL run on?

A: The GPU (graphics processing unit)

Source: WebGL2 Fundamentals, Graphics processing unit - Wikipedia

Q: What are cores of a CPU or GPU? Answer with a simple analogy.

A: Cores are like independent brains.

Q: What's the main difference between the cores of CPUs (central processing units) and the cores of GPUs (graphics processing units)?

A: CPUs have a few versatile cores, and GPUs have thousands of specialized cores.

Hint: A CPU core is like a chef (few in number, versatile), whereas a GPU is like a line cook (great in number, specialized).

Q: What's the benefit of running shaders on GPUs?

A: A shader that computes a single position or color can be run for thousands of vertices or fragments in parallel.

Hint: If you have ten chefs plating dishes vs. a thousand line cooks plating dishes, which will win a race to plate one million dumplings?

Pipeline basics

Now that we understand the basic idea of shaders, and key aspects of the software and hardware that power them, we consider how shader execution is organized.

Q: The programmable geometry pipeline consists of which two interacting sequences?

A: The coordinate pipeline (mathematical spaces) and the execution pipeline (hardware stages).

Q: In 3D graphics, what is the standard sequence of spaces in the coordinate pipeline? List them in order.

A:

  1. Local space (coordinates relative to an object's origin)
  2. World space (coordinates relative to the origin of the world in which objects are placed)
  3. View space (coordinates relative to the camera/eye)
  4. Clip space (coordinates accounting for the eye's field of vision)
  5. Screen space (coordinates for the physical viewport)
Q: In 3D graphics, what is the standard sequence of transforms in the coordinate pipeline? List them in order, with source and target spaces.

A:

  1. Model transform: local $\to$ world
  2. View transform: world $\to$ view
  3. Projection transform: view $\to$ clip
  4. Viewport transform: clip $\to$ screen

Note: A technical distinction must be made regarding the viewport transform's source space. When we cover the details in separate cards, this will be explained.

Q: What is rasterization?

A: The process of converting vector geometry (points, lines, triangles) into fragments.

Q: In WebGL, what are the main stages of the execution pipeline? List them in order.

A:

Vertex shader (positions the geometry)
$\to$ Rasterization (converts vector geometry into fragments)
$\to$ Fragment shader (computes the color of each fragment)
$\to$ Fragment processing (determines how fragments translate into pixels)

Note: When we cover the details, we'll reveal what's inside the "black box" between the vertex shader and rasterization (see the diagram below).

vertex shader  -->  rasterization  -->  fragment shader  -->  fragment processing
                ^
          ("black box")
Q: Does the coordinate pipeline span the entire execution pipeline?

A: No. The coordinate pipeline finishes just before rasterization begins.

Hint: The viewport transform at the end of the coordinate pipeline gets geometry into screen space; right after that, it's possible to identify fragments of primitives that cover particular pixels.

Access syntax

Now we learn how to access the world of WebGL2 that we just described.

Q: In the DOM, what HTML element provides the drawing surface for WebGL?

A: The <canvas> element.

Q: How do we access the WebGL2 API?

A: canvas.getContext('webgl2')

Q: What does canvas.getContext('webgl2') return?

A: A WebGL2RenderingContext

Q: A WebGL2RenderingContext is often given what abbreviated name in code?

A: gl

Q: What is the 2D version of WebGL2RenderingContext?

A: CanvasRenderingContext2D.

๐ŸŽจ Hello canvas

Itโ€™s time to make our first project! We just need to learn a few additional concepts.

Colors and buffers

Q: What color space is used by the WebGL context?

A: RGBA (red, green, blue, alpha)

Q: What is the valid range for color values in WebGL (red, green, blue, and alpha)?

A: 0.0 to 1.0 (floating point numbers).

Q: In a WebGL RGBA color, what value of A (alpha) indicates full opacity?

A: 1.0

Q: In a WebGL context, what function sets the canvas color? Include any parameters.

A: gl.clearColor(r, g, b, a)

Q: What does gl.clearColor(r, g, b, a) do?

A: It sets the "clear color" state but does not change the colors on the screen.

Q: In WebGL, what basic function erases buffers and assigns them preset values? Include any parameters.

A: gl.clear(mask)

Q: What are the standard buffers that gl.clear() can affect?

A: Color, Depth, Stencil

Q: When calling gl.clear(), what constant do we pass to clear the color buffer?

A: gl.COLOR_BUFFER_BIT

Q: When calling gl.clear(), what constant do we pass to clear the depth buffer?

A: gl.DEPTH_BUFFER_BIT

Q: Why does gl.clear() accept a bitmask as its parameter?

A: To allow multiple buffers to be cleared simultaneously via a bitwise OR operation. (This is an optimization that eliminates the need for multiple function calls.)

Q: To clear multiple buffers buffer1 and buffer2 simultaneously with gl.clear(), what syntax is used?

A: Syntax: gl.clear(buffer1 | buffer2). This uses the bitwise OR operator: | (a single pipe character).

Project 1: Colored canvas

yellow canvas

Problem: Set up an index.html file and a JavaScript file. Make a canvas, get the WebGL context, and use it to set the canvas to a color of your choosing.

Solution:
<!DOCTYPE html>  
<html lang="en">  
<head>  
    <meta charset="UTF-8">  
    <meta name="viewport" content="width=device-width, initial-scale=1.0">  
    <title>Yellow canvas</title>  
</head>
<body>  
    <canvas id="yellow-canvas" width="400" height="400">  
      A canvas painted yellow.  
    </canvas>
    <script src="yellow-canvas.js"></script>  
</body>  
</html>  
const canvas = document.getElementById('yellow-canvas');  
const gl = canvas.getContext('webgl2');  
const yellow = [243 / 255, 208 / 255, 62 / 255, 1];

gl.clearColor(...yellow);  
gl.clear(gl.COLOR_BUFFER_BIT);  

๐Ÿ”บHello triangle

Now we'll work toward getting a triangle on the screen. This will take some effort, since we're going to make sure we understand all the low-level boilerplate.

Starting the data bridge (getting CPU data onto the GPU)

Q: In WebGL, what does VBO stand for?

A: Vertex Buffer Object

Q: In WebGL, what is the general purpose of a VBO?

A: To store data in the GPU's memory.

Q: In WebGL, what sorts of data are commonly stored in a VBO?

A: Vertex attribute data like positions, normals, colors, and texture coordinates.

Q: In WebGL, data in a VBO is stored in what data format?

A: Binary

Q: In WebGL, what does VAO stand for?

A: Vertex Array Object

Q: In WebGL, what is the purpose of a VAO?

A: Recording how to read data from the VBOs. (This is typically vertex-attribute data, hence the name.)

Q: In WebGL, what does the term binding mean?

A: Setting an object (e.g. a VBO) as the "active" value for a particular state in the WebGL state machine.

Q: In WebGL, what is the order of operations for creating and configuring the VAO and VBO?

A:

  1. Create and Bind the VAO.
  2. Create and Bind the VBO.
  3. Upload buffer data.
  4. Configure attributes (tell VAO how to read the VBO).

Hint: Since the WebGL context is a state machine, it needs a place to define state (VAO) before it can organize the data values (VBO). Once these are in place, we need to upload data, then tell the VAO how to read it.

Q: In WebGL, what syntax creates a Vertex Array Object?

A: gl.createVertexArray() (this function does not take parameters)

Q: In WebGL, what syntax binds a VAO?

A: gl.bindVertexArray(vao)

Q: In WebGL, what syntax creates a buffer (VBO)?

A: gl.createBuffer() (this function does not take parameters)

Q: In WebGL, what syntax binds a buffer?

A: gl.bindBuffer(target, buffer)

Q: What are the two most common targets for gl.bindBuffer?

A: gl.ARRAY_BUFFER, gl.ELEMENT_ARRAY_BUFFER

Q: What kind of data is usually bound to gl.ARRAY_BUFFER?

A: Vertex attribute data (e.g. position, normal, color, texture data)

Q: What kind of data is usually bound to gl.ELEMENT_ARRAY_BUFFER?

A: Index data (indicating which vertices to connect)

Q: Can you give a concrete example to indicate the purpose of gl.ELEMENT_ARRAY_BUFFER?

A: Suppose you want to create a rectangle from four vertices. This needs to be created out of triangles, and there are two ways to triangulate a rectangle. The element array buffer can be used to specify the triangulation, by indicating which vertices should be connected.

Q: What syntax sends data to the currently bound buffer?

A: gl.bufferData(target, data, usage)

Q: In gl.bufferData(target, data, usage), the data argument usually has what type?

A: Float32Array (a JavaScript typed array)

Q: In gl.bufferData, if the geometry will not change after it is uploaded, what usage constant should be passed?

A: gl.STATIC_DRAW

Q: In WebGL, an attribute will not be used unless you explicitly turn it on, using what function? Name the function (donโ€™t specify any parameters).

A: gl.enableVertexAttribArray()

Q: In WebGL, what function tells the VAO how to interpret the data in the currently bound VBO? Name the function (donโ€™t specify any parameters).

A: gl.vertexAttribPointer()

Coordinates expected by WebGL

Q: In WebGL, vertex coordinates in a VBO are expected to be in what space?

A: Local space. (Also known as model space.)

Q: In WebGL, vertex coordinates output by a vertex shader are expected to be in what space?

A: Clip space.

Q: In a WebGL vertex shader, if you define an attribute as a vec4 but only provide $(x, y)$ data from the buffer, what values are automatically assigned to $z$ and $w$?

A: z = 0.0, w = 1.0.

Hint: Recall that a w-coordinate of 1 makes the vertex a point, rather than a direction, i.e. it is affected by translations.

Fixed-function coordinate transforms

Q: When referring to a graphics pipeline, what does the term โ€œfixed functionโ€ mean?

A: It refers to operations in the pipeline that are not programmable by the user, as they are pre-programmed into the hardware (or driver).

Q: In WebGL, vertex coordinates in clip space are automatically converted to what space, after the vertex shader runs?

A: NDC space (normalized device coordinates).

Q: In WebGL, what operation sends clip space to NDC space? Name it.

A: Perspective division.

Q: In WebGL, what operation sends clip space to NDC space? Specify the input and output.

A: $(x, y, z, w) \mapsto (\frac{x}{w}, \frac{y}{w}, \frac{z}{w})$

Q: In WebGL, what is the range of $x$, $y$, and $z$ in NDC space?

A: -1.0 to 1.0.

Q: In WebGL Normalized Device Coordinates (NDC), where is the origin $(0,0,0)$?

A: The center.

Q: In WebGL Normalized Device Coordinates (NDC), in which direction do the $x$, $y$, and $z$ axes point?

A: Directions: $x$ points right, $y$ points up, and $z$ points away (directionally, into the screen).

Hint: The xy-plane follows mathematical conventions ($x$ points right and $y$ points up). However, itโ€™s a left-handed system.

Q: In WebGL, the depth buffer typically contains values in what range?

A: 0.0 to 1.0

Hint: These are non-negative, as weโ€™d expect of depth values, since "depth" implies a distance measured in only one direction. The depth range may be customized, but this is rarely necessary.

Q: In WebGL, the values in the depth buffer are determined by what transformation? Name it.

A: The viewport transform.

Q: In WebGL, what are the source and target spaces of the viewport transform?

A: NDC space $\to$ screen space.

Note: The source space is sometimes identified informally as clip space, which is the last space the user deals with prior to application of the viewport transform. However, 4D clip space is converted automatically (during a fixed-function stage) to 3D NDC space before the viewport transform is applied.

Q: In web graphics, what is the difference between a viewport and a canvas, if any?

A: The viewport is the rectangular portion of the canvas that is rendered to (e.g., if a canvas is too large to show, the viewport may be smaller and may have scroll bars).

Q: Conceptually, what does the viewport transform do in WebGL?

A: It scales and translates NDC space to match the dimensions and position of the viewport, and it converts z-values from NDC space (in $[-1, 1]$) to depth values (in $[0, 1]$ by default).

Q: What syntax explicitly configures the viewport transform?

A: gl.viewport(x, y, width, height), where the x and y parameters are the lower-left corner of the viewport, and the other parameters are the viewport's dimensions.

Q: In WebGL, the x and y parameters of gl.viewport() are measured in pixels relative to which corner of the canvas?

A: The bottom-left corner.

Hint: This is the standard Cartesian origin, but it differs from the standard HTML coordinate system (which starts at the top-left).

Pipeline details

Q: In WebGL, what steps happen automatically in the execution pipeline, after the vertex shader and before perspective division? Name but do not describe them.

A:

  1. Primitive assembly
  2. Clipping
Q: In the WebGL execution pipeline, why might primitive assembly be necessary? Give a simple example.

A: Two vertices may be interpreted as disconnected points or a single line segment.

Q: In WebGL, why might primitives be assembled prior to clipping? Answer with a simple example.

A: Imagine a paper triangle is pinned to a rectangular corkboard, and one of its vertices extends off the corkboard's edge. You clip the offending portion with scissors. This gives the triangle an extra edge and two new vertices. WebGL adds extra vertices like these automatically, but that requires it to know the clipped vertex was part of a triangle.

Q: In WebGL, why might clipping occur prior to perspective division?

A: There's no sense in performing calculations for vertices that won't make it into the final scene. (In addition to efficiency, there are more significant problems that need to be avoided. So other acceptable answers include a wrap-around effect, where dividing by a negative $w$ causes objects behind the camera to be positioned in front of it, and division by zero, which could happen for example if a vertex is located at the eye's/camera's position.)

Q: In WebGL, what steps happen between the vertex shader and rasterization? List them in order.

A:

  1. Primitive assembly (primitives must be assembled before they can be clipped)
  2. Clipping (clipping prior to perspective division eliminates unnecessary computation)
  3. Perspective division (this converts 4D coordinates to familiar 3D coordinates)
  4. Viewport transform (this maps 3D vector geometry to the screen so it can be rasterized, i.e. fragmented according to the pixels it covers)

Note: The full execution pipeline is visualized below. The stages from this card are shown in all capital letters.

vertex shader
  --> PRIMITIVE ASSEMBLY
  --> CLIPPING
  --> PERSPECTIVE DIVISION
  --> VIEWPORT TRANSFORM
  --> rasterization
  --> fragment shader
  --> fragment processing

Project 2: Create and bind VBO and VAO, supply triangle data

This project is continued from Project 1.

Problem: Extend your yellow-canvas.js program so that it defines a triangle as a flat array of three $(x, y)$ vertices. Create and bind a VAO and VBO, and upload the triangle data to the VBO. Assume the triangle data will not change after itโ€™s uploaded. (We won't render the triangle yet. We'll do that in the next project.)

Solution:
// CANVAS
const canvas = document.getElementById('yellow-canvas');
const gl = canvas.getContext('webgl2');
const yellow = [243 / 255, 208 / 255, 62 / 255, 1];

gl.clearColor(...yellow);
gl.clear(gl.COLOR_BUFFER_BIT);

// TRIANGLE
const TAU = 2 * Math.PI;
const r = 2 / 3;
const t0 = TAU / 4;
const dt = TAU / 3;

const triangleVertices = new Float32Array([
  r * Math.cos(t0), r * Math.sin(t0), 
  r * Math.cos(t0 + dt), r * Math.sin(t0 + dt), 
  r * Math.cos(t0 + 2 * dt), r * Math.sin(t0 + 2 * dt),
]);

// STATE MANAGEMENT: VAO AND VBO
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
const vbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, triangleVertices, gl.STATIC_DRAW);

GLSL ES 3.00 syntax

Q: In GLSL, what is the syntax to declare a floating-point variable named alpha and initialize it to 1.0?

A: float alpha = 1.0;

Hint: It is very similar to C or Java. Semicolons are required.

Q: In GLSL, vec2, vec3, and vec4 refer to vectors whose components have what data type?

A: float (floating point number)

Q: In GLSL, a type (such as vec4) is also a ____________.

A: constructor

Q: In GLSL, how can you create a vec4 with components $(0.1, 0.2, 0.3, 0.4)$?

A: vec4(0.1, 0.2, 0.3, 0.4)

Q: In GLSL, if pos is a variable of type vec2, how do you create a vec4 using pos for the first two components, 0.0 for z, and 1.0 for w?

A: vec4(pos, 0.0, 1.0)

Attribute interpolation

Q: In mathematics, what is interpolation?

A: Interpolation refers to determining an intermediate value between two given values (e.g. finding a position between points A and B that's 40% of the distance from A to B).

Q: What's an example of an attribute that needs to be interpolated between vertices?

A: A color. (Other typical examples are normal vectors or texture coordinates.)

Q: Why might a color need to be interpolated between vertices?

A: If two vertices each have their own color, then this color may need to be interpolated in order to associate a color with a fragment in between them.

Shader syntax

Q: In a WebGL2 shader source string, what must the very first line be?

A: #version 300 es

Hint: This refers to GLSL ES 3.00.

Q: In a WebGL2 shader, what is wrong with the following code?
const shader = `
#version 300 es
// more code here...
`;

A: There is a newline after the backtick, creating a blank line above the version specification. It should look like this instead:

const shader = `#version 300 es
// more code here...
`;
Q: In GLSL, which shader stage requires an explicit precision declaration?

A: The fragment shader.

Hint: If vertices arenโ€™t in the right place, things go wrong, so high precision is mandated for vertex shaders, but lower precision is allowed for fragment shaders, e.g. to avoid draining battery on older mobile devices.)

Q: In a GLSL shader, whatโ€™s the syntax to declare that floats should have high precision?

A: precision highp float;

Q: In a GLSL shader, where does the line of code go that sets the precision of floats?

A: The standard location is at the very top (underneath the line that specifies the GLSL version).

Q: In WebGL, a shader begins with the execution of what function? Whatโ€™s the syntax for it?

A: void main() {/* code goes here */} (as the syntax indicates, this function takes no parameters and returns no value)

Q: In GLSL, whatโ€™s the general syntactical term for the in and out keywords?

A: They are storage qualifiers.

Q: What do in and out storage qualifiers indicate in a vertex shader?

A: in = vertex attribute. out = data to be interpolated (previously a varying).

Q: What do in and out storage qualifiers indicate in a fragment shader?

A: in = interpolated data (previously a varying). out = final fragment color.

Q: In GLSL ES 3.00, what is the syntax for setting up a โ€œportโ€ for a shader to receive data?

A: layout(location = 0) in <type> <variableName>

Q: In GLSL ES 3.00, what is the meaning of layout(location = 0), in the line layout(location = 0) in <type> <variableName>?

A: โ€œReceive data at location 0.โ€

Q: In GLSL ES 3.00, what is the meaning of <variableName>, in the line layout(location = 0) in <type> <variableName>?

A: This is the name of the variable that contains the data received at location 0.

Q: In GLSL ES 3.00, where does the line layout(location = 0) in <type> <variableName> go inside a shader?

A: It goes in the global scope of the shader, before main().

Q: What special built-in variable must the vertex shader write to?

A: gl_Position

Q: In a GLSL vertex shader, what is the data type of the built-in variable gl_Position?

A: vec4

Q: In GLSL ES 3.00, what is the syntax to define the output color variable in a fragment shader?

A: out vec4 fragColor; (You can name the variable whatever you want, but fragColor is conventional.)

Q: In GLSL ES 3.00, where does the code defining the output color variable in a fragment shader go?

A: It goes in the global scope of the shader, before main().

Finishing the data bridge (enabling and configuring attributes)

Q: An attribute will not be used unless itโ€™s explicitly turned on using what syntax?

A: gl.enableVertexAttribArray(index)

Q: In gl.enableVertexAttribArray(index), what does index represent?

A: The location of the attribute that will receive the data in the shader (set by layout(location = index)).

Q: Whatโ€™s the signature of gl.vertexAttribPointer()? (Parameter list and return value.)

A:
gl.vertexAttribPointer(index, size, type, normalized, stride, offset)
Return value: None ( undefined).

Hint: Mentally chunk the parameters into three pairsโ€”index, size; type, normalized; stride, offset.

Q: In gl.vertexAttribPointer(), what does the index parameter represent?

A: The location of the attribute that will receive the data in the shader (set by layout(location = index)).

Q: In gl.vertexAttribPointer(), what does the size parameter represent?

A: The number of components per vertex (e.g., 2 for a vec2, 3 for a vec3).

Q: In gl.vertexAttribPointer(), what does the type parameter represent?

A: The data type of the array components.

Q: In gl.vertexAttribPointer(), the type parameter is typically set to what value?

A: gl.FLOAT

Q: What precise data type is indicated by gl.FLOAT?

A: A 32-bit (IEEE) floating point number.

Q: A gl.FLOAT consists of how many bytes?

A: 4 bytes

Hint: A gl.FLOAT is a 32-bit floating-point data type. A byte consists of 8 bits.

Q: In gl.vertexAttribPointer(), what does the normalized parameter represent?

A: A boolean value indicating whether integer data should be normalized to $[-1, 1]$ or $[0, 1]$ when converted to a float (has no effect for floats, so it's typically set to false in that case, as enabling normalization would have no effect).

Q: In gl.vertexAttribPointer(), what is a basic use case for the normalized parameter?

A: If RGB values for a color are provided in the range $[0, 255]$ (with a type of gl.UNSIGNED_BYTE), setting the normalized parameter to true will automatically convert that data to floats in the required $[0.0, 1.0]$ range for color data.

Q: In gl.vertexAttribPointer(), what does the stride parameter represent?

A: Byte offset (distance in bytes) between the start of one vertex attribute and the next one of the same type. (Equivalently, the number of bytes used to store attributes corresponding to one vertex

Hint: Imagine attributes are stored like x0, y0, u0, v0, x1, y1, u1, v1โ€ฆ The stride tells WebGL that the memory occupied by x0, y0, u0, v0 corresponds to one vertex.

Q: What term do we use to describe attribute data like x0, y0, u0, v0, x1, y1, u1, v1โ€ฆ in which attributes of different kinds are stored together in the same buffer?

A: Interleaved

Q: What term do we use to describe attribute data like x0, y0, x1, y1,โ€ฆ in which attributes in a buffer all have the same kind (e.g. theyโ€™re all positions)?

A: Tightly packed

Hint: If only positions are represented, then that means thereโ€™s zero space between positions (e.g. we donโ€™t have position data, then color data, then position data, etc.).

Q: What value do we give stride when calling gl.vertexAttribPointer(), if we want data to be tightly packed?

A: 0

Hint: This is a special case. If 0 were the byte offset from the start of one vertex position to the start of the next (for example), thatโ€™d mean thereโ€™s no position data. So WebGL interprets zero to mean โ€œtightly packed,โ€ (e.g., zero bytes between the end of one vertex position and the start of the next).

Q: If stride is set to zero when calling gl.vertexAttribPointer(), how can WebGL determine the byte offset to get from one attribute to the next?

A: WebGL interprets a stride of 0 to mean the data is tightly packed (e.g. all position data, with no color data in between). It then automatically calculates the correct byte offset based on the size and type parameters.

Q: When calling gl.vertexAttribPointer(), suppose size is set to 3, type is set to gl.FLOAT, and stride is set to zero. WebGL will automatically calculate that the byte offset between attributes is equal to what value?

A: A stride of zero means the data is tightly packed, so we have attributes with three components packed right next to each other. A gl.FLOAT consists of four bytes. So, the byte offset is 3 components $\times$ 4 bytes / component = 12 bytes.

Q: Roughly, when might it be useful to use tightly packed attributes in a WebGL array buffer?

A: Using tightly packed attributes means that all positions would go into one array buffer, all colors would go into another, etc. This can be useful for dynamic geometry, e.g. when positions need to be updated but not colors.

Q: Roughly, when might it be useful to use interleaved attributes in a WebGL array buffer?

A: This keeps all data for a single vertex close together in memory, which can be more efficient for static geometry, e.g. where itโ€™s not necessary to update positions but keep colors the same. (Interleaved attributes also make it possible to deal with just a single buffer.)

Q: In gl.vertexAttribPointer(), what does the offset parameter represent?

A: The byte offset from the start of the buffer to the first component of the first vertex attribute.

Q: When gl.vertexAttribPointer() is called, how does WebGL know which VBO to read data from?

A: It uses whichever buffer is bound to the gl.ARRAY_BUFFER when gl.vertexAttribPointer() is called.

Q: What object stores the configuration set by gl.vertexAttribPointer() and gl.enableVertexAttribArray()?

A: The Vertex Array Object (VAO).

WebGL2 shader compilation

Q: In WebGL, what are the high-level steps to setting up a shader object? Answer in words.

A:

  1. Create
  2. Upload (the GLSL source code)
  3. Compile
  4. Check (the compile status)
  5. If compiling failed, Throw the error and Delete the shader.

Hint: This list can be chunked into two stages.

  1. As with setting up a VBO, we need to Create the object before we Upload to it.
  2. Since this is a program, we then need to Compile it, Check for errors, and then Throw errors and Delete if needed.

Q: In WebGL, whatโ€™s the syntax for creating a shader?

A: gl.createShader(type)

Q: What are the two types of shaders passed to gl.createShader()?

A: gl.VERTEX_SHADER and gl.FRAGMENT_SHADER.

Q: In WebGL, whatโ€™s the syntax to upload the GLSL source code string to a shader object?

A: gl.shaderSource(shader, sourceString)

Q: In WebGL, whatโ€™s the syntax to compile a shader?

A: gl.compileShader(shader)

Q: After compiling a shader, what syntax checks if it succeeded?

A: gl.getShaderParameter(shader, gl.COMPILE_STATUS)

Q: What is the return type of gl.getShaderParameter(shader, gl.COMPILE_STATUS)?

A: Boolean

Q: If gl.getShaderParameter(shader, gl.COMPILE_STATUS) indicates an error has occurred, what syntax gets a string with information about the error?

A: Use gl.getShaderInfoLog(shader).

Q: In WebGL, why should you delete a shader object after it fails to compile?

A: If it fails to compile, itโ€™s garbage (useless). Deleting it prevents memory leaks (accumulation of useless memory).

Q: What function removes a shader object from GPU memory?

A: gl.deleteShader(shader)

WebGL2 program linking

Q: In WebGL, what does it mean to โ€œlinkโ€ a program?

A: Linking a program connects it to dependencies, resulting in a program thatโ€™s executable.

Q: Regarding executability, what is the difference between a shader object and a program object in WebGL?

A: A shader is an intermediate (unlinked) compiled stage. A program is linked and ready to run (like an .exe).

Q: In WebGL, what are the high-level steps to setting up a program object? Answer in words.

A:

  1. Create
  2. Attach (shaders)
  3. Link (program)
  4. Check (the link status)
  5. If linking failed, Throw (error) and Delete (the program).
Q: In WebGL, whatโ€™s the syntax for creating a program object?

A: gl.createProgram() (no parameters)

Q: In WebGL, whatโ€™s the syntax for attaching a shader to a program object?

A: gl.attachShader(program, shader) (attaches a vertex or a fragment shader)

Q: After attaching shaders to a program, what syntax connects them into a usable executable?

A: gl.linkProgram(program)

Q: After linking a WebGL program, how do you check if it succeeded?

A: gl.getProgramParameter(program, gl.LINK_STATUS)

Q: What is the return type of gl.getProgramParameter(program, gl.LINK_STATUS)?

A: Boolean

Q: If gl.getProgramParameter(program, gl.LINK_STATUS) indicates an error has occurred, what syntax gets a string with information about the error?

A: gl.getProgramInfoLog(program)

Q: In WebGL, why should you delete a program object after it fails to link?

A: If it fails to link, itโ€™s garbage (useless). Deleting it prevents memory leaks (accumulation of useless memory).

Q: What function removes a program object from GPU memory?

A: gl.deleteProgram(program)

Drawing

Q: In WebGL2, which function actually triggers the execution pipeline to run? Give two possible answers.

A: gl.drawArrays() or gl.drawElements().

Hint: Prior to calling one of these functions, you are just setting up state. These commands tell the GPU to actually process the data through the shaders. They're your "go" buttons.

Q: In the WebGL2 API, what's the signature of gl.drawArrays()?

A: gl.drawArrays(mode, first, count)
Return value: None ( undefined).

Q: In the WebGL2 API, what does "arrays" refer to in gl.drawArrays()?

A: As it carries out the drawing task, this function aggregates vertex attributes from multiple arrays (e.g. position, color, and texture arrays), assembling all data for vertex 1, then for vertex 2, and so on.

Q: In gl.drawArrays(mode, first, count), what are the possible values of the mode parameter? Answer with conceptual descriptions.

A: Disconnected points; disconnected lines, an open polyline, or a closed polyline; disconnected triangles, a triangle strip, or a triangle fan.

Hint: The mode parameter is analogous to the kind parameter in p5's beginShape(kind).

Q: In gl.drawArrays(mode, first, count), what are the possible values of the mode parameter? Answer with variable names.

A: gl.POINTS; gl.LINES, gl.LINE_STRIP, gl.LINE_LOOP; gl.TRIANGLES, gl.TRIANGLE_STRIP, gl.TRIANGLE_FAN.

Hint: The mode parameter is analogous to the kind parameter in p5's beginShape(kind).

Q: In gl.drawArrays(mode, first, count), what does the first parameter represent?

A: The starting index to read from, in the arrays of vertex attributes. (Usually 0.)

Q: In gl.drawArrays(mode, first, count), what does the count parameter represent?

A: The number of vertices to be processed (rendered).

Q: If you call gl.drawArrays(mode, 0, 36), how many times will the vertex shader execute?

A: 36 times.

Hint: The vertex shader runs once for every vertex specified in the count parameter.

Q: Before issuing a gl.drawArrays command, what must you tell the GPU, conceptually?

A: You must tell it which shader program to use.

Q: What syntax tells gl.drawArrays which shader program to execute?

A: gl.useProgram(program)

Hint: This tells the WebGL state machine: "For all subsequent draw calls, use this specific compiled executable."

Q: What's the main technical difference between gl.drawArrays() and gl.drawElements()?

A: gl.drawArrays() uses array buffers (bound to gl.ARRAY_BUFFER) and gl.drawElements() uses element array buffers (bound to gl.ELEMENT_ARRAY_BUFFER).

Q: What's a guideline for deciding between using gl.drawArrays() and gl.drawElements()?

A: Use gl.drawArrays() when there is little to no vertex sharing between primitives (e.g. six vertices for two triangles, each with its own vertices), and use gl.drawElements() otherwise (e.g. four vertices for two triangles that share a side).

Project 3: Make boilerplate helper and draw triangle

yellow canvas with an orange triangle in the center

Goal: Update yellow-canvas.js to render your existing triangle geometry in orange, on top of the yellow background.

Context: From Project 2, you already have the geometry (a Float32Array of 2D coordinates) in a VBO, and a VAO that is currently bound. Now you need to build the program to process that data.

Project Specifications:

  1. Helper Function: Create a function createProgram(gl, vsSource, fsSource) at the bottom of your file.
    • It must create two shaders and one program.
    • It must compile the shaders and check their compile status.
    • It must link the program and check its link status.
    • Constraint: If any check fails, throw an error and delete the faulty object to avoid memory leaks. Otherwise, return the program.
  2. Shader Source Code: Define two template strings, vsSource and fsSource.
    • Vertex Shader:
      • Accept an attribute position at location 0. Note that your buffer has 2 numbers per vertex, so this should be a vec2.
      • Output a gl_Position. (Hint: You will need to convert your vec2 input.)
    • Fragment Shader:
      • Declare the variable to output.
      • Output the color orange: vec4(1.0, 0.4, 0.0, 1.0).
  3. Execution:
    • Call your helper to create the program.
    • Tell WebGL to use this program.
    • Draw the triangle.
Solution:
// CANVAS
const canvas = document.getElementById('yellow-canvas');
const gl = canvas.getContext('webgl2');
const yellow = [243 / 255, 208 / 255, 62 / 255, 1];

gl.clearColor(...yellow);
gl.clear(gl.COLOR_BUFFER_BIT);

// TRIANGLE
const TAU = 2 * Math.PI;
const r = 2 / 3;
const t0 = TAU / 4;
const dt = TAU / 3;

const triangleVertices = new Float32Array([
  r * Math.cos(t0), r * Math.sin(t0), 
  r * Math.cos(t0 + dt), r * Math.sin(t0 + dt), 
  r * Math.cos(t0 + 2 * dt), r * Math.sin(t0 + 2 * dt),
]);

// SHADER SOURCE
const vsSource = `#version 300 es
layout(location = 0) in vec2 position;

void main() {
  gl_Position = vec4(position, 0.0, 1.0);
}
`;

const fsSource = `#version 300 es
precision highp float;
out vec4 fragColor;

void main() {
  fragColor = vec4(1.0, 0.4, 0.0, 1.0);
}
`;

// STATE MANAGEMENT: VAO AND VBO
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
const vbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, triangleVertices, gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);

// CREATE AND USE PROGRAM TO DRAW
const program = createProgram(gl, vsSource, fsSource);
gl.useProgram(program);
gl.drawArrays(gl.TRIANGLES, 0, 3);

// CREATION UTILITIES: SHADERS AND PROGRAM 
function createShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  
  // Check success
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    const shaderInfoLog = gl.getShaderInfoLog(shader);
    gl.deleteShader(shader);
    throw new Error(`Could not compile shader: ${shaderInfoLog}`);
  }
  return shader;
}

function createProgram(gl, vsSource, fsSource) {
  const vs = createShader(gl, gl.VERTEX_SHADER, vsSource);
  const fs = createShader(gl, gl.FRAGMENT_SHADER, fsSource);
  
  const program = gl.createProgram();
  gl.attachShader(program, vs);
  gl.attachShader(program, fs);
  gl.linkProgram(program);
  
  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    const programInfoLog = gl.getProgramInfoLog(program);
    gl.deleteProgram(program);
    throw new Error(`Could not link program: ${programInfoLog}`);
  }
  return program;
}

๐ŸงŠ Hello spinning cube

Time for some 3D action!

Uniforms and matrices in GLSL

Q: In GLSL, what storage qualifier is used for variables that remain constant for all vertices in a single draw call (e.g., a transformation matrix)?

A: uniform

Q: In WebGL, before you can set the value of a uniform, you must look up its address using what syntax?

A: gl.getUniformLocation(program, name)

Hint: Just as attributes have integer locations, uniforms have location objects.

Q: In gl.getUniformLocation(program, name), you must be sure that name has what data type?

A: A string. (Be sure to wrap the name of the uniform in single quotes, so that it's just a variable name, not an actual variable.)

Q: What do "row-major" and "column-major" mean when specifying matrices?

A: Imagine reading a matrix aloud to someone else. You'll either read it to them row by row (row-major order) or column by column (column-major order). (The same concept applies when storing a matrix in a flat array.)

Q: In GLSL, what data type represents a $4\times4$ matrix?

A: mat4 (a common shorthand for mat4x4)

Q: In WebGL and GLSL, what is the convention regarding matrix order (column-major or row-major), if any?

A: Always column major.

Hint: This is consistent with mathematical conventions, whereby the matrix of a transformation is built from vectors that are represented as columns.

Q: What is the order of multiplication when multiplying a matrix $M$ by a vector $v$, when following a column-major convention?

A: $Mv$

Hint: Since $v$ is a column, it must go on the right for the matrix product to be defined, in general.

Q: What is the order of multiplication when multiplying a matrix $M$ by a vector $v$, when following a row-major convention?

A: $vM$

Hint: Since $v$ is a row, it must go on the left for the matrix product to be defined, in general.

Q: In WebGL, if you want to set the value of a uniform variable declared as a mat4 in GLSL, what syntax do you use?

A: gl.uniformMatrix4fv(location, transpose, data)

Hint: It stands for "uniform Matrix 4 float vector," where "float vector" refers to the type of data used to specify the matrix.

Q: When calling gl.uniformMatrix4fv() in WebGL, what must the transpose argument always be?

A: false

Hint: The transpose parameter is kept for consistency with OpenGL, but WebGL requires this to be false, so that matrices are always in column-major order.

Q: When calling gl.uniformMatrix4fv() in WebGL, how is the data parameter typically specified?

A: It's typically specified as a Float32Array (it could also be a sequence of separate 32-bit floats).

Q: What function must you call before setting any uniforms?

A: gl.useProgram()

Q: Why must a program be in use before setting any uniforms?

A: Uniforms are global to the program object and are stored in that object, not in the global WebGL state. (WebGL needs to know which program's memory you intend to update.)

Q: WebGL clip space is left handed ($+z$ into screen). However, the popular glMatrix matrix library uses a right-handed system ($+z$ towards viewer), which aligns with standard mathematical conventions. Which matrix in glMatrix handles the conversion between them?

A: The projection matrix (it flips the $z$-axis).

GLSL matrix multiplication

Q: In GLSL, how do we compute the matrix-vector product $Mv$, where $M$ is a mat4 and $v$ is a vec4?

A: M * v

Q: In GLSL, how do we compute the matrix product $AB$, where $A$ is a mat4 and $B$ is a mat4? (Here, $AB$ refers to the standard matrix product, not an entrywise/Hadamard product.)

A: A * B

Q: In GLSL, what happens if you try to multiply vector * matrix (vector on the left)?

A: It treats the vector as a row vector.

Hint: This is valid syntax but usually not what we want, since WebGL adheres to column-major order.

3D-state management (depth and culling)

Q: In WebGL, what feature must be enabled to prevent background triangles from drawing on top of foreground triangles? Answer in words.

A: The depth test.

Q: What syntax enables the depth test in WebGL?

A: gl.enable(gl.DEPTH_TEST)

Q: Does setting gl.enable(gl.DEPTH_TEST) require an active program?

A: No.

Hint: Depth testing is part of the global context state, not the program state.

Q: When the depth test is enabled in WebGL, what update must you be sure to make every frame? Answer in words.

A: Clear the depth buffer. (This ensures that old data doesn't persist.)

Q: In computer graphics, what is "face culling"?

A: It's an optimization that avoids drawing faces that wouldn't be visible anyway (e.g. the back face of a cube).

Q: In WebGL, face culling is applied to triangles if they have what spatial relation to the camera?

A: The triangles are culled if they are facing away from the camera.

Hint: Imagine that you color a paper triangle red, but if someone flips it over, they'll see it's still white on the other side. That's the back face. WebGL also has a way of determining which face of a triangle is the front and which is the back.

Q: By default, WebGL determines a triangle is "front-facing" if its vertices are defined in what winding order?

A: Counter-Clockwise (CCW).

Hint: This is the positive orientation in the xy-plane (starting from the positive x-axis, this direction moves us through Quadrant I first).

Q: What syntax enables face culling in WebGL?

A: gl.enable(gl.CULL_FACE)

3D geometry definition (winding order)

Q: In WebGL, when defining the vertices of a 3D mesh (like a cube), in what winding order should you list the vertices for every face?

A: Counter-Clockwise (CCW).

Q: In WebGL, when determining the CCW winding order for a specific face of a 3D object, where should you imagine yourself standing?

A: Outside the object, looking directly at the face.

Q: How does WebGL know when a face of a 3D object is hidden from view and can therefore be culled?

A: WebGL calculates the winding order on the screen; it assumes you defined all front faces with a CCW order, so if it sees a face with a CW order, it culls it (as it must be looking at the back).

The animation loop

Here, we learn a general Web API for animations that is exposed to JavaScript. It can be used for many things. We will use it to create an animation with WebGL2.

Q: In the browser, what API is the standard for creating smooth animations? Answer with the precise syntax.

A: requestAnimationFrame(callback)

Q: Why is requestAnimationFrame() so named?

A: It requests the browser to call the provided callback function, which determines the next frame in the animation.

Q: How does requestAnimationFrame() behave when the browser tab is inactive (not visible)?

A: It pauses (or slows down significantly) to save battery and CPU cycles.

Q: requestAnimationFrame() runs its callback exactly once. How do you create a continuous loop?

A: Call requestAnimationFrame recursively inside the callback function.

Q: What argument does requestAnimationFrame() automatically pass to its callback function?

A: A timestamp argument (a DOMHighResTimeStamp type, indicating when the frame starts).

Q: What is the minimal code structure for a continuous animation loop created with requestAnimationFrame()? (Assume the callback is named draw).

A:

function draw(timestamp) {
  // 1. Update state and render...
  // 2. Schedule next frame
  requestAnimationFrame(draw);
}

// 3. Start the loop
requestAnimationFrame(draw);
Q: The timestamp passed to the requestAnimationFrame() callback represents time in what unit?

A: Milliseconds.

Q: The timestamp passed to the requestAnimationFrame() callback measures time elapsed since what event? (Be general).

A: Since the time origin (usually when the page loaded).

Q: In the context of an animation created with requestAnimationFrame(), what does it mean to calculate a zeroed time?

A: It refers to calculating an elapsed time starting when the animation logic begins, rather than when the page loaded.

Q: What's a simple way to calculate a zeroed time for an animation made with requestAnimationFrame(callback)? Sketch your answer in code, using a callback function named draw.

A:

let startTime;

function draw(timestamp) {
  if (!startTime) {
    startTime = timestamp;
  }
  const elapsed = timestamp - startTime;
  
  // draw logic...
  
  requestAnimationFrame(draw);
}
Q: What syntax stops a scheduled animation frame request made with requestAnimationFrame()?

A: cancelAnimationFrame(requestID)

Q: Where do you get the requestID needed to cancel an animation frame request made with requestAnimationFrame()?

A: It is the return value of the requestAnimationFrame() call.

Project 4: The Spinning Cube

spinning multicolored cube

Goal: Render a multicolored unit cube, centered at the origin, that rotates in 3D space. You may reuse logic from Project 3 as appropriate.

Allowed linear-algebra dependency: You may use glMatrix for the matrix transformations, by downloading gl-matrix-min.js from the GitHub repo, putting it into a folder called libs in your project directory, and then including it in index.html with a <script> element above the line where you include your own script. You may also use the glMatrix documentation as a reference if needed. Note that the library exposes a global glMatrix object: you'll typically access functions via glMatrix.mat4.create(), glMatrix.vec3.fromValues(), etc. Also note that vectors and matrices (e.g. mat4 and vec3) are all Float32Array instances.

Approach: To allow distinct colors for each face, you may duplicate vertices. There will then be 36 vertices total: 6 faces $\times$ 2 triangles $\times$ 3 vertices.

Specifications:

  1. State Management:
    • Enable the depth test and face culling.
    • Create a VAO.
    • Create two VBOs: one for positions, one for colors (Note: You can use two bufferData calls and two vertexAttribPointer calls attached to the same VAO).
    • Configure position (attribute location 0) and color (attribute location 1).
  2. Shaders:
    • Vertex Shader:
      • Attributes: in vec3 position, in vec3 color.
      • Uniforms: uniform mat4 uModel, uniform mat4 uView, uniform mat4 uProjection.
      • Output: out vec3 vColor ("v" is conventional and stands for "varying," just as "u" stands for "uniform" in uModel).
      • Main: Set gl_Position = uProjection * uView * uModel * vec4(position, 1.0);. Pass color to vColor.
    • Fragment Shader:
      • Input: in vec3 vColor.
      • Output: fragColor using the interpolated input color (alpha 1.0).
  3. Matrix Logic (glMatrix): Create model, view, and projection matrices. Upload view and projection matrices via gl.uniformMatrix4fv.
    • Model: Use mat4.create().
    • View: Use mat4.lookAt. (Eye: [0, 0, 4], Center: [0, 0, 0], Up: [0, 1, 0]).
    • Projection: Use mat4.perspective. (FOV: $\frac{\pi}{4}$ radians, Aspect: canvas width/height, Near: 0.1, Far: 100.0).
  4. Render Loop:
    • Use requestAnimationFrame.
    • Clear both color and depth buffers.
    • Update the model matrix (rotate it slightly every frame around a unit-length axis vector using mat4.rotate).
    • Upload the model matrix via gl.uniformMatrix4fv.
    • Draw 36 vertices using gl.TRIANGLES.
Solution:

The solution below moves the WebGL utility functions createShader() and createProgram() into their own file.

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Spinning cube</title>
</head>
<body>
    <canvas id="spinning-cube-canvas" width="400" height="400">
      A canvas with a spinning cube.
    </canvas>
    <script src="libs/gl-matrix-min.js"></script>
    <script src="webgl-utilities.js"></script>
    <script src="spinning-cube.js"></script>
</body>
</html>

spinning-cube.js:

// GET CONTEXT
const canvas = document.getElementById('spinning-cube-canvas');
const gl = canvas.getContext('webgl2');

// SET CANVAS BACKGROUND COLOR
const lightGray = [220 / 255, 220 / 255, 220 / 255, 1];
gl.clearColor(...lightGray);

// LOAD CUBE DATA
const positions = getCubePositions();
const colors = getCubeColors();

// CREATE MATRICES
const uModel = glMatrix.mat4.create();

const uView = glMatrix.mat4.create();
const eye = glMatrix.vec3.fromValues(0, 0, 4);
const center = glMatrix.vec3.fromValues(0, 0, 0);
const up = glMatrix.vec3.fromValues(0, 1, 0);
glMatrix.mat4.lookAt(uView, eye, center, up);

const uProjection = glMatrix.mat4.create();
const fovy = Math.PI / 4;
const aspect = canvas.width / canvas.height;
const near = 0.1;
const far = 100.0;
glMatrix.mat4.perspective(uProjection, fovy, aspect, near, far);

// CREATE SHADER SOURCE
const vsSource = `#version 300 es
layout(location = 0) in vec3 position;
layout(location = 1) in vec3 color;
uniform mat4 uModel;
uniform mat4 uView;
uniform mat4 uProjection;
out vec3 vColor;

void main() {
  gl_Position = uProjection * uView * uModel * vec4(position, 1.0);
  vColor = color;
}
`;

const fsSource = `#version 300 es
precision highp float;
in vec3 vColor;
out vec4 fragColor;

void main() {
  fragColor = vec4(vColor, 1.0);
}
`;

// MANAGE STATE: VAO AND VBO
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);

const positionVBO = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionVBO);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);

const colorVBO = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorVBO);
gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW);
gl.enableVertexAttribArray(1);
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0);

// CREATE AND ACTIVATE PROGRAM
const program = createProgram(gl, vsSource, fsSource);
gl.useProgram(program);

// ACTIVATE 3D OPERATIONS
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);

// GET MATRIX LOCATIONS
const uModelLocation = gl.getUniformLocation(program, 'uModel');
const uViewLocation = gl.getUniformLocation(program, 'uView');
const uProjectionLocation = gl.getUniformLocation(program, 'uProjection');

// SET STATIC MATRICES
gl.uniformMatrix4fv(uViewLocation, false, uView);
gl.uniformMatrix4fv(uProjectionLocation, false, uProjection);

// CREATE AXIS OF ROTATION (a unit vector indicates the direction of the axis)
const axis = glMatrix.vec3.fromValues(1, 1, 0);
glMatrix.vec3.normalize(axis, axis);

let startTime;

// Tell WebGL how to map the Normalized Device Coordinates (NDC) 
// of the clip space (-1 to +1) to pixel coordinates on the screen.
// Required if the canvas is resized after the context is created.
gl.viewport(0, 0, canvas.width, canvas.height);

// ANIMATE
function draw(timestamp) {
  if (!startTime) {
    startTime = timestamp;
  }
  
  const elapsedTime = timestamp - startTime;

  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

  // Rotate based on elapsed time (0.001 converts ms to seconds)
  glMatrix.mat4.identity(uModel); 
  glMatrix.mat4.rotate(uModel, uModel, elapsedTime * 0.001, axis);
  gl.uniformMatrix4fv(uModelLocation, false, uModel);

  gl.drawArrays(gl.TRIANGLES, 0, 36);

  requestAnimationFrame(draw);
}

requestAnimationFrame(draw);

// CUBE LOADERS
function getCubePositions() {
  // 36 vertices (x, y, z)
  return new Float32Array([
      // Front face
      -0.5, -0.5,  0.5,
      0.5, -0.5,  0.5,
      0.5,  0.5,  0.5,
      -0.5, -0.5,  0.5,
      0.5,  0.5,  0.5,
      -0.5,  0.5,  0.5,

      // Back face
      -0.5, -0.5, -0.5,
      -0.5,  0.5, -0.5,
      0.5,  0.5, -0.5,
      -0.5, -0.5, -0.5,
      0.5,  0.5, -0.5,
      0.5, -0.5, -0.5,

      // Top face
      -0.5,  0.5, -0.5,
      -0.5,  0.5,  0.5,
      0.5,  0.5,  0.5,
      -0.5,  0.5, -0.5,
      0.5,  0.5,  0.5,
      0.5,  0.5, -0.5,

      // Bottom face
      -0.5, -0.5, -0.5,
      0.5, -0.5, -0.5,
      0.5, -0.5,  0.5,
      -0.5, -0.5, -0.5,
      0.5, -0.5,  0.5,
      -0.5, -0.5,  0.5,

      // Right face
      0.5, -0.5, -0.5,
      0.5,  0.5, -0.5,
      0.5,  0.5,  0.5,
      0.5, -0.5, -0.5,
      0.5,  0.5,  0.5,
      0.5, -0.5,  0.5,

      // Left face
      -0.5, -0.5, -0.5,
      -0.5, -0.5,  0.5,
      -0.5,  0.5,  0.5,
      -0.5, -0.5, -0.5,
      -0.5,  0.5,  0.5,
      -0.5,  0.5, -0.5,
  ]);
}

function getCubeColors() {
  // 36 colors (r, g, b)
  // Colors match faces (e.g., first 6 vertices are red, next 6 are green, etc.)
  return new Float32Array([
      // Front: Red
      1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0,
      // Back: Green
      0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0,
      // Top: Blue
      0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1,
      // Bottom: Yellow
      1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0,
      // Right: Purple
      1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1,
      // Left: Cyan
      0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1,
  ]);
}

webgl-utilities.js:

// CREATION UTILITIES: SHADERS AND PROGRAM 
function createShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  
  // Check success
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    const shaderInfoLog = gl.getShaderInfoLog(shader);
    gl.deleteShader(shader);
    throw new Error(`Could not compile shader: ${shaderInfoLog}`);
  }
  return shader;
}

function createProgram(gl, vsSource, fsSource) {
  const vs = createShader(gl, gl.VERTEX_SHADER, vsSource);
  const fs = createShader(gl, gl.FRAGMENT_SHADER, fsSource);
  
  const program = gl.createProgram();
  gl.attachShader(program, vs);
  gl.attachShader(program, fs);
  gl.linkProgram(program);
  
  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    const programInfoLog = gl.getProgramInfoLog(program);
    gl.deleteProgram(program);
    throw new Error(`Could not link program: ${programInfoLog}`);
  }
  return program;
}

Community & next steps

๐Ÿค— Community:

Want to share your progress, ask questions, provide feedback, or share your favorite resources? Drop a link in the "Show and tell" discussion thread!

๐Ÿš€ Next steps:

Now that you have the irreducible minimum of the programmable geometry pipeline memorized, you are ready to build. You can use the following resources to apply your skills to solve specific implementation problems, to deepen your theoretical understanding, or to build advanced procedural engines.

  • WebGL2 Fundamentals:

    • What it is: The definitive encyclopedia and cookbook for WebGL2.

    • How to use it: Use this as a reference. When you need to implement a specific feature not covered in the current primer (like textures, instanced drawing, or shadow maps), look it up here.

  • The Book of Shaders:

    • What it is: A legendary guide to procedural pixel art.

    • How to use it: Use this to master the fragment shader. While this primer focused largely on the vertex pipeline and state machine, the Book of Shaders will teach you how to use math to paint beautiful images on the geometry you create.

  • Learn OpenGL:

    • What it is: A deep dive into foundational graphics theory (written for C++).

    • How to use it: Use this for theory. If you want to understand the physics behind Physically Based Rendering (PBR) or the mathematics of lighting models, this is the industry standard.

  • RMF Engine:

    • What it is: A high-fidelity computational engine and API for creative coding, supporting sweep geometries (brushes, ribbons, tubes) and motion rails (camera trajectories, write-on effects, choreographed motion) .

    • How to use it: This is the perfect capstone project to test your new skills, with significant real-world benefits. You can help flesh out the proof of concept, or implement the spec in your favorite creative-coding library. This path is for the bold, as you will need to develop the necessary mathematical skills if you don't yet have them.

Citation & license

License: CC BY 4.0

If you found this guide helpful and wish to reference it, please include the following citation:

WebGL2 & GLSL primer: A zero-to-hero, spaced-repetition guide by Greg Stanton (2025), licensed under CC BY 4.0

About

Grok WebGL2 and GLSL through spaced repetition and hands-on projects. A zero-to-hero guide covering the programmable geometry pipeline, state management, and 3D shader logic.

Topics

Resources

Stars

Watchers

Forks