Notes on software development

Past and future of HTML Canvas. A brief overview of 2D, WebGL, and WebGPU

What is Canvas?

Canvas is an HTML element designed for drawing using JavaScript. It acts as a container for drawing. Like other HTML elements, the Canvas has its own tag and is embedded on the page in the following way:

<canvas id="canvas">Canvas is not supported by your browser</canvas> 

All further operations with Canvas are done using JavaScript. First, we need to create a Canvas object:

const canvas = document.getElementById('canvas');

Then define the Context:

const context = canvas.getContext('2d');

In addition to the Canvas itself, we've created the Context associated with the Canvas. There are different types of Context, each providing its own set of methods or API.

Each Canvas can have only one Context. Multiple calls of getContext for the same Canvas will return a reference to the same Context.

In the example, we passed "2d" as a parameter, which allows us to use the API of the 2D context and draw in 2-dimensional space.

Canvas - a regular DOM element embedded in an HTML page.

Context - an object of a specific Canvas (API).

Types of context

There are several types of Context:

  • 2d. Two-dimensional Context.
  • webgl / webgl2. Three-dimensional Context.
  • webgpu. Modern three-dimensional Context.
  • bitmaprenderer. Only provides functionality to replace the content of the Canvas with an image.

2D Canvas

2D Context provides a high-level API that is easy to use. It allows to draw various built-in shapes: lines, figures, images, text, video, and perform various manipulations with them. 

Here are some basic examples of using 2D Context.

See the Pen 2D canvas. Basic example by Vyacheslav Demyanov (@VyacheslavDemyanov) on CodePen.

Advantages

  • Simple API.
    2D Context provides a high-level API that is easy to use. Most of the time, the 2D Context is used to draw shapes and images.
  • No knowledge of 3D graphics is required.
    Working with 3D graphics is not easy. It requires an understanding of how GPUs work and solid mathematical knowledge.

Disadvantages

  • Relatively slow, CPU intensive.
    At the moment, the 2D Canvas is hardware accelerated, which means it uses the GPU to perform particular calculations, allowing to move part of the load from the CPU to the GPU. But we can't control it, and in most cases, it will be slower compared to other Contexts.
  • Can't handle complex drawings.
    The 2D API doesn't provide any built-in methods to operate with particles or create interesting effects.
  • Cannot render in 3D.
    All we can draw are different shapes and images in 2D space.

WebGL / WebGL2

WebGL (Web Graphics Library) provides an API to work with 2D/3D graphics and utilizes the GPU for computations and complex drawing. It's based on the OpenGL ES API specification.

WebGL 1.0 is based on OpenGL ES 2.0 and WebGL 2.0 is based on OpenGL ES 3.0. All further examples will use WebGL2.

GPU operates with shaders. Shader is a special program written in GLSL (OpenGL Shading Language).

There are two types of shaders in WebGL: Vertex shaders and Fragment shaders. Together, they form a program that is run on the GPU.

A Vertex shader computes vertex positions. It’s executed for each vertex. Depending on the render type, vertices form different primitives: points, lines, and triangles.

A Fragment shader is executed for each pixel of the primitive and sets the color for each pixel.

Below you can see a classic example that draws a triangle using WebGL.

See the Pen WebGL2. Triangle by Vyacheslav Demyanov (@VyacheslavDemyanov) on CodePen.

You can see that there is much more code required to do the same thing in WebGL compared to 2D Canvas. Since you operate with shaders, you need to provide low-level code and have solid mathematical knowledge to work with 3D.

It is important to understand that WebGL is not a library for working with 3D graphics, you have to do everything yourself. Fortunately, there are various libraries that hide this complexity, and it makes sense to use them instead of writing low-level code from scratch. However, it's always useful to know how it works under the hood.

Now, let's talk about performance.

Since WebGL uses the GPU to perform some calculations and allows to move part of the load from the CPU to the GPU, we can expect it to work faster. In most cases, this is true, but not always.

In my tests, I decided to check how many moving images I could draw on a Canvas at 60 FPS. I created two tests using 2D Canvas and WebGL with shaders. On my desktop computer running Windows and the Chrome browser, I was able to render ~30% more moving images with WebGL at 60 FPS. However, when I ran the same tests on my MacBook in the Chrome browser, the tests showed the opposite result - 2D Canvas rendered ~20% more images than WebGL.

See the Pen 2D canvas. Moving images by Vyacheslav Demyanov (@VyacheslavDemyanov) on CodePen.

What does this tell us? Canvas performance depends not only on hardware but also on the OS and the browser itself. Each OS has its own API to communicate with the GPU, and each browser implements the OpenGL specification to work with the OS API differently.

Currently, there are no plans for a new core version of OpenGL ES. But GPU market continues to evolve, introducing new features like Ray Tracing and other technologies. As WebGL based on OpenGL specification that means it's not possible to access that features. So Web needed something new. 

Advantages

  • Can render in 3D.
    WebGL provides the possibility to use 3D graphics directly in a browser.
  • Better performance.
    WebGL utilizes the GPU for specific calculations. The GPU is designed for different kinds of work than the CPU, which means the calculation speed for particular algorithms is faster.

Disadvantages

  • Low-level API.
    You operate with shaders and need solid mathematical knowledge to work in 3D.
  • Only two types of shaders.
    WebGL supports two types of shaders - Vertex shaders and Fragment shaders, although the OpenGL specification describes more of them.
  • Outdated API.
    The OpenGL ES specification doesn't support features of modern GPUs, and currently, there are no plans to update the core version.

WebGPU

OpenGL, and correspondingly WebGL, are considered outdated. Today, operating systems have their own modern APIs to work with GPUs, and the web also requires better compatibility with modern GPUs.

Engineers from different companies, including Apple, Mozilla, Microsoft, Google, and others, decided to develop a new web standard that would meet their requirements. They called it WebGPU, and it's based on modern APIs such as Metal (Apple), Direct3D (Microsoft), Vulkan (Khronos Group/Android), and others. While it's based on these APIs, it's not a direct port.

Here is the same triangle example with WebGPU:

See the Pen WebGPU. Triangle by Vyacheslav Demyanov (@VyacheslavDemyanov) on CodePen.

WebGPU uses its own shading language called WGSL and supports three types of shaders: Vertex, Fragment, and Compute. We've already discussed the first two shaders in the WebGL part, so let's take a look at the Compute shader.

The Compute shader performs only one task: computations. It is executed in parallel by hundreds or thousands of GPU cores. The input and output of Compute shaders are buffers. A typical task involves sending data to perform calculations on it and then reading the output for the results.

Before we move on, it's important to understand the differences between calculations on the CPU and GPU.

The CPU and GPU have different architectures and are designed for different types of work. The goal of a CPU is to finish a task as quickly as possible with the ability to switch contexts. A GPU, on the other hand, tries to complete as much work as possible in parallel. Modern GPUs have hundreds or even thousands of specialized cores, making them efficient for processing large amounts of data in parallel. In other words, a GPU is good at executing the same code on different data. This is why Compute shaders are highly effective when we need to execute the same instructions on a large amount of data.

Let's move on to a practical example and examine Compute shader performance. I created two tests to calculate the sine of a large set of numbers using a Compute shader and plain JavaScript. The results are as follows: with a small set of numbers, plain JavaScript is about 10 times faster than the Compute shader. However, as the number of numbers grows, the time taken for calculations in JavaScript increases proportionally until it reaches a critical point where it can no longer perform the calculations. On the other hand, calculations with the Compute shader showed opposite results. It takes a little bit of time for setup, but as the number of numbers grows and JavaScript fails to perform the calculations, the Compute shader continues to show great performance. The time for calculations increases only slightly. The graph looks as follows:

Dependence of time on the number of elements.
Dependence of time (t) on the number of elements (n).

Advantages

  • Features of modern GPUs.
    The WebGPU API is under active development, and we can expect it to match the capabilities of modern graphics cards.
  • Greatr performance.
    Compute shaders allow high-performance computations.
  • Improved debugging.
    Using pipelines with labels allows for faster identification of where an error occurred.
  • Each frame executes fewer commands.
    We need to execute fewer commands inside the main loop.

Disadvantages

  • Low-level API.
    You still operate with shaders and need solid mathematical knowledge to work in 3D.
  • Heavy on setup.
    It takes a little more execution time for setup.

Which context to choose?

The choice of Context type depends on your goals. If you need to render something simple, use 2D Context. Despite being the oldest one, it does the job well. If you need 3D, use WebGPU. This doesn't mean that WebGL is not relevant; it will still be used for many years. However, if you're starting something from scratch, it makes sense to use WebGPU instead of WebGL.

There is a variety of different libraries that abstract the complexity of the Canvas API and allow you to export your app to any type of canvas. In most cases, it's better to use one of these libraries/engines:

Комментарии