Webgl Models: End-To-End 30
Total Page:16
File Type:pdf, Size:1020Kb
OpenGL Insights Edited by Patrick Cozzi and Christophe Riccio WebGL Models: End-to-End 30 Won Chun 30.1 Introduction When we were making Google Body in 2010 (see Figure 30.1), WebGL was new technology. It wasn’t entirely clear what we’d be able to accomplish; indeed, the uncertainty was one of the reasons for building it in the first place. Adopting a new technology like WebGL was aleapoffaith.Evenifthe idea made perfect sense to us, so many things outside of our control needed to go right. We eventually open sourced Google Body,1 and unequivocally demonstrated that it is possible to build a rich, 3D application using WebGL. Google Body made the use of WebGL less an act Figure 30.1. Google Body, a 3D anatomy browser for of faith, and more an act of the web. execution. 1The content is now available at zygotebody.com. 431 432 V Transfers When we started, the simple act of loading a 3D model was its own little leap of faith. The source 3D model data was incredibly detailed; we ended up shipping a reduced version with 1.4 million triangles and 740 thousand vertices for 1,800 anatomical entities. Our first prototype simply loaded everything using COLLADA, but that took almost a minute to load from a local web server. That’s a really long time to wait for a web page. COLLADA was inefficient in its use of bandwidth and computation, but it wasn’t immediately apparent how we could do better. There is no DOM node or MIME-type for 3D models. WebGL added TypedArray support to JavaScript, but XMLHttpRequest wouldn’t get it for months. Fortunately, we devised a way to load Google Body’s 1.4 million triangles much more efficiently than we expected using a surprising combination of standard web technologies. I’ve shared WebGL Loader, http://code.google.com/p/webgl-loader/, as an open- source project so that other WebGL projects can benefit. This chapter describes how WebGL Loader works. WebGL Loader can compress a 6.3 MB OBJ file to under 0.5 MB, better than a 13:1 compression ratio. GZIP alone shrinks the same file to about 1.8 MB and yields a model file that is slower to parse and render. Taken apart, the techniques and concepts are actually quite simple to understand. I also hope to give insights into the process of how we went about making those choices so that we can be equipped with more than just faith for the next time we are spurred to delve into the depths of technical uncertainty. 30.2 Life of a 3D Model Loading 3D models was the apparent challenge, but it is always important to keep the big picture in mind. When I joined Google, as part of orientation, I attended internal lectures about how the key internal systems at Google work. They all had titles like“Life of a [blank].” For example, “Life of a Query” walks through, step by step, what happens when a user performs a search on google.com. This end-to-end perspective is the key to building computer systems because it motivates what we build, even when we focus on a single component. I spent two years optimizing a tiny corner of web search by a few percent, but I knew the effort was worthwhile because it was in the critical path for every web search by all of our users. If “Life of a Query” teaches us anything, it is that there are lots of queries. It doesn’t make sense to think about loading 3D models outside the context of all the other steps a 3D model takes through an application. Each step informs the de- sign. For Google Body, we thought of the model as it related to four key stages. They are general enough that they should apply to most other WebGL projects as well: • Pipeline. Where does the 3D model data originate? • Serving. How does the 3D model data arrive in users’ browsers? • Encoding. What format should the 3D model data use? • Rendering. How does the 3D model data get to the screen? 30. WebGL Models: End-to-End 433 30.2.1 Stage 1: Pipeline Unless the application is a demo where 3D models are mere stand-ins to showcase some new technology, it is important to understand their creation process, called the art pipeline. The art pipeline can include many creative steps, like concept drawing or 3D modeling, but the key technological steps to understand are the tools that get models from the care of the artists to the application. Art creation tools like Maya, Blender, Photoshop, or GIMP have their own file formats, but applications usually don’t use them directly. The flexibility that is a necessity during creation ends up as baggage after the art is created. Contrast a Photoshop PSD file with a plain old JPG image. The PSD file can contain lots of extra sophistication like layers or color transforms that are essential for lossless, nondestructive editing, but that will never get used by something that simply needs to present the final image. A similar thing happens with 3D models. For example, 3D modeling software keeps track of positions, normals, texture coordinates, or other vertex attributes in separate lists. A vertex is represented as a tuple of indices, one for each vertex at- tribute. This is an excellent choice for creation and editing; if an artist changes an attribute like a position or texture coordinate, all the vertices that share that attribute are implicitly updated. GPUs don’t support these multi-indexed vertex lists; they must first be converted to a single-indexed vertex list. WebGL additionally enforces a limit of 65,536 unique vertices in a single drawElements call for maximum porta- bility. Larger meshes must be split into batches. Somewhere along the line, the model needs to be converted from a multipurpose, creation-appropriate form to a streamlined, render-appropriate form. The best time and place to do this is in the art pipeline. From the perspective of the application, everything that happens in the art pipeline appears to happen magically ahead of time. Since more time can be spent in the art pipeline than during loading, it is a great opportunity to offload work from the stages ahead. This is where to optimize the mesh for rendering and compress it for serving. 30.2.2 Stage 2: Serving We don’t have much control over what happens between the server and client because it is all governed by Internet standards; all we can do is understand how things work and how to best use them. I learned about high-performance web serving during my first project at Google, optimizing infrastructure behind iGoogle.2 As it turns out, serving 3D models over HTTP is not that different from serving other kinds of static contents, such as stylesheets or images. Almost everything in this section applies equally to images and to meshes since it is all about how to send bits onto the client. Perhaps more accurately, this section is about how not to send bits to the client using that reliable system design workhorse: caching. The bits we never send are the fastest and cheapest of all. 2iGoogle is the customizable homepage for Google: http://www.google.com/ig. 434 V Transfers HTTP caching fundamentals. Before a browser makes an HTTP GET request to download the data for a URL, it first checks the cache to see if the data corresponding to that URL are already present. If so, the browser potentially has a cache hit. The problem with simply using these data right away is that they might be stale; the data in the cache might not reflect what is actually being served anymore. This is the trouble with caches; they seem like a cheap and simple way to improve performance, but then we have to deal with the extra complexity of validation. There are several ways a browser validates its cache data. The basic approaches use a conditional GET. The browser remembers when it originally fetched the data, so it uses an if-modified-since header in a GET request, and the server responds with a new version only if necessary. Another approach is to use ETags, which can act as a content fingerprint so the server can detect when a URL has changed since last requested. These approaches work as we would expect, but both still require a round-trip communication with the server. These requests, while usually cheap, still consume connections, a limited resource on both the client and server, and can have unpre- dictable latencies depending on the state of the network. It is better to completely avoid this parasitic latency with a simple tweak in the server. Instead of forcing the browser to ask, “Is this stale?” for each request, the server can proactively tell the browser how long the data will be fresh by using an Expires header. With an explicit expiration, the browser can use locally cached data immediately. AlongExpires header (the longest supported is just under a year) improves caching performance, but makes updates less visible. My favorite approach to deal with this is to simply never update the data for a given URL by using unique URLs for each version of the data. One particularly simple way is to embed a content fingerprint in the URL—no need to maintain any extra state. Fingerprinting works best if you control the references to your fingerprinted as- sets; otherwise, clients can end up with stale references.