WebGL Hightlights

Vertex Array Objects

We can implement vertex array objects in WebGL 1 by using the OES_vertex_array_object extension. That being said, they are available by default in WebGL 2. This is an important feature that should always be used, since it significantly reduces rendering times. When not using vertex array objects, all attributes data is in a global WebGL state, which means that calling functions such as gl.vertexAttribPointer, gl.enableVertexAttribArray, and gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer) manipulates the global state.

On the other hand, with vertex array objects, we would set up all attributes during our application’s initialization and simply bind the data during rendering, yielding much better performance.

Wider Range of Texture Formats

While WebGL 1 had a limited set of texture formats, WebGL 2 provides a much larger set.

3D Textures

A 3D texture is a texture in which each mipmap level contains a single three-dimensional image. A 3D texture is essentially just a stack of 2D textures that can be sampled with $x$, $y$, and $z$ coordinates in the shader. This functionality allows us to have multiple 2D textures in a single object.

This is useful for visualizing volumetric data (like medical scans), 3D effects like smoke, storing lookup tables, and so on.

Texture Arrays

Texture arrays are a great feature for reducing complexity, improving code maintainability, and increasing the number of textures that can be used. By ensuring that all texture slices in a texture array are the same size.

Instanced Rendering

In WebGL 2, instancing or instanced rendering is available by default. Instance rendering is a way to execute the same drawing commands many times in a row, with each producing a slightly different result. This can be a very efficient method for rendering a large amount of geometry with very few API calls.

Instancing is a great performance booster for certain types of geometry, especially objects with many instances but without many vertices. Good examples are grass and fur. Instancing avoids the overhead of an individual API call per object, while minimizing memory costs by avoiding storing geometric data for each separate instance.

gl.drawArraysInstances(gl.TRIANGLES, 0, 3, 2);

Non-Power of 2 Texture Support

While in WebGL 1 the height and width of each image, or level, in the mipmap is a power of two smaller than the previous level, in WebGL 2, that limit is removed. That is, non-power of 2 textures work the same as power of 2 textures.

Fragment Depth

In WebGL 2, we can manually set our own custom values to the depth buffer (z-buffer). This feature allows you to manipulate the depth of a fragment from the fragment shader. This can be expensive, because it forces the GPU to bypass a lot of it’s normal fragment discard behavior.

Texture Size in Shaders

In WebGL 2, you can look up the size of any texture within ESSL shaders

vec2 size = textureSize(sampler, lod);

Sync Objects

In WebGL 2, sync objects allow the developer to gain a little more insight into when the GPU has completed it’s work. Using gl.fenceSync, you can place a marker at some point in the GPU command stream and then later call gl.clientWaitSync to pause Javascript execution until the GPU has completed all commands up to the fence. This could be very beneficial for bechmarking.

Direct Texel Lookup

It’s often convenient to store large arrays of data in a texture. . In WebGL 2, accessing this sort of data is considerably easier, as you can easily look up values from a texture with pixel/texel coordinates.

vec4 values = texelFetch(sampler, ivec2Position, lod);

Shader Matrix Functions

Given that WebGL 2’s shading language is much more feature-rich than WebGL 1’s, we now have many more matrix math operations.

Common Compressed Textures

In WebGL 1, there are various compressed texture formats that are hardware-dependent. However, in WebGL 2, there are several formats which are much more flexible by being hardware independent.

Uniform Buffer Objects

With WebGL 2, we can use uniform buffer objects, which allow us to specify a large number of uniforms from a single buffer. This is a major boost in performance. Additionally, uniform buffers can be bound to multiple programs at the same time, so it’s possible to update global data (like projection or view matrices) once and all programs that use them will automatically see the changed values.

Transform Feedback

A powerful technique offered in WebGL 2 is that vertex shaders can write their results back into a buffer. This can be very useful in situations where we want to leverage the GPU’s computational power to perform complex computations so that we are able to read them within our application.

Sampler Objects

While in WebGL 1 all texture parameters are per texture, in WebGL 2, we can optionally use sampler objects. By using samplers, we can move all texture parameters to a sampler, allowing a single texture to be sampled in different ways.

const samplerA = gl.createSampler();
gl.samplerParameteri(
  samplerA,
  gl.TEXTURE_MIN_FILTER,
  gl.NEAREST_MIPMAP_NEAREST
);
gl.samplerParameteri(samplerA, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.samplerParameteri(samplerA, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.samplerParameteri(samplerA, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

const samplerB = gl.createSampler();
gl.samplerParameteri(samplerB, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
gl.samplerParameteri(samplerB, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.samplerParameteri(samplerB, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT);
gl.samplerParameteri(samplerB, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT);

// ...

gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.bindSampler(0, samplerA);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.bindSampler(1, samplerB);

Depth Textures

A major drawback to WebGL 1 is the lack of support for depth textures. In WebGL 2, they are available by default.

Standard Derivatives

While in WebGL 1 you’d need to compute normal and pass them to shaders, in WebGL 2, you can compute them within shaders by using a larger set of mathematical operations that are available by default.

Multiple Render Targets (MRT)

In WebGL 2, you can draw to multiple buffers at once from a shader.

Query Objects

Query objects give developers another, more explicit way to peek at the inner workings of the GPU. A query wraps a set of GL commands for the GPU to asynchronously report some sort of statistic about. It should be noted that these queries are asynchronous, which means that a queries’ results may not be ready for many frames after the query was originally issued!

gl.beginQuery(gl.ANY_SAMPLES_PASSED, query);
gl.drawArraysInstanced(gl.TRIANGLES, 0, 3, 2);
gl.endQuery(gl.ANY_SAMPLES_PASSED);

//...

(function tick() {
  if (!gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE)) {
    // A query's result is never available in the same frame
    // the query was issued. Try in the next frame.
    requestAnimationFrame(tick);
    return;
  }
  var samplesPassed = gl.getQueryParameter(query, gl.QUERY_RESULT);
  gl.deleteQuery(query);
})();

Texture LOD

The texture LOD parameter is used to determine which mipmap to fetch from. This allows for mipmap streaming, that is, loading only the mipmap levels currently needed. This is very useful for a WebGL environment, where textures are downloaded via a network.

gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MIN_LOD, 0.0);
gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAX_LOD, 10.0);