Getting Started with WebGL

Making a simple webpage for your game, hosting it locally, and getting a canvas to render a basic 2D object using the WebGL API.

In our previous article, we laid out the concept and game design doc about making an office simulator game people can play in their browser. We have done a bit of planning, but now it's time to get something basic in the works to give us a starting point.

Open up your favorite editor or IDE. We will need to make a couple text files and also will need to run a terminal at one point. Here at Final Parsec, we are big fans of using Visual Studio Code for game development and that's what I plan to use here. Go ahead and make or find a directory you want to use to follow along.

Making a basic web page for your game

For local development, we are going to want a basic HTML page we can view in a web browser. That page is just going to have a <canvas> and a JavaScript entry point.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Office Sim</title>
    <script src="office-sim.js" type="module"></script>
  </head>
  <body>
    <canvas id="glcanvas" width="640" height="480"></canvas>
  </body>
</html>

This page has the bare minimum to be a valid HTML5 page with a couple additions. First, we've got a title "Office Sim". Cool, now we know what we're supposed to be looking at. But more interestingly, we have a <script> element in here and also a <canvas> element.

The canvas element is part of the HTML standard and provides a rectangular drawing surface within our web page. It has specific dimensions that we specify with the width and height attributes. It doesn't do much on its own, though. It is meant to be used in combination with the Canvas API or the WebGL API which are the JavaScript APIs enabling you to draw graphics within the canvas. The Canvas API is primarily meant for rendering 2D graphics with a simplified interface. The WebGL API has wider capabilities and the potential for better performance as a result of GPU acceleration. Those perks of WebGL come at the cost of less ease of using the API and more complexity in the software you write using it. As always, there's a great answer on StackOverflow comparing and contrasting using these two APIs to build a 2D game.

So if you can make a 2D game with the Canvas API and it might even be easier, why did I pick WebGL for this project? I touch on it a bit while writing the development section of the game design doc for this game, but this is more of an exercise for me to learn than anything. Most people even using the WebGL API to make a game will use a library like three.js or babylon.js rather than work with it directly. Many people work at an even higher level using a complete game engine like Unity to make games targeting WebGL. The simple answer is I'm trying to punish myself for the sake of a hobby to learn something new. This game in particular is likely to be a GUI heavy game with relatively simple 2D graphics. I won't really need a lot of the conveniences that come with a full featured engine, so I'm hoping to explore building something a little closer to the metal for fun.

Making our canvas do something

If you tried to look at your website now, you wouldn't see much. A pure white webpage with a pure white, undifferentiated canvas. Remember, the canvas is meant to come to life when we use JavaScript APIs to render something.

We don't have any JavaScript running yet to actually utilize the WebGL API, but take notice we reference a javascript file (office-sim.js). Let's go ahead and add that new file to our project in the same directory as the HTML file. Then we can add the following content:

main();

function main() {
  const canvas = document.querySelector("#glcanvas");
  const canvasContext = canvas.getContext("webgl");

  if (canvasContext == null) {
    alert("Unable to initialize WebGL.");
    return;
  }

  canvasContext.clearColor(0.0, 0.0, 0.0, 1.0);
  canvasContext.clear(canvasContext.COLORBUFFERBIT);
}

Here we have written a function called main which will be the entry point for our application since it's immediately called. Inside the function, we have three core steps...

Getting a drawing context

const canvas = document.querySelector("#glcanvas");
const canvasContext = canvas.getContext("webgl");

This block defines two variables, one referencing the canvas from our HTML page and another which is the drawing context for that canvas. We get a reference to the canvas by locating it within the HTML document by its ID attribute.

Once we have a reference to the canvas, we can call getContext() with the "webgl" string parameter to get a drawing context. Because we said we wanted the "webgl" context, we should get an object of type WebGLRenderingContext. This is that WebGL API we have been talking about!

"Handling" lack of support for WebGL

Some older or more unusual browsers may not support WebGL rendering, and getContext() will return null. Rather than just trying to go on running our game, we can add a brief snippet to look for this condition and alert the user. Strictly speaking this part isn't necessary, but your attempts to render anything will fail pretty ungracefully if what you're trying to do isn't supported by the user's browser.

if (canvasContext == null) {
  alert("Unable to initialize WebGL.");
  return;
}

Clearing the color buffer

Ok this is the part that's actually going to make something visually happen!

canvasContext.clearColor(0.0, 0.0, 0.0, 1.0);
canvasContext.clear(canvasContext.COLOR_BUFFER_BIT);

clearColor() allows us to specify a preset color to use when clearing the color buffer. Think of the color buffer as a collection of information representing the color values for every pixel to be shown on the canvas you are attempting to control. This "clear" color is just a default value we can quickly and easily write to all the pixels in the canvas by calling clear().

The clearColor() function takes four arguments in the following order: red, green, blue, alpha. So in this case, we set the clear color to be black with no opacity.

Then we immediately call clear() with a parameter to specify which buffer to clear. The result is that every pixel will be set to that opaque black color.

Hosting your game locally

So we have the expectation now that if we view our webpage, we should see our canvas as a completely black rectangle. If you just try double clicking and opening up your web page using something like file://, you might see it's still blank white. Open up the developer console and you are likely to see an error that the loading of your separate script file was blocked due to CORS policy.

To address this, we can use Python to host the world's most basic web server. Open up a terminal at the directory where your HTML and JS files exist and run:

python3 -m http.server

I picked Python because it's quick, easy, and already installed on a lot of operating systems. To be clear, though, you can run a local HTTP server pointed at a specific directory with pretty much any language of your choice or even accomplish this with a VS Code extension. To see the specifics of all the approaches you can take, see this answer over on StackOverflow.

Wow. A black rectangle on a white page. Are we going to do anything more interesting? You bet. Let's draw a 2D object on there.

Drawing a 2D object with WebGL

Obviously a black canvas is quite cool. But if we want to progress to making a video game, we're going to have to do more. Rendering a 2D object on top of that black background seems like a good next step.

Let's add the following snippet after the initial clear:

let boxSize = 100;
let x = canvasContext.drawingBufferWidth / 2 - boxSize / 2;
let y = canvasContext.drawingBufferHeight / 2 - boxSize / 2;
canvasContext.enable(canvasContext.SCISSOR_TEST);
canvasContext.scissor(x, y, boxSize, boxSize);

canvasContext.clearColor(1.0, 0.0, 0.0, 1.0);
canvasContext.clear(canvasContext.COLORBUFFERBIT);

It's not very visually engaging, but we have a great starting point to making a 2D game with WebGL! Sometime soon we'll move onto getting some art in a sprite sheet and rendering that, but let's talk about what we've done now... Here we are using a technique called scissoring in combination with clearing the color buffer (which we've done before) to draw a square in the center of our canvas.

Limiting drawing with a scissor box

The first block from our sample uses the WebGL API to create a scissor box. Once we enable this behavior and establish a scissor box, any drawing operations that would change pixels on the screen can only happen within that scissor box. It operates effectively like a mask which limits the drawing we will do in the next step.

let boxSize = 100;
let x = canvasContext.drawingBufferWidth / 2 - boxSize / 2;
let y = canvasContext.drawingBufferHeight / 2 - boxSize / 2;
canvasContext.enable(canvasContext.SCISSOR_TEST);
canvasContext.scissor(x, y, boxSize, boxSize);

The first couple lines here just do some basic math to determine the x and y coordinates of the square if we want it centered within the canvas viewport. It's the last two lines, though, that create the scissor box. enable(canvasContext.SCISSOR_TEST) turns on this type of testing, and scissor() sets the position and size of the scissor box.

Drawing with scissor test enabled

canvasContext.clearColor(1.0, 0.0, 0.0, 1.0);
canvasContext.clear(canvasContext.COLOR_BUFFER_BIT);

Assuming this is your first exposure to WebGL, we only know one way to draw so far and we're going to reuse that approach here. Let's set a new color that we want for our 2D square by calling clearColor() with an RGBA value; I chose red. Then we'll call clear() again.

This time, however, the clear() won't change every single pixel in the canvas! It will only change the pixels within our scissor box. The result to the viewer of the canvas is a red square against a black background.

Starting simple

So far we have a guiding vision for where we want this game to go and a very basic implementation of rendering something in a canvas on a web page using the WebGL API. While each step may not feel like much, incremental work of this nature applied over a longer period of time will get you great results. Each small piece of work you can plan and deliver gets you a little closer to the project you're imagining.

This game is open source and the complete source code is available on GitHub. Check out pull request #2 to see the changes we made in this blog post.

Recommended posts

We have similar articles. Keep reading!

Office Simulator: The Concept

We're creating an office life simulation game from scratch. Learn how to transform an abstract game idea into a better documented plan. Follow along as we start the development process.

Caching and Apache

Quick tutorial covering how HTTP headers affect browser caching and configuring Apache for your needs.

Enemy Health and Explosions

Video tutorial about managing enemy health in a rail shooter built with Unity3d. We'll also discuss killing enemies in fiery explosions.

A* Pathfinding Tutorial

Technical talk from Matt Bauer about A* pathfinding in Nauticus Act III.

Comments

Log in or sign up to leave a comment.