Categories
General

HTML5 canvas sprite optimisation

Note : this post is from 2011, the techniques described here are probably no longer necessary


If we’re serious about making HTML games then we need to know the most efficient ways to render multiple sprites. Many are saying that Canvas isn’t fast enough for gaming and we should use DOM objects instead. But before we give up Canvas altogether, let’s see if we can squeeze out just a little more performance…

A little while ago my podcasting mate Iain Lobb converted his sprite blitting test “Bunny Benchmark” to HTML5 canvas and was impressed with the results – he got 3000 bunnies rendering quite comfortably on all of his browsers.

But I was massively unimpressed – I could only get 5-10fps on all of my browsers. The reason for the difference in performance – Iain’s on Windows and I’m on OSX.


I did some hunting and I found this benchmark on JSPerf which showed that if you call drawImage on the Canvas element, it’s much faster if you round the x and y position to a whole number.

Whole pixel vs sub-pixel bunny
Whole pixel bunny vs sub-pixel bunny

If you draw into canvas using sub-pixels (not whole numbers) the browser will interpolate the image as though it was actually between those pixels. It’ll give you much smoother animation (you can genuinely move at half a pixel per update) but it’ll make your images fuzzy.

And it seems that browsers in OSX are super slow at doing this interpolation.

The solution : round your x and y position to whole numbers before rendering.

[GEEKY DIVERSION]

A quick search on JSPerf shows that using a sneaky bitwise operation is faster than the built in Math.round.

If you apply a binary NOT (represented by the tilda : ‘~’) every bit that was at 1 is now 0 and vice versa. If you do that again, the number is switched back to what it was. Except that any bitwise operation takes away any digits after the decimal point, which rounds it down to the nearest whole number.

But we’re trying to round to the nearest whole number, not just round down (like Math.floor). So if we add 0.5 first it rounds up to the next number if we’re close to it, so the final operation is:

x = ~~ (x+0.5);

Note that this only works on positive numbers!

[UPDATE] thanks to lab9 in the comments below who reminded me that you can do a binary OR with 0. This compares every binary digit in your number to every digit in 0 :

00110110010 OR
00000000000 =
00110110010

In other words, it does nothing! But again, because it’s a binary operation, you lose everything after the decimal point. I just added it to the JSPerf test and it’s even faster – thanks again lab9!

Now I wouldn’t normally advise premature optimisation, especially if it makes your code unreadable, but as we’re making a benchmark I thought it would probably be fair.

[GEEKY DIVERSION ENDS]

And so without further ado, see the results for yourself:

Bunny Benchmark with snapping option

I get a massive improvement on most browsers – 7fps to 30fps. It’s only Firefox that only improves marginally. 7fps to 10fps, even on FF4. Is it the same for you? Is there a improvement on Windows with snapping? Do any of you know how I could implement something like this on JSPerf.com? I’d like to hear from you!

Coming next: DOM object bunnies!

We’ll be covering this and more at my Creative JS workshops – next one in Brighton, only one space left!

38 replies on “HTML5 canvas sprite optimisation”

Cheers Seb, this should help one of my current canvas tests run faster 🙂
Chrome 9 on Windows I get a very subtle ~57fps without snapping and about 60 with.

I’m on MacBook Pro 2.8Ghz C2D 4Go 10.6.6 and I get:
FireFox 4b10: 8fps vs. 15fps
Chrome 8: 9fps vs. 30fps
Opera 11: 8fps vs. 25fps
Safari 5: 12fps vs. 30fps

Windows 7 – 2.8 Ghz – dual core
Firefox 3.6: 2 to 3 fps. With snapping 2 to 8 fps
Chrome 8: 57 fps (I don’t see any change with snapping)
Safari 4: 12 fps. With snapping 16 to 29 fps

The slow ones can really drop the frame rate with simply moving the mouse around.

actually I did the jsperf test on firefox 3.6 and.. the fastest method is Math.round! and the difference is huge. Probably because the engine is trying to do some optimizations itself.
so, no easy choices here.

You make an excellent point here wildcard. The difference in performance is imperceptible compared to the improved rendering performance. The lesson : always concentrate on the rendering bottleneck first!

I’m loving how Iains bunnies keep appearing everywhere…

Btw, I seem to remember flash used to need hacking to make it run just as fast on a mac. About 5-10 years ago.

You’d think Mr Jobs would give this issue his attention…

I have updated the number of test cases : //jsperf.com/math-round-vs-hack/4
and I was amazed to see how wildly different the results are depending on the browser. I would suggest using the method which seems the most consistent across browsers, which SEEMS to be the double-not hack.

Note:
All tested on a macbook pro using arora, camino (which doesn’t seem to show up on the results table), chrome, firefox, opera and safari

interesting points, just to add another finding:
IE9 platform preview on Win7 64 ~45-51fps almost equal in both settings. Seems only FF improves a bit (3.6.13) but still way below IE9 ~50 and Chromes ~60 fps…

I’ve added a couple more stuff to the JSPerf: //jsperf.com/math-round-vs-hack/5 – check the notes at the beginning as well.

I’ve wrote a couple months ago about premature/micro optimization (//blog.millermedeiros.com/2010/10/the-performance-dogma/) and in this case I believe that rounding shouldn’t be the bottleneck (as you also said) so it shouldn’t be optimized unless you really need it and know the target environment (since results vary so much).

It is really good to know that avoiding sub-pixel rendering can improve performance that much. Thanks for sharing.

That’s super fast – very nice… im curious to see if you made the canvas element the full browser size how it would perform.

This brings up another point of optimization that is a double edged sword.
Clearing the entire canvas, is usually slower than clearing only rect only the ‘dirty’ areas, but at this high volume, it would probably be much slower.

i think ill try making a test to confirm

Well, my FF got 1 fps unchecked and 5 checked, so improvement was massive.
However, when I switched to chrome (which finally showed me what I was supposed to see) – 30 fps either way

Using Chrome (on Windows 7) I see no FPS difference with snapping on or off – it always stays above 50 – usually at about 56 or 57 regardless.

I’ve never seen sub-pixel on windows (where I do all my canvas coding).
So OSX is slower because it does something extra.
But sub sampling could be useful sometimes, so i’d like an option to switch it on-off directly form the canvas API.

Hi Omiod,

I’m pretty sure you get sub-pixel on Windows but I’d love for you to check! Just render a single bunny at a position that changes 0.05 every frame. If the bunny appears to move smoothly, you got sub-pixel. If he snaps from pixel to pixel, you don’t. I’d love confirmation on this one way or another!

Seb

[…] HTML5 canvas поддерживает суб-пиксельный рендеринг и нет никакой возможности его отключить. Если вы рисуете с нецелыми координатами, он автоматически использует анти-алиасинг, чтобы сгладить линии. Вот визуальный эффект суб-пиксельной производительности из статьи Seb Lee-Delisle: […]

Having done it before, I would advise AGAINST attempting to do a game in the DOM instead of using a CANVAS tag. There are extreme difficulties when it comes to getting consistent positioning of sprites between different browsers, even if you use position:absolute.

Browsers have no respect for your game’s coordinate system. The appropriate top and left attributes to use to position a sprite may depend on the user’s Zoom setting. The numbers reported by things like window.innerHeight also depend on the user’s Zoom setting. The size of your image as reported by its width and height properties, OTOH, may or may not be affected by the user’s Zoom setting.

There is also no way to reliably mix DOM objects with a CANVAS. Mario may fall into a pit on Chrome but fall into the solid ground to the left of the pit in Firefox.

There is no way to programmatically test of any of this in JS.

DOM rendering is so fucked up, that you really shouldn’t try doing anything more than simple documents with it.

What about aliasing when scaling sprites? I’m using drawImage with all the parameters to scale sprites and if I scale the sprites up there is a lot of smoothing going on. Any way to get rid of that?

[…] Ein wesentlich wichtiger Punkt, den man sich merken sollte wenn man mit Canvas arbeitet, ist: Canvas-Koordinaten sind Integer oder, passender formuliert, ganze Zahlen! Obwohl das Drawing API auch Fließkommazahlen als gültige Werte entgegennimmt, merkt man spätestens beim Zeichnen, dass sich hier einige Frames gewinnen lassen. Sprich, vor jedem Zeichnen sollten die Koordinaten und Längenangaben zu ganzen Zahlen gewandelt werden. Positiver Nebeneffekt: Die Kanten der gezeichneten Elemente bleiben scharf [10]. […]

Comments are closed.