WebGL & EaselJS: a Technical Intro

Our past experiments demonstrated that there is no benefit to building a general purpose WebGL renderer for EaselJS. Modern canvas implementations already do a great job at rendering arbitrary 2D content on the GPU, and they use native code to do the heavy lifting. We’re not going to beat them in JS.

However, it is possible to realize huge performance benefits with WebGL if you place appropriate restrictions on content. The new SpriteStage & SpriteContainer display objects provide the familiar EaselJS APIs, but add some limitations so your content can be aggressively optimized for WebGL rendering.

The new APIs are able to fall back seamlessly to canvas 2D if WebGL is not supported, to ensure your content can run anywhere. Further, a number of integration points allow you to leverage the high performance WebGL renderer and the more robust canvas renderer together easily, giving you the best of both worlds!

SpriteStage & SpriteContainer
SpriteStage manages a WebGL stage. It extends the existing EaselJS Stage you know (and hopefully) love. It adds a few new methods, but most importantly it restricts addChild() to only allow display objects that can be drawn to WebGL easily. This includes Bitmap, Sprite, BitmapText, and SpriteContainer, but not Shape, Text, or Container.

var mySpriteStage = new SpriteStage("mycanvas");
mySpriteStage.addChild(myBitmap, mySprite, myBitmapText); // good
mySpriteStage.addChild(myText); // error: content not allowed

Similarly, SpriteContainer extends Container, and enforces its own set of content restrictions. Each SpriteContainer has an associated SpriteSheet specified, which must be shared by the Sprite, BitmapText, and SpriteContainer instances it contains. Additionally, only sprite sheets using a single image are allowed.

var mySpriteContainer = new SpriteContainer(mySpriteSheet);

var myGoodSprite = new Sprite(mySpriteSheet, "idle");
mySpriteSheetContainer.addChild(myGoodSprite); // good

var myBadSprite = new Sprite(otherSpriteSheet, "run");
mySpriteSheetContainer.addChild(myBadSprite); // error: sprite sheet doesn't match

What this ultimately means is that each “branch” from the SpriteStage “root”, no matter how deep or complex, can be drawn with a single image. This means that each branch of the display list can be batched into a single draw call – that makes your content REALLY fast!!

For the most part, display objects can be manipulated normally (x, y, rotation, scaleX/Y, alpha, etc), but some advanced features are ignored in WebGL, such as masks and filters.

While it might sound restrictive at first, it’s actually quite powerful. In a game, your stage could contain a Bitmap backdrop, a SpriteContainer containing your terrain Sprites, another SpriteContainer with character Sprites, and a top SpriteContainer with BitmapText instances and Sprites to display your UI / scores. Each of these SpriteContainers could use its own SpriteSheet.

It’s also worth noting that DOMElement works just fine as a child of SpriteStage and SpriteContainer, because it doesn’t need to be rendered to WebGL.

Automatic Fallback
If WebGL is not supported on a device, the SpriteStage will automatically fall back to using the canvas 2D renderer. Of course, this could cause serious performance issues if your content is dependent on the rendering power of WebGL. You can use SpriteStage.isWebGL to check which renderer is being used, and adjust your content as appropriate.

if (mySpriteStage.isWebGL) {
	numParticles = 20000; // go nuts!
} else {
	numParticles = 2000; // careful now.
}

SpriteStage + Stage
There are also a number of ways to use the fast WebGL renderer with the more robust canvas renderer.

Because you can use a canvas as a source image, you can wrap a Stage’s target canvas in a Bitmap, and add it to a SpriteStage. The Stage can have complex animated vector content, and it should show up in the SpriteStage just fine. It’s also possible to reverse this trick to include a SpriteStage’s WebGL canvas in a Stage.

var stageBmp = new Bitmap(myStage.canvas);
mySpriteStage.addChild(stageBmp);

Similarly, you can leverage the SpriteSheetBuilder class to create sprite sheets using the canvas renderer, then use them in WebGL. For example, this could be used to create bitmap fonts from existing TTF fonts at runtime, apply filters like color shifts to loaded sprite sheets, or generate sprite sheets from vector content.

Finally, it’s easy to layer a Stage canvas with a SpriteStage canvas, and pass through mouse/touch interactions using the upgraded Stage.nextStage API. Even interactions like mouseover/out will work correctly between stages.

// pass mouse events through to the next stage.
myTopStage.nextStage = myBottomStage;
// chain multiple stages together:
myBottomStage.nextStage = yetAnotherStage;

You can have multiple stages, all chained together in this manner. The canvases can be different sizes, and even be updated independently, so you could have an ultra fast, full screen game engine rendering to WebGL at 60fps, with a beautiful vector UI overlay rendering into a half height canvas at 20fps.

Getting Started
I recently uploaded a couple of simple examples to the EaselJS Github repo in a /examples/WebGL/ directory. The “Runners” example shows how to work with SpriteStage and SpriteContainer, and how to adjust your content to work well when running on a device that lacks WebGL support. The “TwoStages” example uses nextStage to seamless pass mouse interactions between layered canvases.

Check out Planetary Gary for a more complex game example that uses these new APIs. Only 3 lines of code had to be changed to make it work with WebGL (add a second canvas, create the SpriteStage, and use SpriteContainer instead of Container in the game engine). Planetary Gary also uses SpriteSheetBuilder to convert tiny (85kb gzipped) vector graphics into a WebGL-friendly sprite sheet at run time.

We should be adding additional examples over the next couple of months as we polish things up for an official release. My hope is that existing EaselJS devs will find this really simple to use, and that the combination of the robust & feature rich Stage renderer, with the extremely fast SpriteStage renderer will let web developers build some truly amazing content. If you try it out, we’d love to hear your feedback!

38 thoughts on WebGL & EaselJS: a Technical Intro

  1. Very nice indeed. However one question bothers me a lot: There is a Graphics object in the Shape, and it is what we use for drawing rects, circles, etc.. If you not allow Shape anymore, what about Graphics object ?

    • As mentioned in the article, the goal of SpriteStage is to render bitmap content really quickly. Rendering vector content in WebGL is possible, but requires a huge investment to get right, and it will never be particularly fast.

      As Makzan suggested, you can absolutely rasterize your vector content at runtime, and then use it as bitmaps in SpriteStage. Do this either via .cache() and .cacheCanvas, or even better, with SpriteSheetBuilder.

  2. Hi Grant, thanks a lot for the WebGL update

    I tried to play planetary Gary but it doesn’t load the game for me (firefox, Mac):
    http://sandbox.createjs.com/PlanetaryGary/

    Also with regards to this “iOS Safari has a major bug which currently results in very poor performance. Apple has been made aware of this issue, and I am attempting to isolate it.”

    Does this apply to any EaselJS project that uses WebGL or just Planetary Gary?

    Also I’ve come across a canvas/iPad performance issue in the past, I wonder if it’s related:
    https://www.scirra.com/forum/viewtopic.php?t=74851&start=0
    (note there is a workaround)

  3. Hi ,

    I use php editor jetstorm brain. How do I insert createjs or easejs or soundjs plugin in to my editor, so the editor can provide autocomplete from createjs or easejs library. What should I do? Thanks

  4. Hi,

    Im trying to resize the SpriteStage canvas after initialization. It leads to unexpected results. The canvas is resized, but the content gets rendered in a square matching the initial width/height of the canvas.

    and then on resize set:

    _webGlCanvas.width = window.innerWidth;
    _webGlCanvas.height = window.innerHeight;

    It will keep rendering the content in a 600/600px square, that moves around on the y axis.

    Am i doing something wrong here ?

    Thanks

    • Hi Torkiel, there’s an API on SpriteStage called “updateViewport” that you can use for this.
      Ex)
      spriteStage.updateViewport(window.innerWidth, window.innerHeight);

      Note that this will not update the width/height of the canvas element itself so you’ll still have to go:
      _webGlCanvas.width = window.innerWidth;
      _webGlCanvas.height = window.innerHeight;

  5. I’m doing some looking into how we can enable this for CodeCombat, and I’m running into trouble. We’ve got a bunch of high-res sprite sheets in our game, and in order to fit them all into a single sprite sheet so they can be ordered properly in a single SpriteContainer, we’d need to make them much lower resolution, or render them with something other than SpriteSheetBuilder, and we’d prefer not to do either of those.

    To give a concrete example, say I have two Soldier Sprites and two Munchkin Sprites fighting each other. To render them to the resolution I want, they cannot both fit on the same single SpriteSheet image. But given their positions in the game, I want SoldierA drawn, then MunchkinA drawn, then SoldierB drawn, then MunchkinB drawn. It seems with this system impossible to do. The Soldier Sprites would both have to be in one SpriteContainer while the Munchkin Sprites would have to be in another, and you can’t interweave Sprites that have different parents.

    I’ve been talking with some contributors and they mention that WebGL ought to be able to support multiple image assets with interwoven layers rendered using the depth buffer. Is there a chance the restriction that a SpriteContainer must have one and only one SpriteSheet associated with it? Or is there some other solution I’m not seeing? Or am I misunderstanding how graphics work entirely? :)

  6. @Scott – we’ve tested supporting multiple images previously, and found that it negated a lot of the performance benefits of the WebGL renderer. We were edging closer to reimplementing what a hardware accelerated Canvas2d implementation already does.

    Since the goal is to provide a super-fast but limited WebGL renderer, alongside the full-featured (but still quite fast for sprites) C2D renderer, we ditched the feature for the first release.

    We have some other ideas that may provide better results, but they will take some time to test, and we’re a little tapped out at the moment (gotta pay the bills).

    If you’d like to take a look at this, I can definitely have one of my guys document some of our thoughts on next steps. Let me know.

    • Thanks for the response! We certainly understand the need to husband time resources. Other than subscribing to this blog and watching the EaselJS GitHub repository, how can I keep tabs on further developments?

    • > We were edging closer to reimplementing what a hardware accelerated Canvas2d
      > implementation already does.
      WebGL works fast without batching, this can be seen in Phaser or PIXI demos.

  7. The Gary game runs smoothly in iOS 8 (which supports WebGL in Safari). You do die soon though, as there are no arrow keys to press. Could be worth adding in some touch controls.

    The only strange thing is that when you scroll the page the game takes a moment to catch up, and then doesn’t settle in exactly the right place.

    • Jimmy – Similarly to how you can integrate WebGL and C2D within CreateJS, you can do the same thing with WebGL. A WebGL canvas can be used as a Bitmap source for EaselJS – for example, a 3D model spinning inside a 2D UI. Likewise a EaselJS canvas can be used as a texture within ThreeJS – such as a an animated HUD or display inside a 3D world.

    • I think “unusable” is a stretch. As the article states, the restrictions allow SpriteContainer/SpriteStage to offer some considerable performance benefits.

      It may be possible to support multiple spritesheets, but opening this up has huge performance implications. Once you lose that benefit, you might as well use Canvas2D, which is already accelerated fairly well by the browser. In fact there have been some tests by the community members, and there was a significant drop-off in performance.

      • > but opening this up has huge performance implications.
        I think “huge” is a stretch. PIXI works very fast with multiple spritesheets.

        > Once you lose that benefit, you might as well use Canvas2D
        No. WebGL with mutliple spritesheets works much faster than canvas.

        something like:
        WebGL with 1 spritesheet – 100% performance
        WebGL with 10 spritesheets – 95% performance
        CANVAS – 5% performance

        • That is counter to the tests we ran while implementing this. We are always interested in improving performance. Feel free to take a stab at implementation, we would be happy to accept a pull request.

  8. Pingback: HTML5 CanvasとWebGLの使い分け―ICS LAB

    • There might be gains due to the fact that it restricts the number of images, which should play nicer with the GPU — however you can get the same effect by conforming to that requirement using the regular Stage.

    • You can extend any class in CreateJS using the createjs.extend method. There is nothing in SpriteContainer preventing it from being extended, but you will have to conform to the requirements in SpriteStage to use it properly.

  9. Hello Grant,

    I’m working with easel for a long moment now, and I just migrate to webgl iteration, because my game need good performances on mobile : it’s a bullet hell, and in my point of view it’s awesome (your library, and maybe a little bit my game too :))

    I have a question : I have bitmaped a font on my spritesheet, but, I need to use different size on those bitmaps, and obviously it’s not very beautiful when the size is decreased.
    So what about using different bitmaps for bitmapText class, for one character, and different size ?

    One day, I need to send you the demo, to see your awesome library in action !

    Cheers, herzuull

  10. Hi,

    I’ve just been experimenting with this. On a regular createjs.Stage, I used Containers to replicate “Layers” like in Flash. For example:

    var main = new createjs.Container(); // all interactive elements here
    var background = new createjs.Container(); // all background elements here

    stage.addChild(background, main)

    Now from my understanding, the equivalent of Container for createjs.SpriteStage is ‘SpriteContainer’. I’m also restricted to only adding children which use the same image as the sprite sheet passed into the SpriteContainer. With this being the case, how should I handle “layering”?

    The approaches I know so far is to maintain the order for every SpriteContainer I use and add them in to the stage this way:

    this.stage.addChild(backgroundTree1, backgroundTree2, menuHomeBtn, menuBackBtn)

    or create multiple canvases and assign multiple stages to them.

    None of the above seem like good approaches if I had to handle 1000’s of Sprites.

    Thanks for your help

    Jonny

  11. Pingback: Javascript Resources – </>

Leave a Reply

Your email address will not be published. Required fields are marked *

© Copyright 2017