In the previous articles, we learned how to write simple vertex and fragment shaders, make a simple webpage, and prepare a canvas for drawing. In this article, we'll start working on our WebGL boilerplate code.
We'll obtain a WebGL context and use it to clear the canvas with the color of our choice. Woohoo! This can be as little as three lines of code, but I promise you I won't make it that easy! As usual, I'll try to explain the tricky JavaScript concepts as we meet them, and provide you with all the details you need to understand and predict the corresponding WebGL behavior.
This article is part of the "Getting Started in WebGL" series. If you haven't read the previous parts, I recommend that you read them first:
Recap
In the first article of this series, we wrote a simple shader that draws a colorful gradient and fades it in and out slightly. Here's the shader that we wrote:
In the second article of this series, we started working towards using this shader in a webpage. Taking small steps, we explained the necessary background of the canvas element. We:
- made a simple page
- added a canvas element
- acquired a 2D-context to render to the canvas
- used the 2D-context to draw a line
- handled page resizing issues
- handled pixel density issues
Here's what we made so far:
In this article, we borrow some pieces of code from the previous article and tailor our experience to WebGL instead of 2D drawing. In the next article—if Allah wills—I'll cover viewport handling and primitives clipping. It's taking a while, but I hope you'll find the entire series very useful!
Initial Setup
Let's build our WebGL-powered page. We'll be using the same HTML we used for the 2D drawing example:
<!DOCTYPE html><html><head><meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1"></head><body><canvas id="glCanvas" width="1" height="1"></canvas></body></html>
... with a very small modification. Here we call the canvas glCanvas
instead of just canvas
(meh!).
We'll also use the same CSS:
html, body { height: 100%; } body { margin: 0; } canvas { display: block; width: 100%; height: 100%; background: #000; }
Except for the background color, which is now black.
We won't use the same JavaScript code. We'll start with no JavaScript code at all, and add functionality bit by bit to avoid confusion. Here's our setup so far:
Now let's write some code!
WebGL Context
The first thing we should do is obtain a WebGL context for the canvas. Just as we did when we obtained a 2D drawing context, we use the member function getContext
:
glContext = glCanvas.getContext("webgl") || glCanvas.getContext("experimental-webgl");
This line contains two getContext
calls. Normally, we shouldn't need the second call. But just in case the user is using an old browser in which the WebGL implementation is still experimental (or Microsoft Edge), we added the second one.
The cool thing about the ||
operator (or operator) is that it doesn't have to evaluate the entire expression if the first operand was found to be true
. In other words, in an expression a || b
, if a
evaluates to true
, then whether b
is true
or false
doesn't affect the outcome at all. Thus, we don't need to evaluate b
and it's skipped entirely. This is called Short-Circuit Evaluation.
In our case, getContext("experimental-webgl")
will be executed only if getContext("webgl")
fails (returns null
, which evaluates to false
in a logical expression).
We've also used another feature of the or
operator. The result of or-ing is neither true
nor false
. Instead, it's the first object that evaluates to true
. If none of the objects evaluates to true
, or-ing returns the rightmost object in the expression. This means, after running the above line, glContext
will either contain a context object or null
, but not true
or false
.
Note: if the browser supports both modes (webgl
and experimental-webgl
) then they are treated as aliases. There would be absolutely no difference between them.
Putting the above line where it belongs:
var glContext; function initialize() { // Get WebGL context, var glCanvas = document.getElementById("glCanvas"); glContext = glCanvas.getContext("webgl") || glCanvas.getContext("experimental-webgl"); if (!glContext) { alert("Failed to acquire a WebGL context. Sorry!"); return false; } return true; }
Voila! We have our initialize
function (yeah, keep dreaming!).
Handling getContext Errors
Notice that we didn't use try
and catch
to detect getContext
issues as we did in the previous article. It's because WebGL has its own error reporting mechanisms. It doesn't throw an exception when context creation fails. Instead, it fires a webglcontextcreationerror
event. If we are interested in the error message then we should probably do this:
// Context creation error listener, var errorMessage = "Couldn't create a WebGL context"; function onContextCreationError(event) { if (event.statusMessage) errorMessage = event.statusMessage; } glCanvas.addEventListener("webglcontextcreationerror", onContextCreationError, false);
Taking these lines apart:
glCanvas.addEventListener("webglcontextcreationerror", onContextCreationError, false);
Just like when we added a listener to the window load event in the previous article, we added a listener to the canvas webglcontextcreationerror
event. The false
argument is optional; I'm just including it for completeness (since the WebGL specification example has it). It's usually included for backwards compatibility. It stands for useCapture
. When true
, it means that the listener is going to be called in the capturing phase of the event propagation. If false
, it's going to be called in the bubbling phase instead. Check this article for more details about events propagation.
Now to the listener itself:
var errorMessage = "Couldn't create a WebGL context"; function onContextCreationError(event) { if (event.statusMessage) errorMessage = event.statusMessage; }
In this listener, we keep a copy of the error message, if any. Yep, having an error message is totally optional:
if (event.statusMessage) errorMessage = event.statusMessage;
What we've done here is pretty interesting. errorMessage
was declared outside the function, yet we used it inside. This is possible in JavaScript and is called closures. What is interesting about closures is their lifetime. While errorMessage
is local to the initialize
function, since it was used inside onContextCreationError
, it won't be destroyed unless onContextCreationError
itself is no longer referenced.
In other words, as a long as an identifier is still accessible, it can't be garbage collected. In our situation:
errorMessage
lives becauseonContextCreationError
references it.onContextCreationError
lives because it's referenced somewhere among the canvas event listeners.
So, even if initialize
terminates, onContextCreationError
is still referenced somewhere in the canvas object. Only when it's released can errorMessage
be garbage-collected. Moreover, subsequent calls of initialize
won't affect the previous errorMessage
. Every initialize
function call will have its own errorMessage
and onContextCreationError
.
But we don't really want onContextCreationError
to live beyond initialize
termination. We don't want to listen to other attempts at getting WebGL contexts anywhere else in the code. So:
glCanvas.removeEventListener("webglcontextcreationerror", onContextCreationError, false);
Putting it all together:
To verify that we've successfully created the context, I've added a simple alert
:
alert("WebGL context successfully created!");
Now switch to the Result
tab to run the code.
And it doesn't work! Obviously, because initialize
was never called. We need to call it right after the page is loaded. For this, we'll add these lines above it:
window.addEventListener('load', function() { initialize(); }, false);
Let's try again:
It works! I mean, it should unless a context couldn't be created! If it doesn't, please make sure you are viewing this article from a WebGL-capable device/browser.
Notice that we did another interesting thing here. We used initialize
in our load
listener before it was even declared. This is possible in JavaScript due to hoisting. Hoisting means that all declarations are moved to the top of their scope, while their initializations remain in their places.
Now, wouldn't it be nice to test if our error reporting mechanism actually works? We need getContext
to fail. One easy way to do so is to obtain a different type of context for the canvas first before attempting to create the WebGL context (remember when we said that the first successful getContext
changes the canvas mode permanently?). We'll add this line just before getting the WebGL context:
glCanvas.getContext("2d");
And:
Great! Now whether the message you saw was "Couldn't create a WebGL context
" or something like "Canvas has an existing context of a different type
" depends on whether your browser supports webglcontextcreationerror
or not. At the time of writing this article, Edge and Firefox don't support it (it was planned for Firefox 49, but still doesn't work on Firefox 50.1). In such case, the event listener won't be called and errorMessage
will remain set to "Couldn't create a WebGL context
". Fortunately, getContext
still returns null
, so we know that we couldn't create the context. We just don't have the detailed error message.
The thing about WebGL error messages is that... there are no WebGL error messages! WebGL returns numbers indicating error states, not error messages. And when it happens to allow error messages, they are driver dependent. The exact wording of the error messages is not provided in the specification—it's up to the driver developers to decide how they should put it. So expect to see the same error worded differently on different devices.
Ok then. Since we made sure that our error reporting mechanism works, the "successfully created
" alert and getContext("2d")
are no longer needed. We'll omit them.
Context Attributes
Back to our revered getContext
function:
glContext = glCanvas.getContext("webgl");
There is more to it than meets the eye. getContext
can optionally take one more argument: a dictionary that contains a set of context attributes and their values. If none is provided, the defaults are used:
dictionary WebGLContextAttributes { GLboolean alpha = true; GLboolean depth = true; GLboolean stencil = false; GLboolean antialias = true; GLboolean premultipliedAlpha = true; GLboolean preserveDrawingBuffer = false; GLboolean preferLowPowerToHighPerformance = false; GLboolean failIfMajorPerformanceCaveat = false; };
I will explain some of these attributes as we use them. You can find more about them in the WebGL Context Attributes section of the WebGL specification. For now, we don't need a depth buffer for our simple shader (more about it later). And to avoid having to explain it, we'll also disable premultiplied-alpha! It takes an article of its own to properly explain the rationale behind it. Thus, our getContext
line becomes:
var contextAttributes = {depth: false, premultipliedAlpha: false}; glContext = glCanvas.getContext("webgl", contextAttributes) || glCanvas.getContext("experimental-webgl", contextAttributes);
Note: depth
, stencil
and antialias
attributes, when set to true
, are requests, not requirements. The browser should try to its best to satisfy them, but it's not guaranteed. However, when they are set to false
, the browser must abide.
On the other hand, the alpha
, premultipliedAlpha
and preserveDrawingBuffer
attributes are requirements that must be fulfilled by the browser.
clearColor
Now that we have our WebGL context, it's time to actually use it! One of the basic operations in WebGL drawing is clearing the color buffer (or simply the canvas in our situation). Clearing the canvas is done in two steps:
- Setting the clear-color (can be done only once).
- Actually clearing the canvas.
OpenGL/WebGL calls are expensive, and the device drivers are not guaranteed to be awfully smart and avoid unnecessary work. Therefore, as a rule of thumb, if we can avoid using the API then we should avoid using it.
So, unless we need to change the clear-color every frame or mid-drawing, we should write the code setting it in an initialization function instead of a drawing one. This way, it's called only once at the beginning and not with every frame. Since the clear-color is not the only state variable that we'll be initializing, we'll make a separate function for state initialization:
function initializeState() { ... }
And we'll call this function from within the initialize
function:
function initialize() { ... // If failed, if (!glContext) { alert(errorMessage); return false; } initializeState(); return true; }
Beautiful! Modularity will keep our not so short code cleaner and more readable. Now to populate the initializeState
function:
function initializeState() { // Set clear-color to red, glContext.clearColor(1.0, 0.0, 0.0, 1.0); }
clearColor
takes four parameters: red, green, blue, and alpha. Four floats, whose values are clamped to the range [0, 1]. In other words, any value less than 0 becomes 0, any value larger than 1 becomes 1, and any value in-between remains unchanged. Initially, the clear-color is set to all zeroes. So, if transparent black was ok with us, we could have omitted this altogether.
Clearing the Drawing Buffer
Having set the clear-color, what's left is to actually clear the canvas. But one can't help but ask a question, do we have to clear the canvas at all?
Back in the old days, games doing full-screen rendering didn't need to clear the screen every frame (try typing idclip
in DOOM 2 and go somewhere you are not supposed to be!). The new contents would just overwrite the old ones, and we would save the non-trivial clear operation.
On modern hardware, clearing the buffers is extremely fast. Moreover, clearing the buffers can actually improve performance! To put it simply, if the buffer contents were not cleared, the GPU may have to fetch the previous contents before overwriting them. If they were cleared, then there's no need to retrieve them from the relatively slower memory.
But what if you don't want to overwrite the whole screen, but incrementally add to it? Like when making a painting program. You want to draw the new strokes only, while keeping the previous ones. Doesn't the act of leaving the canvas without clearing make sense now?
The answer is still no. On most platforms you'd be using double-buffering. This means that all the drawing we perform is done on a back buffer while the monitor retrieves its contents from a front buffer. During the vertical retrace, these buffers are swapped. The back becomes the front and the front becomes the back. This way we avoid writing to the same memory that's currently being read by the monitor and displayed, thus avoiding artifacts due to incomplete drawing or drawing too fast (having drawn several frames written while the monitor is still tracing a single one).
Thus, the next frame doesn't overwrite the current frame, because it's not written to the same buffer. Instead, it overwrites the one that was in the front buffer before swapping. That's the last frame. And whatever we've drawn in this frame won't appear in the next one. It'll appear in the next-next one. This inconsistency between buffers causes flickering that is normally undesired.
But this would have worked if we were using a single buffered setup. In OpenGL on most platforms, we have explicit control over buffering and swapping the buffers, but not in WebGL. It's up to the browser to handle it on its own.
Umm... Maybe it's not the best time, but there's one thing about clearing the drawing buffer that I didn't mention before. If we don't explicitly clear it, it would be implicitly cleared for us!
There are only three drawing functions in WebGL 1.0: clear
, drawArrays
, and drawElements
. Only if we call one of these on the active drawing buffer (or if we've just created the context or resized the canvas), it is to be presented to the HTML page compositor at the beginning of the next compositing operation.
After compositing, the drawing buffers are automatically cleared. The browser is allowed to be smart and avoid clearing the buffers automatically if we cleared them ourselves. But the end result is the same; the buffers are going to be cleared anyway.
The good news is, there's still a way to make your paint program work. If you insist on doing incremental drawing, we can set the preserveDrawingBuffer
context attribute when acquiring the context:
glContext = glCanvas.getContext("webgl", {preserveDrawingBuffer: true});
This prevents the canvas from being automatically cleared after compositing, and simulates a single buffered setup. One way it's done is by copying the contents of the front buffer to the back buffer after swapping. Drawing to a back buffer is still necessary to avoid drawing artifacts, so it can't be helped. This, of course, comes with a price. It may affect performance. So, if possible, use other approaches to preserve the contents of the drawing buffer, like drawing to a frame buffer object (which is beyond the scope of this tutorial).
clear
Brace yourselves, we'll be clearing the canvas any moment now! Again, for modularity, let's write the code that draws the scene every frame in a separate function:
function drawScene() { // Clear the color buffer, glContext.clear(glContext.COLOR_BUFFER_BIT); }
Now we've done it! clear
takes one parameter, a bit-field that indicates what buffers are to be cleared. It turns out that we usually need more than just a color buffer to draw 3D stuff. For example, a depth buffer is used to keep track of the depths of every drawn pixel. Using this buffer, when the GPU is about to draw a new pixel, it can easily decide whether this pixel occludes or is occluded by the previous pixel that resides in its place.
It goes like this:
- Compute the depth of the new pixel.
- Read the depth of the old pixel from the depth buffer.
- If the new pixel's depth is closer than the old pixel's depth, overwrite the pixel's color (or blend with it) and set its depth to the new depth. Otherwise, discard the new pixel.
I used "closer" instead of "smaller" because we have explicit control over the depth function (which operator to use in comparison). We get to decide whether a larger value means a closer pixel (right-handed coordinates system) or the other way around (left-handed).
The notion of right- or left-handedness refers to the direction of your thumb (z-axis) as you curl your fingers from the x-axis to the y-axis. I'm bad at drawing, so, look at this article in the Windows Dev Center. WebGL is left-handed by default, but you can make it right-handed by changing the depth function, as long as you are taking the depth range and the necessary transformations into account.
Since we chose not to have a depth buffer when we created our context, the only buffer that needs to be cleared is the color buffer. Thus, we set the COLOR_BUFFER_BIT
. If we had a depth buffer, we would have done this instead:
glContext.clear(glContext.COLOR_BUFFER_BIT | glContext.GL_DEPTH_BUFFER_BIT);
The only thing left is to call drawScene
. Let's do it right after initialization:
window.addEventListener('load', function() { // Initialize everything, initialize(); // Start drawing, drawScene(); }, false);
Switch to the Result
tab to see our beautiful red clear-color!
Canvas Alpha-Compositing
One of the important facts about clear
is that it doesn't apply any alpha-compositing. Even if we explicitly use a value for alpha that makes it transparent, the clear-color would just be written to the buffer without any compositing, replacing anything that was drawn before. Thus, if you have a scene drawn on the canvas and then you clear with a transparent color, the scene will be completely erased.
However, the browser still does alpha-compositing for the entire canvas, and it uses the alpha value present in the color buffer, which could have been set while clearing. Let's add some text below the canvas and then clear with a half-transparent red color to see it in action. Our HTML would be:
<body><p style="position:absolute; left:0; top:0; z-index:-1;"> Shhh, I'm hiding behind the canvas so you can't see me.</p><canvas id="glCanvas" width="1" height="1"></canvas></body>
and the clear
line becomes:
// Set clear-color to transparent red, glContext.clearColor(1.0, 0.0, 0.0, 0.5);
And now, the reveal:
Look closely. Up there in the top-left corner... there's absolutely nothing! Of course you can't see the text! It's because in our CSS, we've specified #000
as the canvas background. The background acts as an additional layer below the canvas, so the browser alpha-composites the color buffer against it while it completely hides the text. To make this clearer, we'll change the background to green and see what happens:
background: #0f0;
And the result:
Looks reasonable. That color appears to be rgb(128, 127, 0)
, which can be considered as the result of blending red and green with alpha equals 0.5 (except if you are using Microsoft Edge, in which the color should be rgb(255, 127, 0)
because it doesn't support premultiplied-alpha for the time being). We still can't see the text, but at least we know how the background color affects our drawing.
Alpha Blending
The result invites curiosity, though. Why was red halved to 128
, while green was halved to 127
? Shouldn't they both be either 128
or 127
, depending on the floating point rounding? The only difference between them is that the red color was set as the clear-color in WebGL code, while the green color was set in the CSS. I honestly don't know why this happens, but I have a theory. It's probably because of the blending function used to merge the two colors.
When you draw something transparent on top of something else, the blending function kicks in. It defines how the pixel's final color (OUT
) is to be computed from the layer on top (source layer, SRC
) and the layer below (destination layer, DST
). When drawing using WebGL, we have many blending functions to choose from. But when the browser alpha-composites the canvas with the other layers, we only have two modes (for now): premultiplied-alpha and not premultiplied-alpha (let's call it normal mode).
The normal alpha mode goes like:
OUTᴀ = SRCᴀ + DSTᴀ(1 - SRCᴀ) OUTʀɢʙ = SRCʀɢʙ.SRCᴀ + DSTʀɢʙ.DSTᴀ(1 - SRCᴀ)
In the premultiplied-alpha mode, the RGB values are assumed to be already multiplied with their corresponding alpha values (hence the name pre-multiplied). In such case, the equations are reduced to:
OUTᴀ = SRCᴀ + DSTᴀ (1 - SRCᴀ) OUTʀɢʙ = SRCʀɢʙ + DSTʀɢʙ(1 - SRCᴀ)
Since we are not using premultiplied-alpha, we are relying on the first set of equations. These equations assume that the color components are floating-point values that range from 0
to 1
. But this is not how they are actually stored in memory. Instead, they are integer values ranging from 0
to 255
. So srcAlpha
(0.5
) becomes 127
(or 128
, based on how you round it), and 1 - srcAlpha
(1 - 0.5
) becomes 128
(or 127
). It's because half 255
(which is 127.5
) is not an integer, so we end up with one of the layers losing a 0.5
and the other one gaining a 0.5
in their alpha values. Case closed!
Note: alpha-compositing is not to be confused with the CSS blend-modes. Alpha-compositing is performed first, and then the computed color is blended with the destination layer using the blend-modes.
Back to our hidden text. Let's try making the background into transparent-green:
background: rgba(0, 255, 0, 0.5);
Finally:
You should be able to see the text now! It's because of how these layers are painted on top of each other:
- The text is drawn first on a white background.
- The background color (which is transparent) is drawn on top of it, resulting in a whitish-green background and a greenish text.
- The color buffer is blended with the result, resulting in the above... thing.
Painful, right? Luckily, we don't have to deal with all of this if we don't want our canvas to be transparent!
Disabling Alpha
var contextAttributes = {depth: false, alpha: false}; glContext = glCanvas.getContext("webgl", contextAttributes) || glCanvas.getContext("experimental-webgl", contextAttributes);
Now our color buffer won't have an alpha channel to start with! But wouldn't that prevent us from drawing transparent stuff?
The answer is no. Earlier, I mentioned something about WebGL having flexible blending functions that are independent from how the browser blends the canvas with other page elements. If we use a blending function that results in a premultiplied-alpha blending, then we have absolutely no need for the drawing-buffer alpha channel:
OUTᴀ = SRCᴀ + DSTᴀ (1 - SRCᴀ) OUTʀɢʙ = SRCʀɢʙ + DSTʀɢʙ(1 - SRCᴀ)
If we just disregard outAlpha
altogether, we don't really lose anything. However, whatever we draw still needs an alpha channel to be transparent. It's only the drawing buffer that lacks one.
Premultiplied-alpha plays well with texture filtering and other stuff, but not with most image manipulation tools (we haven't discussed textures yet—assume they are images we need to draw). Editing an image that is stored in premultiplied-alpha mode is not convenient because it accumulates rounding errors. This means that we want to keep our textures not premultiplied as long as we are still working on them. When it's time to test or release, we have to either:
- Convert all textures to premultiplied-alpha before bundling them with the application.
- Leave the textures be and convert them on-the-fly while loading them.
- Leave the textures be and get WebGL to premultiply them for us using:
glContext.pixelStorei(glContext.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
Note: pixelStorei
has no effect on compressed textures (Umm... later!).
All of these options may be a bit inconvenient. Fortunately, we can still achieve transparency without having an alpha channel and without using premultiplied-alpha:
OUTʀɢʙ = SRCʀɢʙ.SRCᴀ + DSTʀɢʙ(1 - SRCᴀ)
Just ignore outAlpha
completely and remove the dstAlpha
from the outRGB
equation. It works! If you get used to using it, you may start questioning the reason why dstAlpha
was ever included in the original equation to begin with!
Since we didn't draw primitives in this tutorial (we only used clear
, which doesn't use alpha-blending) we don't really need to write any alpha-blending concerned WebGL code. But just for reference, here are the steps needed to enable the above alpha-blending in WebGL:
function initializeState() { .... glContext.enable(glContext.BLEND); glContext.blendFunc(glContext.SRC_ALPHA, glContext.ONE_MINUS_SRC_ALPHA); }
If you still insist on having an alpha channel in the color buffer, you can use blendFuncSeparate to specify separate blending functions for RGB and alpha.
Clearing Alpha
If for some blending effect you need an alpha channel in your color buffer but you just don't want it to blend with the background, you can clear the alpha channel after you are done rendering:
glContext.colorMask(true, true, true, true); /* ... draw stuff ... */ glContext.colorMask(false, false, false, true); glContext.clear(glContext.COLOR_BUFFER_BIT);
This concludes our tutorial. Phew! So much for the simple act of clearing the screen! I hope you've found this article useful. Next time, I'll explain WebGL viewports and primitives clipping. Thanks a lot for reading!