While it is possible to render 3D graphics using basic HTML and CSS or with the Canvas API, there’s an alternative Web API for rendering detailed graphics with the same level of visual fidelity as that of video games.

WebGL is an adaptation of the OpenGL ES 2.0 Spec to JavaScript, and started off as a collaboration of the Khronos Group and Mozilla.

It’s been used to showcase procedural art pieces in places like ShaderToy and CodePen, 3D models such as those rendered with Marmoset Viewer, geovisualizations such as those in Uber’s deck.gl, and much more.

WebGL is currently supported on all major browsers as far back as year 2012.

At its core, WebGL is a state machine that lets you as the developer tell it how and where it will draw triangles/points/lines, so it’s your job as a engine developer to organize when and how the state of the application will change.

I’ve prepared a Github repo with everything you need to get started. We’ll walk through writing a WebGL Hello Triangle application in TypeScript (JavaScript with optional type checking).

Check out my other post on OpenGL for writing native C++ applications with nearly the same interface. It’s possible to export OpenGL apps as WebGL via WebAssembly.

Setup

First install:

Then type the following in your terminal.

Refer to this blog post on designing web libraries and apps for more details on Node.js, packages, etc.

Project Layout

As your project becomes more complex, you’ll want to separate files and organize your application to something more akin to a game or renderer, check out this post on game engine architecture and this one on real time renderer architecture for more details.

Dependencies

  • gl-matrix — A JavaScript library that allows users to write glsl like JavaScript code, with types for vectors, matrices, etc. While not in use in this sample, it's incredibly useful for programming more advanced topics such as camera matrices.
  • TypeScript — JavaScript with types, makes it significantly easier to program web apps with instant autocomplete and type checking.
  • Webpack — A JavaScript compilation tool to build minified outputs and test our apps faster.

Overview

In this application we will need to do the following:

  1. Initialize the API — Create your HTMLCanvasElement, either directly on your webpage or dynamically. Then call .getContext('webgl') to get a handle to the gl state machine which will write directly to that canvas. Then setup any initial state for the gl state machine, such as enabling depth testing, your clear color, etc.
  2. Initialize Resources — Create your WebGLBuffers for your Vertex, Index data, your Vertex/Fragment WebGLShaders, your WebGLProgram.
  3. Render — Define your vertex layout, bind your WebGLBuffers and WebGLProgram to the state machine, set any uniform data for that draw call, and drawElements.
  4. Destroy — Destroy any WebGL handles that you’ve created once you’re done using them.

The following will explain snippets from that can be found in the Github repo, with certain parts omitted, and member variables (this.memberVariable) declared inline without the this. prefix so their type is easier to see and the examples here can work on their own.

Initialize API

The key to starting a WebGL application is to call the getContext function on an HTMLCanvasElement. You can then supply a specific version of WebGL ('webgl' or 'webgl2') along with an optional config object.

Initialize Resources

Vertex Buffer Object

A Vertex Buffer Object (VBO) is a block of memory (such as a Typed Array) containing vertex data.

You could describe this data with one big buffer containing everything or with independent arrays for each element in your vertex layout, whichever best fits your use case and performance requirements.

Having them split can be easier to update if you’re changing your vertex buffer data often, which may be useful for CPU animations or procedurally generated geometry.

Index Buffer Object

An Index Buffer Object (IBO) is a list of vertex indices that’s used to make triangles, lines, or points. When rendering a set of triangles, Index Buffers allow for the reuse of a given vertex for a different triangle.

Now if you’re rendering triangles, there should be 3 indices per triangle in the index buffer, for lines there should be 2 indices per line, and points refer to just 1 element of an index buffer.

Note the use of a TypedArray for the variable indices. WebGL expects typed arrays for data buffers. We're opting to use 16 bit unsigned integers to specify our indices, which is faster than using 32 bit integers, however there's a limit to how many vertices you can reference, the maximum value of a 16 bit unsigned int.

Vertex Shader

A Vertex Shader is a GPU program that executes on every vertex of what you’re currently drawing. Often times developers will place code that handles positioning geometry here.

And do the following to create a vertex shader in JavaScript:

Fragment Shader

A Fragment Shader executes on every fragment. A fragment is like a pixel, but not limited to just 8 bit RGB, there could be multiple attachments that you’re writing to, in multiple encoded formats.

And do the following to create a fragment shader in JavaScript:

For more information on shader languages, check out this post comparing all shader languages across graphics apis.

Program

A Shader Program binds the vertex and fragment shaders together and sets them up to be used in the WebGL state machine.

Uniforms

Uniforms are variables that you send to your shader program to adjust its output.

All you need to do is declare a uniform in your shader:

And do the following to send your requested data to your shader:

Textures

Textures are image data structures that you can use as inputs to a shader or as frame buffer attachments.

And once they’re created, you can send access them in your shader like so:

And do the following to send it to your shader:

Rendering

To draw, call either gl.drawArrays to draw based off a specified pattern of how your vertex buffer is organized, or gl.drawElements if you're using an index buffer. More often than not you'll be using gl.drawElements.

Destroy

Even though the JavaScript runtime features garbage collection, GL Objects will not be garbage collected over the course of an application’s lifetime (so as long as you’re on the web page). If you want to get rid of a buffer, texture, frame buffer, etc. You’ll need to call gl.destroy...(handle), where the ... could be a Buffer, Framebuffer, Program, Renderbuffer, Shader, or Texture.

Conclusion

While a bit different from other Web APIs, WebGL’s interface can be surprisingly intuitive, and everything you do in it can translate to other languages such as C++, C, Rust, etc. in the form of OpenGL.

Now there were a few things I didn’t cover in this post as they would have been a beyond the scope of this post, such as:

  • Frame Buffers
  • Cube Maps
  • glDrawArrays options
  • Blend Modes
  • Matrices

Not to mention aspects of software engineering such as project organization, WebAssembly implementations, game engine architecture, real time renderer architecture, etc.

Additional Resources

You’ll find all the source code described in this post in the GitHub repo here.

https://Alain.xyz | Graphics Software Engineer @ Marmoset.co. Guest lecturer talking about 🛆 Computer Graphics, ✍ tech author.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store