Wednesday, July 27, 2011

Dirty Full-Frame WebGL Performance Hack

So since WebGL first started appearing in browsers it's been people's natural instinct to create a 3D canvas that fills the entire browser window. Obviously this is because we like our 3D games to run full-screen (or as close to it as we can get). But you'll notice that I usually have my demos run in a window (usually 854x480). The reason for this has traditionally been because when WebGL was still gaining steam there was a severe performance penalty that was directly related to the size of your canvas. (See this early thread for a good idea of what I'm talking about)

Of course, things have improved on the browser side, and computers are always getting faster so this problem isn't as noticeable any more, but that doesn't mean it has disappeared. Netbooks/Chromebooks/etc are still popular, and don't have a lot of muscle. WebGL-capable mobile devices and tablets probably aren't too far off either. (N900 anyone?) For these environment, it would be great to preserve that fullscreen feel (especially on mobiles!) but still maintain a reasonable framerate (hopefully ~30fps or more.)

Since the dawn of 3D games we've had the ability to render at a lower res than your monitor is capable of and still have it fill the screen. Gamers are often willing to deal with some jagged edges to get smoother gameplay (but not many want to play in a window the size of a postage stamp.) So, is this an effect that we can emulate on the web? As it turns out, yes! I was playing with just such a situation a couple of days ago and stumbled on a great little hack.

The idea is simple: Create the WebGL canvas at a lower res (say, half width and height), and use CSS3 transforms to scale it to the full browser size. The code snippet is pretty simple:

// Create a WebGL canvas at half the document size
var canvas = document.getElementById("glCanvas");
canvas.width = document.width/2;
canvas.height = document.height/2;

And apply the following CSS style to the canvas element:

#glCanvas {
/* Anchor to the upper left */
position: absolute;
top: 0;
left: 0;

/* Scale out 2X from the corner */
-webkit-transform: scale3d(2.0, 2.0, 1.0);
-webkit-transform-origin: 0 0 0;

And done! Everything else works just like your standard WebGL app! In my experience, there is a small performance hit for the upscale (and yes, it interpolates), but it's nowhere near the performance hit of rendering everything at twice the resolution. On one slower machine I tried the fullscreen render was running at 6 fps, the half-sized render was going at 20 fps, and the half-size upscaled was getting about 16 fps. Not bad numbers overall!

As a proof of concept, I retrofitted the technique onto my Quake3 demo, which has a new variant here:

Full Screen Quake 3 (Touch enabled)

I've also taken the time to add some basic touch controls to the demo, since this technique will probably benefit mobile devices most as they gain WebGL capabilities.

  • One finger drag: Look around

  • Two Finger drag: Move/strafe

  • Three Finger tap: Jump

A small caveat for this demo is that the canvas will not scale to fill the window dynamically as you resize, but that wouldn't be too hard to add. Still, it's really cool to put your browser in fullscreen mode and see corner-to-corner WebGL running at a decent speed on most any device!

So now the fun part: What's the coolest device you can get this sucker to run on?


  1. I actually don't understand why you have to do the css when there is a viewport feature in the glcontext

    var canvas = document.getElementById("glCanvas");
    canvas.width = document.width;
    canvas.height = document.height;


    I don't know if it looks good or so or what the performance penalty is??

  2. I also noticed this in the past by changing the viewport (as gero3 said) but the loss of quality...

    You can gain a lot of performance also by set the anti-alias off (in the getContext).

    By the way, nice blog Toji, I learned a lot of Webgl with some of your demos (nothing better than a proper code to learn a language :p).

  3. If the loss of quality is such a big problem,
    then wouldn't it be better to use a framebuffer half the size and then interpolate it to a larger canvas.

  4. @gero3: That's essentially what this method does - render the buffer smaller and interpolate it to a larger area. The use of CSS scaling allows the browser to hardware accelerate the scale up at a low level, though, so the performance is better.

    As for your viewport suggestion, I must admit that I'm a little confused. Perhaps I'm missing something, but if you set a viewport that is half the size of the actual canvas, you'll just end up rendering in one corner. gl.viewport doesn't actually scale anything, it simply blocks out the area of the canvas that you are rendering to. Or did I misunderstand your suggestion?

    @Vince: Thanks for reminding me about the anti-aliasing setting. I think I'll apply that to this demo as well, since this one is all about the speed. :)

  5. Honestly I'm quite impressed with the quality. I thought that it would be more res'd out than it is. As always, nice work.

  6. no, viewport is for total amount of pixels rendered in the canvas. quote form webgl spec:

    The viewport specifies the affine transformation of x and y from normalized device coordinates to window coordinates. The size of the drawing buffer is determined by the HTMLCanvasElement. The scissor box defines a rectangle which constrains drawing. When the scissor test is enabled only pixels that lie within the scissor box can be modified by drawing commands. When enabled drawing can only occur inside the intersection of the viewport, canvas area and the scissor box. When the scissor test is not enabled drawing can only occur inside the intersection of the viewport and canvas area.

    scissor is for what you mention of only using half a canvas

  7. Sorry, gero, but I don't think you're right on this one. The key is actually in the quote you pasted:

    "The size of the drawing buffer is determined by the HTMLCanvasElement."

    Hence, the backbuffer size will always match the dimensions of the canvas you are rendering to. If you want to render at a lower res you have to make the canvas smaller.

    Scissor and viewport are similar concepts, so they could be easily confused, but they do have different uses. This thread does a decent job of explaining it:

  8. oops indeed.

    I had that technique wrong indeed.

    But I found what the solution was I had in mind becuase I knew that I had such a technique that was even simpler then this one.

    Here you can see that if you make the canvas width and canvas height different from the css width and height that you get teh same result as you're solution. And It even seemed to perform a little better in my initial tests.

  9. Very interesting! I was under the assumption that canvas elements ignored CSS width and height settings (certainly I've never had much luck with them.) But obviously as your sample shows it works, and apparently gives the same results as my method. I would imagine that if they perform the same it's because browsers have had such an emphasis on hardware accelerated compositing lately.

    Another interesting tidbit, though: Your method seems to work on Firefox (6.0) where mine fails. That alone may be enough to merit it's use! I'll play with it some more, do some speed tests, and may amend my original post. Thanks for following up on that!

  10. This is an unrelated question.

    Is it possible to have relative mouse movements for games in HTML5? For first person shooters (FPS) this is extremely important: a full-screen game captures the mouse at all positions, but once you scroll to the edge, further movement in that direction is ignored. This makes it nearly impossible to use the mouse proficiently for FPS games!

    Any insight? I believe this is a pretty big deal as the mouse is a powerful pointing tool for games!

  11. Currently, no. There's no way short of using a Java applet or similar plugin to get FPS style movement, or anything else that requires additional control of the mouse. This is an issue that is receiving attention, however, and proposals are in place and being implemented as we speak!

    I'm not sure when this will start appearing in browsers, but I've heard that it should start showing up in Chrome dev builds sooner rather than later. I'm certainly looking forward to it when it does!

    In related news, there are also joystick APIs in the works (Glee!), though I have a hunch that they'll be a bit longer before we can play with them. There's a couple of competing proposals at the moment.

  12. Fantastic! Thanks Brandon!

    I hope you update your full-screen Quake3 viewer when this API comes into being! ;)

  13. You wanted to know the coolest device we got this to run on. Well, it runs on my Motorola ATRIX in Firefox 6. I got 16-20 fps. touch controls are quite twitchy.. but it worked !

  14. I got this to run on my iPad 2 (had to use some MobileSubstrate trickery to enable WebGL, as it is disabled by default with no way to enable it normally)! It is interesting, but the controls are jerky, and it sucks to fall in the pit because you can't get out :P.

  15. I think this is a pretty interesting project, I was a big fan of quake 3 as a kid and learned to develop a ton of different types of 3D and digital media because of it.

    Whats interesting is I am getting horrible frame rates in Chrome 35, not even sure if my GPU is being utilized, testing in Firefox it runs at a steady 45fps. I am running a GFORCE8800GT, something is up with my Chrome, even my tablet galaxy tab 3 gets better frames...