Building a 60FPS WebGL Game on Mobile

winter-rush-740

Last year I was invited to contribute to the Christmas Experiments website. This site features cutting-edge web experiments by some of the top names in interactive web development. Since WebGL now runs everywhere* I figured I would try to build a game that runs well on mobile devices.

In order to effectively use the few days I had available, I decided to to create a simple ‘endless runner’ in the style of ‘Flappy Bird’ and ‘Temple Run’. For this experiment my objective was to build a playable game that runs at close to 60FPS on mobile.

This post will discuss some techniques to get WebGL content running at 60FPS on mobile. We will be using three.js in the code examples.

Why is 60 FPS Important?

The higher the frame rate, the smoother your content will be. Stutter and lag kills the brain’s flow state. For a game it is especially important that motion is smooth and controls are responsive. Computer screens typically refresh at 60Hz, so this is the maximum bound we aim for. Note that 60FPS is the ideal target, but anything above 30FPS will still look pretty good. Paul Lewis has talked extensively about making websites ‘jank free’ and there are lots of great resources here.

Here is a video of Winter Rush pushing 60FPS on an iPad 4th Gen and a Nexus 4:

A video posted by Felix Turner (@felixturner) on

 

To achieve the FPS target I used the following techniques:

Simplify the 3D Scene

Geometry: Simplify scene geometry by reducing the number of meshes and the vertex count of each mesh. Remember that ‘low poly’ is cool. In this game the trees are simply 2 cylinders: one for the leaves and one for the trunk. There are only 10 trees on the track that are re-positioned as the track moves.

Materials: A big part of a 3D engine cost is in calculating lighting for each face in the scene. The less lights in the scene the better. Three.js materials can be ordered from cheap to expensive like this:

  1. Basic. This is the cheapest material. No lighting calculations are required. You can do a lot with basic materials and image textures.
  2. Lambert. Gives a non-shiny appearance.
  3. Phong. Gives a shiny appearance. In my tests Phong proved to be significantly more expensive than Lambert. For this demo, switching Lambert materials to Phong drops the FPS from 60 to 15 on iOS.

Reuse Objects

This is probably the most important rule for performant web experiences. After object creation on initialization, no new objects should be created during the run of the game. This avoids memory thrashing which causes the browser to choke. Here is a good article on using JS object pools. In Winter Rush we reuse 3D objects (e.g. trees) by resetting their position when they go behind the camera. On every frame, we check if the object is behind camera. If so, we reset its position to be further down the track. We use a THREE.Fog to obscure the trees as they pop in.

MOVING THE TRACK

The snowy floor of the track is a flat plane mesh. We use Perlin noise to generate the height of the terrain (e.g. the Y-coordinates of the vertices). This gives a random but smoothly changing set of bumps. To give the appearance of a seamlessly moving track we use the following technique:

    1. Each frame we move the entire floor toward the camera by a small amount based on the speed of the player.
    2. We check if the floor has moved behind the camera beyond a predefined STRIP_WIDTH amount. If it has, we reset the floor back up the track by the STRIP_WIDTH. We then recalculate the terrain heights by incrementing the Perlin noise position to be equal to the STRIP_WIDTH.

See this in action in this video:

A video posted by Felix Turner (@felixturner) on

Simple Collision Detection

You can do accurate per-face collision detection in Three.js using Raycasters. Lee Stemkoski has a good example here. However this method can be expensive and must be performed for every pair of objects that may collide. In many cases you can simplify collision detection by assuming each object is a sphere and simply measuring the distance between objects.

Note that you may need to manually tweak collision distances and hitbox locations to give a more playable feel. At one point there was an issue where the player could hit objects that were off camera when strafing. The solution was to move the player hitbox out in front of the camera a little. Thanks to @neurofuzzy for the tip.

Combine Shaders

In Three.js the EffectComposer allows you to chain multiple post-processing shaders. This approach requires multiple off screen buffers to pass the result of each shader to the next. This can give bad performance on mobile. The solution is to combine your Shaders into a ‘SuperShader’. This is mostly a matter of copy and pasting the shader code and putting them in the correct sequence. For Winter Rush we combine the Vignette, Brightness/Contrast and Hue/Saturation shaders into one. Also note that some effects are just too GPU heavy for mobile, most notably blurring.

Use Clock Delta

For animation loops we should use Request Animation Frame and the clock delta for animation. This make animation speeds independent of framerate. Travel distances should depend on the actual time that has passed rather than the number of frames. This technique won’t improve your FPS but will improve player’s perception of speed if the FPS does drop.

//kick off animation
var clock = new THREE.Clock();
clock.start();
gameLoop();

function gameLoop(){
    requestAnimationFrame(gameLoop );
    var delta = clock.getDelta();
    //use delta to determine all distances travelled
    movePlayer(MOVE_SPEED * delta);
}

Test on Target Devices

Once you have picked your target devices, continually test on those devices and keep an eye on the FPS. The iOS Simulator for OS X is a great tool for debugging iOS issues on the desktop, but be aware that the simulator does not reflect the performance of the actual devices. Adobe Edge Inspect is another great tool which allows you to easily connect multiple mobile devices to a local webpage. It will automatically reload the page when the page changes and also allows you to access Android console errors.

Good JS Libraries for Mobile Dev

These are all great libs for mobile development:

  • Three.js – goes without saying 🙂
  •  Zepto.js – a fantastic jQuery replacement that is much smaller (25k) and faster on mobile.
  • Howler.js – a great little audio library that handles multiple mobile x-platform issues (such as the iOS click to play sounds issue)
  • TweenLite – make tweening easy. Works well on mobile.

Which Devices Can Run WebGL?

WebGL device support is growing fast. In addition to running on all major desktop browsers, WebGL content now runs on iOS and Android devices.

However not all WebGL capable devices are born equal. WebGL is a demanding technology and older devices will have a hard time running anything but the most basic content. For example, the iPad 2 which came out in 2011 will run WebGL but it’s power is very limited. WebGL typically runs well on mobile devices built in the last 2 years. My primary mobile test devices are an iPad 4th Gen (from 2013) and a Nexus 4 (from 2012) which give a pretty good baseline.

To Do

When I get some more free time I would like to add the following to this project:

  • Tilt controls on mobile . I went with tap to move on mobile since it more closly matches the desktop experience. Using the tilt accelerometer is whole different control system.
  • Fancier Desktop version. Since this game is built to run well on slower devices I had to forego fancier effects and geometry. It would be nice to add a desktop version with richer graphics.
  • Use the Android fullscreen API
  • Move the HTML menu overlay into WebGL and perhaps add some nice shader wobble transitions.

Conclusion

Hopefully these tips will help you build performant WebGL content for mobile. Thanks for reading and let me know your high score in the comments 🙂

7 Responses

  1. About the libraries that you recommends:
    I’m using Three.js and TweenMax too, are great!.

    But for the audio I choose SoundJS to integrate with PreloadJS to preload ogg or mp3 files (but Howler sounds great) I actually trying to mix a SoundManager that I have created to manage music and effects managed with SoundJS, with 3D audios positioned on the scene with THREE.Audio and I wish I could also to analyze to modify shaders and meshes. Do you tried to join Howler advantages with the possibility of analyzing the sounds loaded?

    And about Zepto, I have used JQuery 1.10.2 and when I read your opinion about Zepto I thought about switch it, but it really has a better performance? Because for me with Jquery already works at 60 fps (And I search in Google about performance test between Zepto & Jquery http://jsperf.com/zepto-1-0-vs-1-1-performance/7 and not sure if the benefit outweighs performance level)

    I’m actually making a 3D experience with webGL optimized for mobile and your advices are very useful, thanks!

    • Felix Turner says:

      Those jsperf test results are interesting, but just testing selector speed is not the whole story. Also I notice the results do not include mobile browsers. I believe zepto is faster than older versions of jquery that include legacy IE support, possibly newer jquerys have closed the gap. Zepto is smaller that jquery (25k vs 95k) which is a win especially on mobile.

  2. Great article and great experiment! Thanks for sharing it.

    You can update the run everywhere section to add any Windows Phone 8.1 devices 😉 It works perfectly fine on my Lumia 1520 for instance.

    You can add also FirefoxOS devices that support WebGL

  3. Jonas says:

    Just on a side note, be very careful with variable timesteps (movePlayer(MOVE_SPEED * delta);). It can very quickly lead to problems with your integrator, collision detection (clipping through things when the garbage collector decides to come around) and so on. If the performance somehow allows it I’d always go with fixed timesteps and interpolation in the renderer. If not at very least clamp the delta into a sensible range (if you have a 500ms gc pause for what ever reason you don’t want the player to travel for that time until the next collision detection).

    Cheers,
    Jonas

  4. bullet force says:

    This is really interesting information for me. Thanks for sharing!

Leave a Reply

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