Friday, August 16, 2013

Holistic WebGL

ho·lis·tic  
Adjective
  1. Characterized by comprehension of the parts of something as intimately interconnected and explicable only by reference to the whole.
  2. Characterized by the treatment of the whole person, taking into account mental and social factors, rather than just the physical...
Definition from Google Search

As I've established way back on the very first post on this site, WebGL is awesome. So awesome that one might be tempted to start seeing opportunities everywhere around the web to throw some 3D goodness at it. After all: when your hammer is a 3D API, every problem looks like a vertex array... er, or something like that. Now never let it be said that I discouraged the use of WebGL, but as developers we should also be aware that every technology has a time and place.

What is holistic WebGL? It's application of the technology in a way that takes in to account the entire web experience, not just the content of your canvas tag. Holistic WebGL content is considerate about it's context and respectful of the user's time, attention, and battery life. In essence, it's the exact opposite of this:

Now I'm guessing that very few visitors to this site were actively working on "Punch the Polygonal Monkey" banner ads, but it's not hard to envision such a thing. After all, we all know that "eye catching" is an advertiser's friend. What's a poor ad designer to do now that their beloved Flash is being slowly smothered? Why, hellllooooo there HTML5! What lovely WebGL you have! And now we have pages where a deluge of banner ads all rendering beautifully shaded cars, watches, and pharmaceuticals makes your battery commit suicide, your fans squeal, and your page chug.


But wait! It doesn't have to be like that! WebGL can be used in a way that's both exciting and engaging while simultaneously being non-obtrusive and (crucially) non-rage-inducing. Much of it involves simply being aware of what place your WebGL content in the page as a whole.

"Full Page" WebGL games and apps.

The easiest scenario to envision is a WebGL game or WebGL-centric app (like the new Google Maps). This will most likely be the primary content of the page, and will probably be very large or even full-screen. Most other page content will be secondary to it. This is going to be the most permissive scenario, and allows developers to focus on high performance, detailed graphics with the assumption that the user is continuously interacting with it. Given this environment it's easy to think that you have the whole GPU to yourself. But wait! Even in this best-case scenario you need to take some basic steps to ensure that your application recognizes the realities of the web.

The user can switch tabs at any time!

Have you actually seen how people use web browsers? Honestly it's kind of horrifying. It's not unusual for people to have 100+ tabs open across several different browser windows in a session that's been running continuously since sometime in the late 90s. Of course, it's the browsers job to deal with that kind of load, not yours, but it does demonstrate a point: Unlike your standard desktop games where the app went fullscreen and that's pretty much all your PC did till the user quit when you're talking about content in the browser you must expect that your page may be shoved into the background at any moment, no matter how engaging you make it. Planning for this and handling it gracefully is an absolute necessity for any WebGL application that wants to be seen as more than a curiosity.

Fortunately the browser provides several mechanisms for coping with this. First and foremost if you're using requestAnimationFrame (and if you're not then you are doing it wrong!) then the browser can automatically scale back how frequently your render routine is called when your tab is not in the foreground. This behavior is "free" to you and simply a benefit of using the right API for the job.

However, we can do better! If the user isn't looking at your page why animate it at all? For this we have the Page Visibility API. This API will notify your page when the user can't see it anymore. This may be for any number of reasons: The user switched tabs, minimized the window, changed desktops, maybe even covered up the window with another window. The exact scenarios are up to the browser, but in general all you should care about is "The user can't see me anymore."

The most user/battery friendly thing to do when you get a visibilityChanged notification is to completely pause your app. Stop animating, halt your game logic, kill any sounds, and generally shut down any other aspects of your app that continuously update. In some games that may not be an option (multiplayer or other timing sensitive games), but in these scenarios you should still stop any visuals and sounds until the user comes back.

If you do pause a game, though, resist the temptation to resume the moment you are notified that the page is visible again. Show your "pause" screen instead, let the user take a moment to reorient themselves, and allow them to choose when to resume. It's far less startling and frustrating that way. Keep in mind that your user may be returning to you many days after they left! Don't expect them to jump right back in without warning.

Nice context you have there, shame if something were to happen to it...

The other big part of respecting the user's browsing environment is to properly Handle Context Loss. As I mentioned a moment ago, the user may navigate away from your page and not come back for a good long time. What are they doing during that time? Using other WebGL apps? Installing new graphics drivers? Playing desktop games? Changing monitor configurations? Maliciously attempting to crash your GPU? Who knows! The point is that there are any number of reasons why the browser may decide that it needs to kill your WebGL context, sometimes even while your page is in the foreground! When this happens it's up to you as the application developer to:
  1. Indicate to the browser "Yes, I can recover from this" by calling event.preventDefault() in the webglcontextlost handler.
  2. When the context is finally restored, reload all of the necessary GPU resources.
For the record, step 2 sucks. It's something that you have to design your entire app around: Keeping track of all the resources being used at any time and being able to rebuild/reload them at a moment's notice. But if you don't, the alternative isn't great. In Chrome, we'll throw the following delightful message up in front of your user, and your WebGL canvas will turn into a black box.


Don't want rats in your HTML? Handle context loss!

WebGL content as an appetizer, not the entrée.

What about the banner ads that we were talking about earlier, though? Or, more specifically how should we handle WebGL content that is somehow secondary to the rest of the page? Beyond ads there are a lot of really cool uses for WebGL content as part of a larger page whole. Here's some great examples:
In these cases WebGL is used not as the centerpiece of the page but as a way to visualize content in a way that static images and 2D rendering simply fail to capture. These sites are unquestionably better off for having WebGL content embedded in them, but they do so in a way that's not antagonistic to the user. So what are the guidelines that should be followed for this kind of content? For one, everything listed above still applies. You should still be sensitive to page visibility and context loss, and for the love of battery life you should be using requestAnimationFrame! But now there's some additional things to take into consideration. (Conversely, many of the following tips can apply to full page apps as well, but they are especially important in mixed-content pages.)

Light on the shaders, please! (And meshes, and textures, and...)

Let's be blunt: If your WebGL content is not going to be the focus of the page it's in your best interest and the users to scale it back a bit. Yes, we know you can render normal mapped, subsurface scattered skin with realistic sweat and veins, but did you really need to load 5 megs of textures for a mesh that takes up 120x240 on the screen? Make sure your content compliments the page, and accomplishes it's goal as minimally as is reasonable. Faster loads make for happier users. (Unless, of course, the entire point of your content was to render realistic skin. Then have at it! WebGL would be awesome in science textbooks!)

Prefer user interaction to starting automatically.

As a general rule, you should let the user tell you when they want to see your content, rather than revving it up the moment you get your page load notification. There's a couple of reasons for this:

  1. Cuts down the annoyance factor considerably. You may have the coolest animation in the world, but if the user has to endure it every time they visit your page it's going to get old fast. This is only exaggerated if there are multiple blinking, swirling, bouncing elements on the page.
  2. Reduces page load time. You can cache your resources on load to make sure your element is ready to go when the user asks for it, but that should only happen when the rest of the critical content is ready. Users will be very forgiving if your 3D widget loads 2-3 seconds after the rest of the page. They will hunt you down with pitchforks if the article text is delayed because you just HAD to show them that spinning cube first.
  3. Battery life! I keep mentioning this, but it's super critical! Especially considering that WebGL is starting to be enabled by default on some tablets and phones. If your page starts cranking up the GPU the moment users land there they will want to spend as little time on your page as possible, and possibly avoid it all together.
Keep in mind that there are ways that you can do this without requiring an explicit "play" button, which can make your 3D elements feel more natural and interactive. Let's say that you create a cloth simulation that interacts with the user's mouse. (Okay, that demo's not WebGL, but work with me...) It would be perfectly reasonable to start the simulation at rest, and render a single static frame instead of continuously animating. Then, when the user's mouse passes over the canvas you can kick the animation loop on and let the cloth react to their movements. (Of course it would be preferable to switch back to a static frame again if when the simulation comes to a rest.) This is great because it provides a fun, discoverable surprise for the user by letting parts of your page spring to life while still remaining respectful of their overall experience.

This isn't a hard and fast rule, of course. Two of the pages I linked earlier had animations that started immediately on page load. It's important to realize the context of these effects though. In both cases they are the only animated element on the page (in view, anyway), and the animation is subtle and not too distracting. Less is definitely more in this case. It's also worth mentioning that in the case of the Acko.net page the rest of the interactive elements wait for the user to interact with them before they start showing off their goodies.

Give the user a way to say "I'm done."

Imagine if YouTube didn't have a pause/stop button? Videos just looped forever as long as the page was up, playing Gangam Style in the background while you browsed other clips, made comments, etc. The collective Internet Rage pointed in their direction would be enough to telepathically melt their servers.

Having some way to stop an animated element for animating is a great way to help your users feel in control. If you don't provide that, you have implicitly told the user that if they want the distracting, battery sucking element to stop dancing around they can always leave your page. If driving users away is your goal then great! Otherwise, give them some mechanism to stop your content.

Case in point: Florian Boesch's page, Codeflow.org, has a subtle little "wandering balls" effect in the header. It's simple, not terribly distracting, and probably not going to strain anyone's CPU. It also has an "On/Off" button next to it. If that simple little effect is worthy of a pause button, so is yours.

Don't draw what hasn't changed.

This is pretty simple: If your content isn't moving, stop drawing! Take Google Maps, for example. Interaction is smooth and beautiful, but when the map isn't moving the image doesn't change. It would be crazy for the maps to sit there and render at 60Hz just to draw the same static image over and over again, and would guarantee that your laptop would overheat any time you left the page open. Obviously unacceptable from a major app like Google Maps, but equally offensive if it's some random simple 3D widget sitting in a corner that only moves on mouse down.

Don't flood the page with Canvases (Canvasi?)

ShaderToy pushes the limits of what's acceptable here, but it works in their case because the shader gallary is the focus of the site. (Note that they also take pains to follow some of the tips listed here, like not animating the thumbnails unless your mouse is hovering over them.)

Not only is moderation in this area important for a good user experience, but it's also something that's currently strictly enforced by the browsers. Both Firefox and Chrome have static limits on the number of WebGL contexts you can have on a page at once, Chrome's limit being 16. That sounds harsh, and eventually we would like to move to a more flexible, resource-centric limit, but we can't simply allow web pages to spin up an unlimited number of GL contexts. At some point you'll run out of the resources to do so, and probably crash your tab in the process. In any case, the limit helps prevent the worst abuses, but as a developer you can be more intelligent about how to allocate your pixels. 

A favorite trick of mine is to have a single canvas that is detached and reattached to the page as needed, replacing static images with live content when requested. This can simplify resource management too, since everything is shared by a one context.

Be kind to users that don't have/don't want WebGL.

WebGL is becoming more ubiquitous, but there's still plenty of browsers (especially on mobile) that don't have access to it. Try to be helpful to these users!

By no means am I suggesting that you should built 2D Canvas fallbacks for everything, but anything beyond showing a blank black rect on the screen is appreciated. In many cases a static filler image hinting at what content the user could be seeing with WebGL support is sufficient. It's also a great idea to provide a link to resources like get.webgl.org that can help users figure out how to get WebGL on their platform. If you do have WebGL controls on your page that are critical to it's function, however, you better be ready to provide fallbacks! (I have yet to see WebGL-based submit buttons, but mark my words: it will happen. Weep for your page accessibility.)

Most WebGL content I've seen does a pretty good job of this already, but it's still worth having on this list.

It's your content! Make it shine!

Everything that's been covered here is great in general terms, but the truth of the matter is that there's no single "Right Way" to make web content of any form, 3D or otherwise, user friendly. A lot of these tips are aimed at games, 3D apps, small embedded visualizations, or ad-like content. But like any good technology WebGL has unlimited uses, and your application of it may completely invalidate some or all of the above. It's up to you to ensure that your content respects your users, but there's a lot of motivation for you to do so! It's safe to say that any content that's put online is put there in hopes of finding an audience of some sort. Content that users find annoying, distracting, or that negatively impacts their performance is not content that people want to share.

Holistic WebGL == Happy Users == More eyes on your awesome content. (Isn't it great when we can all win?) 

2 comments:

  1. Great post!
    Just one small thing to mention: contrary to what everyone thinks (I did too) webgl is officialy not part of html5. It doesnt matter a lot though.

    ReplyDelete
  2. SceneJS now has automatic recovery from context loss - when the context is restored, it just re-allocates VBOs, textures etc from data retained in the scene graph. There's a delay for a moment or two, then it's back in business like nothing happened, with zero disruption to scene state and the app layer. Wasn't hard to do, just needs nodes to remember things like texture params and geometry arrays, outside of WebGL, and for each node have a context restored handler that rebuilds its VBOs etc.

    http://scenejs.org/examples.html?page=webglContextLost

    It's not yet bullet proof though - it really hangs on not exceeding memory on WebGL (and we can't query the available memory yet, alas!)

    ReplyDelete