Naive fallback to canvas from WebGL

Original Author: Rolando-Abarca

Disclaimer

Falling back to canvas should be used only when your game is really simple and has no fancy shader. It can also be used to provide the player with a subset of the experience of your game, for instance for mobile devices, when there’s no WebGL support. So what I’ll descrive here only works for really simple games and perhaps, to just show a limited experience instead of a “download Chrome to play this game”

The theory and implementation

When writing ChesterGL I realized that if I kept the rendering logic isolated enough, I could easily create a thin layer for adding new rendering techniques, like Canvas API or plain DOM. The rationale behind is that all the math is done in a very general way (using the good old matrix transformations), so I just needed to think through it. One thing that makes things easier, is that for every image (sprite) rendered on screen, you don’t really need to position the image, just set the transform of the current context before drawing and that’s it. And of course, you can easily get the transform from the current model-view matrix:

ChesterGL.Block.prototype.render = function () {
 
  	if (ChesterGL.webglMode) {
 
  		// ... the usual WebGL way
 
  	} else {
 
  		var gl = ChesterGL.offContext;
 
  		// canvas drawing api - we only draw textures
 
  		if (this.program == ChesterGL.Block.PROGRAM.TEXTURE) {
 
  			var m = this.mvMatrix;
 
  			var texture = ChesterGL.getAsset('texture', this.texture);
 
  			gl.globalAlpha = this.opacity;
 
  			gl.setTransform(m[0], m[1], m[4], m[5], m[12], m[13]);
 
  			var w = this.contentSize[0], h = this.contentSize[1];
 
  			var frame = this.frame;
 
  			gl.drawImage(texture, frame[0], texture.height - (frame[1] + h), frame[2], frame[3], -w/2, -h/2, w, h);
 
  		}
 
  	}
 
  }

I added an option to set the opacity of the sprite as well by changing the state of the context before drawing. If you look at the drawImage call, we can even support sprite sheets very easily. So with only a few lines, you can support WebGL sprites as well as pure canvas API sprites. Since we use the same matrix for WebGL and canvas, the whole scene graph is preserved so everything else works the same way.

So where should you start this? I did it in the initialization code, where you create the WebGL context from the canvas, if that fails, then create a 2d context:

/**
 
   * tryies to init the graphics stuff:
 
   * 1st attempt: webgl
 
   * fallback: canvas
 
   */
 
  ChesterGL.initGraphics = function (canvas) {
 
  	try {
 
  		this.canvas = canvas;
 
  		if (this.webglMode) {
 
  			this.gl = canvas.getContext("experimental-webgl");
 
  		}
 
  	} catch (e) {
 
  		console.log("ERROR: " + e);
 
  	}
 
  	if (!this.gl) {
 
  		// fallback to canvas API (can use an offscreen buffer)
 
  		this.gl = canvas.getContext("2d");
 
  		if (this.usesOffscreenBuffer) {
 
  			this.offCanvas = document.createElement('canvas');
 
  			this.offCanvas.width = canvas.width;
 
  			this.offCanvas.height = canvas.height;
 
  			this.offContext = this.offCanvas.getContext("2d");
 
  			this.offContext.viewportWidth = canvas.width;
 
  			this.offContext.viewportHeight = canvas.height;
 
  			this['offContext'] = this.offContext;
 
  			this.offContext['viewportWidth'] = this.offContext.viewportWidth;
 
  			this.offContext['viewportHeight'] = this.offContext.viewportHeight;
 
  		} else {
 
  			this.offContext = this.gl;
 
  		}
 
  		if (!this.gl || !this.offContext) {
 
  			throw "Error initializing graphic context!";
 
  		}
 
  		this.webglMode = false;
 
  	}
 
  	this['gl'] = this.gl;
 
   
 
  	// get real width and height
 
  	this.gl.viewportWidth = canvas.width;
 
  	this.gl.viewportHeight = canvas.height;
 
  	this.gl['viewportWidth'] = this.gl.viewportWidth;
 
  	this.gl['viewportHeight'] = this.gl.viewportHeight;
 
  }

There are some things that will not work, the most obvious being batched sprites (BlockGroup in ChesterGL) and shaders. But if you want to have a very simple fallback line, this could work.

For clearing the screen, you have two options: either clear the whole rect, or paint it some color. I opted for drawing a black rectangle to simulate the glClear in WebGL:

/**
 
   * main draw function, will call the root block
 
   */
 
  ChesterGL.drawScene = function () {
 
  	if (this.webglMode) {
 
  		// WebGL draw mode here
 
  	} else {
 
  		var gl = this.offContext;
 
  		gl.setTransform(1, 0, 0, 1, 0, 0);
 
  		gl.fillRect(0, 0, gl.viewportWidth, gl.viewportHeight);
 
  	}
 
   
 
  	// start mayhem
 
  	if (this.rootBlock) {
 
  		this.rootBlock.visit();
 
  	}
 
   
 
  	if (!this.webglMode) {
 
  		// copy back the off context (if we use one)
 
  		if (this.usesOffscreenBuffer) {
 
  			this.gl.fillRect(0, 0, gl.viewportWidth, gl.viewportHeight);
 
  			this.gl.drawImage(this.offCanvas, 0, 0);
 
  		}
 
  	}
 
  }

I even let the option there to use an offscreen buffer for drawing (something like double buffering). I did some quick performance test and I couldn’t find a big difference between using fillRect instead of clear. Also, we need to set the transform to the unit matrix.

Conclusion

It can be very trivial to have a simple fallback to canvas API, but you must keep in mind what you will use it for. One concern would be performance: it is definitively not bad, but it’s not as good as it can be with WebGL, also you will lose all the cool things you would be able to do in WebGL, like adding 3D objects to your 2d game, 3D effects/transitions or fancy shaders.

One way to optimize the canvas API rendering would be to do not draw the whole screen and use just dirty rects to only draw the things that where modified. Another optimization would be to port BlockGroup (batched sprites) and draw those sprites to an offscreen buffer, this would work great for tiled maps or backgrounds.

That’s it… easy and simple fallback to canvas API when WebGL is now available for your game. Oh, and it also works on iOS! I got ~26fps with 12 moving sprites on iOS 4.3.5 and ~35fps with 42 moving sprites on iOS 5 – pretty good for canvas!

If you want to try this technique, you’re more than welcome to download github (but please note that ChesterGL is still a work in progress that I maintain on my free time). If you’re also interested, you can read the original article where I introduced ChesterGL.