dmytrish's WebGL 2 notes

These are my notes based on the videos about WebGL 2 by Andrew Adamson.

Each chapter starts with the corresponding video. Also, I added WebGL demos at the end of some sections.

at -->

References

Hello, WebGL

A minimal WebGL program

We'll need a <canvas> HTML element:

const canvas = document.querySelector('#canvas');
const gl = canvas.getContext('webgl2');
if (!gl) { throw "WebGL2 not supported"; }

const program = gl.createProgram();

A program must have at least one vertex shader:

const VERTEX_SHADER = `...`;    // see vertex.glsl below
const vertShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertShader, VERTEX_SHADER);
gl.compileShader(vertShader);

and at least one fragment shader:

const FRAGMENT_SHADER = `...`;  // see fragment.glsl below
const fragShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragShader, FRAGMENT_SHADER);

Then we attach them to program and link them:

gl.attachShader(program, vertShader);
gl.attachShader(program, fragShader);

gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    console.log(gl.getShaderInfoLog(vertShader));
    console.log(gl.getShaderInfoLog(fragShader));
}

gl.useProgram(program);

Actually drawing something

Drawing one single huge point:

// vertex.glsl:
#version 300 es

void main() {
    gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
    gl_PointSize = 80; // pixels
}
// fragment.glsl:
#version 300 es
precision mediump float;

out fragColor vec4;

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

precision mediump float is explained in https://www.youtube.com/watch?v=lWLqi3DzaCk.

// all the previous program setup, then
gl.drawArrays(gl.POINTS, 0, 1);

Drawing primitives

gl.drawArrays(gl.POINTS, 0, n_vertices);
gl.drawArrays(gl.LINES, 0, n_vertices);
gl.drawArrays(gl.LINE_LOOP, 0, n_vertices);
gl.drawArrays(gl.TRIANGLES, 0, n_vertices);
...

Demo

A big green point demo:

WebGL Uniforms

If you look at the shaders in the previous section, you see only hardcoded values, with no way to change anything, other than changing and recompiling the shaders source code.

Uniform provide a way to pass values to a linked program from the API. They are like global variables accessible from vertex and frament shaders. Uniforms cannot be changed during a draw call.

// vertex.glsl:
#version 300 es

// these will be set from the API:
uniform vec2 uPosition;         // vec2(x, y)
uniform float uPointSize;       // in pixels

void main() {
    gl_Position = vec4(uPosition, 0.0, 1.0);
    gl_PointSize = uPointSize;
}
// webgl.js:
const uPosition = gl.getUniformLocation(program, 'uPosition');
const uPointSize = gl.getUniformLocation(program, 'uPointSize');
// do check for null!

gl.uniform1f(uPointSize, 100.0);
gl.uniform2f(uPosition, 0.2, -0.2);
gl.uniform2fv(uPosition, [0.2, -0.2]); // same

GLSL types

typeCan be set via
floatgl.uniform1f(p, a)
vec2gl.uniform2f(p, a, b)
gl.uniform2fv(p, [a, b])
vec3gl.uniform3f(p, a, b, c)
gl.uniform3fv(p, [a, b, c])
vec4gl.uniform4f(p, a, b, c, d)
gl.uniform4fv(p, [a, b, c, d])
typeCan be set via
intgl.uniform1i(p, a)
ivec2gl.uniform2i(p, a, b)
gl.uniform2iv(p, [a, b])
ivec3gl.uniform3i(p, a, b, c)
gl.uniform3iv(p, [a, b, c])
ivec4gl.uniform4i(p, a, b, c, d)
gl.uniform4iv(p, [a, b, c, d])
typeCan be set via
uintgl.uniform1u(p, a)
uvec2gl.uniform2u(p, a, b)
gl.uniform2uv(p, [a, b])
uvec3gl.uniform3u(p, a, b, c)
gl.uniform3uv(p, [a, b, c])
uvec4gl.uniform4u(p, a, b, c, d)
gl.uniform4uv(p, [a, b, c, d])
typeCan be set via
mat2gl.uniformMatrix2fv(p, [a11, a12, a21, a22])
mat3gl.uniformMatrix3fv(p, [a11, a12, a13, ..., a33])
mat4gl.uniformMatrix4fv(p, [a11, a12, a13, ..., a44])

uniformNTv() can set arrays of values, not just one vec2/vec3/ivec2/etc:

// fragment.glsl
uniform vec4 uColors[3];
const uColors = gl.getUniformLocation(program, 'uColors');
gl.uniform4fv(uColors, [
    1.0, 0.0, 0.0, 1.0,  // red
    0.0, 1.0, 0.0, 1.0,  // green
    0.0, 0.0, 1.0, 1.0,  // blue
]);

Demo

This is a WebGL uniform demo that translates mouse wheel and click events into changing uPointSize and uPosition uniforms in WebGL:

WebGL Attributes

Previously we only drew one point per one draw call. How do we draw multiple primitives in one draw call? Uniforms are like shader constants, they do not vary between vertices, so they are not suitable for this.

WebGL attributes allow passing a buffer that describes some information (e.g. position, point size or any other input to shader calculations) for each vertex individually.

Basically:

uniform values
  |---> vertex shader instance 0
  |---> vertex shader instance 1
  ...
  |---> fragment shader instance 0
  |---> fragment shader instance 1
  ...

attribute value 0 --> vertex shader instance 0
attribute value 1 --> vertex shader instance 1
...

Since each vertex connects to its rendering fragment(s?), you can pass per-vertex information (e.g. color) downstream to corresponding fragement shaders. In WebGL1, this is called varying <type> v<Name>;, e.g. varying vec4 vColor; in both vertex/fragment shaders.

WebGL2 changes this to out vec4 vColor; in the vertex shader and in vec4 vColor; in the fragment shader. Values for a vertex are then passed downstream to corresponding fragment shader instances.

Limits and WebGL1/WebGL2 syntax

Each WebGL program is guaranteed at least 16 attributes (check gl.getParameter(gl.MAX_VERTEX_ATTRIBS))

Each attribute is guaranteed to have at least 65536 vertices.

// WebGL1, GLSL 1.0:
attribute vec4 aPosition;
attribute float aAlpha;

// WebGL2, GLSL 3.0 ES
in vec4 aPosition;
in float aAlpha;
// WebGL1, GLSL 1.0
varying vec4 vColor;

// WebGL2, GLSL 3.0 ES
out vec3 vColor;    // vertex shader
in vec3 vColor;     // fragment shader

in and out in WebGL2:

// vertex.glsl
#version 300 es

in vec4 aPosition;  // from WebGL
out vec4 vColor;    // to fragment shader
...
// fragment.glsl
#version 300 es

in vec4 vColor;     // from vertex shader
out vec4 fragColor; // to fragment output
...

Attribute locations

Each vertex attribute (e.g. aPosition, aColor, etc) are identified by indexes called attribute locations. Attribute locations are just numbers, you get them via gl.getAttribLocation(program, 'aName'). They are allocated dynamically, unless you pre-bind them to desired fixed numbers.

Attribute locations can be bound to fixed values before linking the shaders:

// bind aName to location 0
gl.bindAttribLocation(program, 0, 'aName');

or in GLSL 3.0:

// bind aName to location 0
layout(location = 0) in vec2 aName;

Drawing using attributes

Now let's draw using attributes in one call:

// vertex.glsl
#version 300 es

// set per vertex:
in float aPointSize;
in vec2 aPosition;

void main() {
    gl_PointSize = aPointSize;
    gl_Position = vec4(aPosition, 0.0, 1.0);
}
const aPosition = gl.getAttribLocation(program, 'aPosition');
const aPointSize = gl.getAttribLocation(program, 'aPointSize');
// do check for -1

Vertex attributes must be enabled before use (otherwise they zeroed out?):

gl.enableVertexAttribArray(aPosition);
gl.enableVertexAttribArray(aPointSize);

Passing data to attributes

Let's say we have 2 vertices described by contiguous chunks of bytes (e.g. floats):

const bufferData = new Float32Array([
    /* [0] */ /*position*/0.0, 0.0, /*size*/100,
    /* [1] */ /*position*/0.4, 0.2, /*size*/50,
]);

const pointLen = 3;         // one chunk is 3 contiguous floats

// position of a vertex starts at the 0th float in a chunk:
const pointPositionOffset = 0;

// position of a vertex occupies two floats:
const pointPositionSize = 2;

// point size of a vertex starts at the float [2] in a chunk:
const pointSizeOffset = 2;
const pointSizeSize = 1;    // one float

To pass data to attributes, you need a WebGL buffer. You can have multiple buffers and bind them to a target, which is like a "slot" for WebGL functions to use. There are specific targets used by specific WebGL functions: e.g. gl.vertexAttribPointer uses the gl.ARRAY_BUFFER target and, correspondingly, the buffer currently bound to it.

const buffer = gl.createBuffer();
gl.bindBuffer(/*target*/gl.ARRAY_BUFFER, /*object*/buffer);

// Buffer data are passed to a target, not a buffer directly!
// Since `buffer` is currenly bound to gl.ARRAY_BUFFER, it will be used.
gl.bufferData(gl.ARRAY_BUFFER, bufferData, gl.STATIC_DRAW);

// Then we tell GPU the layout of the data,
// different portions of it describing different attributes:
const FLOAT_SIZE = Float32Array.BYTES_PER_ELEMENT; // = 4
gl.vertexAttribPointer(
    /*attrib*/      aPosition,
    /*size, type*/  pointPositionSize, gl.FLOAT,
    /*normalized*/  false,
    /*stride*/      pointLen * FLOAT_SIZE,
    /*offset*/      pointPositionOffset * FLOAT_SIZE,
);
gl.vertexAttribPointer(
    /*attrib*/      aPointSize,
    /*size, type*/  pointSizeSize, gl.FLOAT,
    /*normalized*/  false,
    /*stride*/      pointLen * FLOAT_SIZE,
    /*offset*/      pointSizeOffset * FLOAT_SIZE,
);

gl.drawArrays(gl.POINTS, /*start index*/0, /*n_vertices*/2);

Note that stride and offset are calcalated in bytes. Generally, you can have an array of arbitrary C structures containing floats in them.

Normalization

Useful when:

  • converting uint8/uin16/int to float.
  • float clamped to [0.0 .. 1.0].

Threading color into fragment shader

Let's add colors to our vertex data:

const bufferData = new Float32Array([
    /* [0] */ /*position*/0.0, 0.0,
              /*size*/ 100,
              /*color*/ 1.0, 0.0, 0.0,
    /* [1] */ /*position*/0.4, 0.2,
              /*size*/50,
              /*color*/ 0.0, 0.0, 1.0,
]);

const FLOAT_SIZE = 4;
const stride = 6 * FLOAT_SIZE;
const offsetPosition = 0;
const offsetPointSize = 2 * FLOAT_SIZE;
const offsetColor = 3 * FLOAT_SIZE;
// vertex.glsl
in vec3 aColor;
out vec3 vColor;

void main() {
    // or calculate vColor depending on the location/size/anything!
    vColor = aColor;
    ...
}
// fragment.glsl
in vec3 vColor;
out vec4 fragColor;

void main() {
    fragColor = vColor;
}
const aColor = gl.getAttribLocation(program, 'aColor');
gl.enableVertexAttribArray(aColor);

gl.vertexAttribPointer(
    aPosition,      // attrib location
    2,              // size
    gl.FLOAT,       // type
    false,          // normalized int/uint
    stride,         // the stride size in bytes
    offsetPosition,
);
gl.vertexAttribPointer(
    aPointSize,
    1,
    gl.FLOAT,
    false,          // normalized, if converting from int/uint
    stride,         // the stride
    offsetPointSize,
);
gl.vertexAttribPointer(
    aColor,
    3,
    gl.FLOAT,
    false,
    stride,
    offsetColor,
);

Generic attribute values

gl.vertexAttrib2fv() does the same to attributes as gl.uniform2fv() for uniforms.

Demo

The attributes demo draws three vertex points with their position, size and color defined via attribute values:

WebGL Objects, Targets and Bindings

GL objects

Objectremarks
Buffer
Texture
RenderBuffer
Sampler*
Query*
VertexArray*container
Framebuffercontainer
TransformFeedback*container

*new in WebGL 2

  • create<object>(): e.g. createBuffer(), createTexture(), etc
  • bind<object>(); unbind by binding to null
  • delete<object>()

Each object may have multple targets. Targets determine what you can do with an object, its specific behaviors. E.g. binding a buffer to gl.ELEMENT_ARRAY_BUFFER enables gl.drawElements().

WebGL functions may depend on objects bound to targets. Binding connects an object with a set of functions.

Functions can be target-free, e.g. gl.drawArrays.

A very incomplete list of dependencies:

ARRAY
_BUFFER
COPY
_READ
_BUFFER
COPY
_WRITE
_BUFFER
ELEMENT
_ARRAY
_BUFFER
PIXEL
_PACK
_BUFFER
PIXEL
_UNPACK
_BUFFER
bindBuffer++++++
bufferData++++++
bufferSubData++++++
copyBufferSubData++++++
getBufferParameter++++++
vertexAttribPointer+
drawElements+
drawElementsInstanced+
drawRangeElements+
readPixels+
compressedTexImage2D+

and so on, and so on.

WebGL Primitives

TODO

WebGL Elements

In many situations, one vertex is used by many primitives. Using drawArrays() requires repeating the vertex coordinates each time we need it.

Elements provide an indirection layer over vertex data, a way to use indices of vertices instead of vertex data themselves. This allows sharing of data for vertices.

For example,

// a pentagon made of triangles:
const elementVertexData = new Float32Array([
    0.0,      0.0,
    0.0       1.0,
    0.95106,  0.30902,
    0.58779,  -.80902,
    -.58779,  -.80902,
    -.95106,  0.30902,
]);
const elementIndexData = [
    0, 1, 2,
    0, 2, 3,
    0, 3, 4,
    0, 4, 5,
    0, 5, 1,
];

Now, let's bind arrayVertexBuffer to the target gl.ARRAY_BUFFER.

elementVertexBuffer can be bound to another target gl.ELEMENT_ARRAY_BUFFER.

const arrayVertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, arrayVertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, arrayVertexBuffer, gl.STATIC_DRAW);

const elementVertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elementVertexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, elementIndexData);

Once gl.ELEMENT_ARRAY_BUFFER is bound, we can use gl.drawElements (it will throw if there's no buffer bound to gl.ELEMENT_ARRAY_BUFFER):

gl.drawElements(gl.TRIANGLES, elementIndexData.length, gl.UNSIGNED_BYTE, 0);

Downsides: more round-trips to GPU.

WebGL Textures

TODO