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.
atReferences
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
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
type | Can be set via |
---|---|
float | gl.uniform1f(p, a) |
vec2 | gl.uniform2f(p, a, b) gl.uniform2fv(p, [a, b]) |
vec3 | gl.uniform3f(p, a, b, c) gl.uniform3fv(p, [a, b, c]) |
vec4 | gl.uniform4f(p, a, b, c, d) gl.uniform4fv(p, [a, b, c, d]) |
type | Can be set via |
---|---|
int | gl.uniform1i(p, a) |
ivec2 | gl.uniform2i(p, a, b) gl.uniform2iv(p, [a, b]) |
ivec3 | gl.uniform3i(p, a, b, c) gl.uniform3iv(p, [a, b, c]) |
ivec4 | gl.uniform4i(p, a, b, c, d) gl.uniform4iv(p, [a, b, c, d]) |
type | Can be set via |
---|---|
uint | gl.uniform1u(p, a) |
uvec2 | gl.uniform2u(p, a, b) gl.uniform2uv(p, [a, b]) |
uvec3 | gl.uniform3u(p, a, b, c) gl.uniform3uv(p, [a, b, c]) |
uvec4 | gl.uniform4u(p, a, b, c, d) gl.uniform4uv(p, [a, b, c, d]) |
type | Can be set via |
---|---|
mat2 | gl.uniformMatrix2fv(p, [a11, a12, a21, a22]) |
mat3 | gl.uniformMatrix3fv(p, [a11, a12, a13, ..., a33]) |
mat4 | gl.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
Object | remarks |
---|---|
Buffer | |
Texture | |
RenderBuffer | |
Sampler* | |
Query* | |
VertexArray* | container |
Framebuffer | container |
TransformFeedback* | container |
*new in WebGL 2
create<object>()
: e.g.createBuffer()
,createTexture()
, etcbind<object>()
; unbind by binding tonull
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