Nearly four years ago I wrote a blog post about a native, cross-platform OpenGL demo that I’d written. That post (which was only my second ever) was actually a response to another blog post written by Chris Wellons (aka null program) on the same topic. Back then, I spent hours and hours just trying to figure out how I could write (and compile) a single C program that would run on all three major platforms: Windows, macOS, and Linux. Chris used GLFW3 to solve this problem while mine used SDL2 (both are valid options). Here is a screenshot of the demo for reference:

SDL2 OpenGL Demo

While not particularly fancy, the code serves as a decent foundation for writing this type of program. I read through it the other day and wondered if I could rebuild the same application in a way that was easier to share with others…

Enter WebGL Link to heading

Lately, I’ve been quite interested in WebGL and how it trades top-end performance for the complete elimination of all distribution woes. I even touched on this topic in a previous post. In summary: WebGL allows you to build graphical applications (such as games) that are written in JavaScript and are executed directly in the user’s browser. They don’t need to download any files, install anything, or tell their computer to trust an untrusted executable (everyone gets suspicous when they hear: “Oh yeah, just ignore the virus warnings!”).

These days, simple graphics programs are incredibly easy to create and distribute via the modern browser. You can throw some JS+WebGL onto an HTML page, host it on a web server, and have it be instantly accessible by anyone with an internet connection. People from anywhere in the world (and on any operating system) can simply click on a link and experience the content! In fact, just by visiting this page, the new version of the demo has already been delivered to your machine! Look forward to seeing that at the end of this post.

Code Structure Link to heading

This is a pretty bare-bones WebGL demo without too many abstractions in place. I did write a few helpers for things like resizing the canvas and building shader programs, however. The demo uses a single shader to draw and rotate a red square. A single buffer is used to hold all four of the square’s vertices (2D points, one for each corner). The program starts by querying for an HTML canvas element that is included just before the script. Then, it asks the canvas (and the browser) for a WebGL context and gets to work!

<canvas id="glcanvas" width="640" height="640"></canvas>;
<script>
function main() {
  const canvas = document.querySelector("#glcanvas");
  const gl = canvas.getContext("webgl2");

  // load shaders
  // buffer geometry
  // start the draw loop
}

main();
</script>

After building the shader and buffering the square’s geometry, the program starts the draw loop. A WebGL-based draw loop utilizes requestAnimationFrame to let the browser decide when to draw a frame. It’ll usually match your screen’s refresh rate but could change depending on how the browser chooses to allocate its resources. From the docs:

The frequency of calls to the callback function will generally match the display refresh rate. The most common refresh rate is 60hz, (60 cycles/frames per second), though 75hz, 120hz, and 144hz are also widely used. requestAnimationFrame() calls are paused in most browsers when running in background tabs or hidden iframe’s, in order to improve performance and battery life.

To start the loop, you call this function with a function of your own that is responsible for drawing the scene. Additionally, your drawing function must be sure to call requestAnimationFrame again at the end to queue up the next frame and continue the loop. Essentially, your draw function and requestAnimationFrame form an infinite loop by continuously calling each other. Pretty neat, huh? In short:

function draw() {
  // activate our shader and buffer
  // update the square's rotation value
  // clear the canvas
  // draw the square
  // tell the browser to call our draw function again
  requestAnimationFrame(draw);
}
// kick off the loop by telling the browser to call our draw function
requestAnimationFrame(draw);

The Demo Link to heading

Enough talk, let’s see this thing!

Isn’t it beautiful? A simple graphics demo was distributed to your machine with the same ease and convenience as reading this blog post. To reiterate the sentiment from my previous post about WebGL:

While the upper bounds of performance are certainly lower when using a browser-based, JavaScript-based graphics programming environment, I don’t think that it impacts me very much. I don’t plan on creating anything incredibly large or high fidelity: just basic games and examples. If completely solving the problem of distribution means sacrificing a bit of performance, then I’m satisfied.

Thanks for reading!