tags that are z-ordered. This means that you develop your 3D graphics using WebGL, but all your other elements are built using familiar old HTML. The browser composites (combines) all of the graphics on the page into a seamless experience for the user. WebGL is built for dynamic web applications WebGL has been designed with web delivery in mind. WebGL starts with OpenGL ES, but it has been adapted with specific features that integrate well with web browsers, work with the JavaScript language, and are friendly for web delivery. WebGL is cross-platform WebGL is capable of running on any operating system, on devices ranging from phones and tablets to desktop computers. WebGL is royalty-free Like all open web specifications, WebGL is free to use. Nobody will be asking you to pay royalties for the privilege. The makers of Chrome, Firefox, Safari, and Opera have committed significant resources to developing and supporting WebGL, and engineers from these teams are also key members of the working group that develops the specification. The WebGL specification process is open to all Khronos members, and there are also mailing lists open to the public. See Appendix A for mailing list information and other specification resources.
As sexist as the infamous quote may be, I have to say that whenever I code something in 3D, I, like Barbie, get a very strong urge to indulge in shop therapy. It’s hard stuff and it often involves more than a little math. Luckily, you won’t have to be a math whiz to build something in WebGL; we are going to use libraries that do most of the hard work for us. But it is important to understand what’s going on under the hood, and to that end, here is my attempt to summarize the entire discipline of interactive 3D graphics in a few pages.
3D Coordinate Systems 3D drawing takes place, not surprisingly, in a 3D coordinate system. Anyone familiar with 2D Cartesian coordinate systems such as you find on graph paper, or in the window coordinates of an HTML document, knows about x and y values. These 2D coordinates define where
tags are located on a page, or where the virtual “pen” or “brush” draws in the case of the HTML Canvas element. Similarly, 3D drawing takes place in a 3D coordinate system, where there is an additional coordinate, z, which describes depth (i.e., how far into or out of the screen an object is drawn). The WebGL coordinate system is arranged as depicted in Figure 1-2, with x running horizontally left to right, y running vertically bottom to top, and positive z coming out of the screen. If you are already comfortable with the concept of the 2D coordinate system, I think the transition to a 3D coordinate system is pretty straightforward. However, from here on, things get a little complicated.
Meshes, Polygons, and Vertices While there are several ways to draw 3D graphics, by far the most common is to use a mesh. A mesh is an object composed of one or more polygonal shapes, constructed out of vertices (x, y, z triples) defining coordinate positions in 3D space. The polygons most typically used in meshes are triangles (groups of three vertices) and quads (groups of four vertices). 3D meshes are often referred to as models. Figure 1-3 illustrates a 3D mesh. The dark lines outline the quads that comprise the mesh, defining the shape of the face. (You would not see these lines in the final rendered image; they are included for reference.) The x, y, and z components of the mesh’s vertices define the shape only; surface properties of the mesh, such as the color and shading, are defined using additional attributes, as we will discuss shortly.
4
|
Chapter 1: An Introduction to WebGL
www.it-ebooks.info
Figure 1-2. A 3D coordinate system (https://commons.wikimedia.org/wiki/File: 3D_coordinate_system.svg; Creative Commons Attribution-Share Alike 3.0 Unported license)
Materials, Textures, and Lights The surface of a mesh is defined using additional attributes beyond the x, y, and z vertex positions. Surface attributes can be as simple as a single solid color, or they can be complex, comprising several pieces of information that define, for example, how light reflects off the object or how shiny the object looks. Surface information can also be represented using one or more bitmaps, known as texture maps (or simply textures). Textures can define the literal surface look (such as an image printed on a t-shirt), or they can be combined with other textures to achieve sophisticated effects such as bump iness or iridescence. In most graphics systems, the surface properties of a mesh are referred to collectively as materials. Materials typically rely on the presence of one or more lights, which (as you may have guessed) define how a scene is illuminated.
3D Graphics—A Primer
www.it-ebooks.info
|
5
Figure 1-3. A 3D mesh (http://upload.wikimedia.org/wikipedia/commons/8/88/ Blender3D_UVTexTut1.png; Creative Commons Attribution-Share Alike 3.0 Unported license) The head in Figure 1-3 has a material with a purple color and shading defined by a light source emanating from the left of the model (note the shadows on the right side of the face).
Transforms and Matrices 3D meshes are defined by the positions of their vertices. It would get awfully tedious to change a mesh’s vertex positions every time you want to move it to a different part of the view, especially if the mesh were continually moving across the screen or otherwise animating. For this reason, most 3D systems support transforms, operations that move the mesh by a relative amount without having to loop through every vertex, explicitly changing its position. Transforms allow a rendered mesh to be scaled, rotated, and translated (moved) around, without actually changing any values in its vertices. A transform is typically represented by a matrix, a mathematical object containing an array of values used to compute the transformed positions of vertices. If you are a linear
6
|
Chapter 1: An Introduction to WebGL
www.it-ebooks.info
algebra geek like me, you probably feel comfortable with this idea. If not, please don’t break into a cold sweat. The Three.js toolkit we are using in this book lets us treat matrices like black boxes: we just say translate, rotate, or scale and the right thing happens.
Cameras, Perspective, Viewports, and Projections Every rendered scene requires a point of view from which the user will be viewing it. 3D systems typically use a camera, an object that defines where (relative to the scene) the user is positioned and oriented, as well as other real-world camera properties such as the size of the field of view, which defines perspective (i.e., objects farther away appearing smaller). The camera’s properties combine to deliver the final rendered image of a 3D scene into a 2D viewport defined by the window or canvas. Cameras are almost always represented using a couple of matrices. The first matrix defines the position and orientation of the camera, much like the matrix used for trans forms (see the earlier discussion). The second matrix is a specialized one that represents the translation from the 3D coordinates of the camera into the 2D drawing space of the viewport. It is called the projection matrix. I know—sigh—there’s that pesky math again! But the details of camera matrices are nicely hidden in most toolkits, so you usually can just point, shoot, and render. Figure 1-4 depicts the core concepts of the camera, viewport, and projection. At the lower left, we see an icon of an eye; this represents the location of the camera. The red vector pointing to the right (in this diagram labeled as the x-axis) represents the direction in which the camera is pointing. The blue cubes are the objects in the 3D scene. The green and red rectangles are, respectively, the near and far clipping planes. These two planes define the boundaries of a subset of the 3D space, known as the view volume or view frustum. Only objects within the view volume are actually rendered to the screen. The near clipping plane is equivalent to the viewport, where we will see the final rendered image. Cameras are extremely powerful, as they ultimately define the viewer’s relationship to a 3D scene and provide a sense of realism. They also provide another weapon in the animator’s arsenal: by dynamically moving the camera around, you can create cinematic effects and control the narrative experience.
Shaders There is one last topic before we conclude our exploration of 3D graphics: shaders. In order to render the final image for a mesh, a developer must define exactly how vertices, transforms, materials, lights, and the camera interact with one another to create that image. This is done using shaders. A shader (also known as a programmable shader) is a chunk of program code that implements algorithms to get the pixels for a mesh onto
3D Graphics—A Primer
www.it-ebooks.info
|
7
Figure 1-4. Camera, viewport, and projection (http://obviam.net/index.php/3dprogramming-with-android-projections-perspective/), reproduced with permission the screen. Shaders are typically defined in a high-level C-like language and compiled into code usable by the graphics processing unit (GPU). Most modern computers come equipped with a GPU, a processor separate from the CPU that is dedicated to rendering 3D graphics. If you read my earlier decoded definition of WebGL carefully, you may have noticed that I glossed over one bit. From the official Khronos description: …It uses the OpenGL shading language, GLSL ES…
Unlike many graphics systems, where shaders are an optional and/or advanced feature, WebGL requires shaders. You heard me right: when you program in WebGL, you must define shaders or your graphics won’t show up on the screen. WebGL implementations assume the presence of a GPU. The GPU understands vertices, textures, and little else; it has no concept of material, light, or transform. The translation between those highlevel inputs and what the GPU puts on the screen is done by the shader, and the shader is created by the developer. So now you know why I didn’t bring up this topic earlier: I didn’t want to scare you! Shader programming can be pretty intimidating, and writing a C-like program, short 8
|
Chapter 1: An Introduction to WebGL
www.it-ebooks.info
though it may be, seems like an awfully high price to pay just to get an image on the screen. However, take heart: many popular libraries written for WebGL come with prebuilt shaders that you can just drop into your code and are powerful enough to cover all your conceivable shading needs. I should note here that shaders aren’t only about pain and suffering. They exist for a very good reason. Shaders give the graphics program mer full control over every vertex and pixel that gets rendered. This power can be used to create the most awesome effects, from uncanny photorealism (such as the jellyfish in Figure 1-1) to cartoonish fantasy. But with this great power also comes great responsibility. Shaders are an advanced topic, and I don’t want to climb that mountain together unless we have a thorough understanding of the basics. That’s why the examples in this book will stick to using simple shaders.
The WebGL API The basic concepts of interactive graphics haven’t changed much over the past several years. Implementations, however, are continually evolving, especially due to the recent proliferation of devices and operating systems. Bedrock among these changing tides has been OpenGL. Originally developed in the late 1980s, OpenGL has been an industrystandard API for a very long time, having endured competitive threats from Microsoft DirectX to emerge as the undisputed standard for programming 3D graphics. But not all OpenGLs are the same. The characteristics of various platforms, including desktop computers, set-top televisions, smartphones, and tablets, are so divergent that different editions of OpenGL had to be developed. OpenGL ES (for “embedded sys tems”) is the version of OpenGL developed to run on small devices such as set-top TVs and smartphones. Perhaps unforeseen at the time of its development, it turns out that OpenGL ES forms the ideal core for WebGL. It is small and lean, which means that not only is it (relatively) straightforward to implement in a browser, but it makes it much more likely that the developers of the different browsers implement it consistently, and that a WebGL application written for one browser will work identically in another browser. All of this high-performance, portable goodness comes with a downside. The lean nature of WebGL puts the onus on application developers to layer on top of it their own object models, scene graphs, display lists, and other structures that many veteran graphics programmers have come to take for granted. Of more concern is that, to the average web developer, WebGL represents a steep learning curve full of truly alien concepts. The good news here is that there are several open source code libraries out there that make
The WebGL API
www.it-ebooks.info
|
9
WebGL development quite approachable, and even fun. Think of them as existing at the level of jQuery or Prototype.js, though the analogy is rough at best. We will be talking about one such library, Three.js, in just a few short pages. But before we get to that, we are going to take a quick tour of the underpinnings, the drive train if you will, of WebGL.
The Anatomy of a WebGL Application At the end of the day, WebGL is just a drawing library—a drawing library on steroids, granted, considering that the graphics you can draw with WebGL are truly awe-inspiring and take full advantage of the powerful GPU hardware on most machines today. But it is really just another kind of canvas, akin to the 2D Canvas supported in all HTML5 browsers. In fact, WebGL actually uses the HTML5 element to get 3D graphics into the browser page. In order to render WebGL into a page, an application must, at a minimum, perform the following steps: 1. Create a canvas element. 2. Obtain a drawing context for the canvas. 3. Initialize the viewport. 4. Create one or more buffers containing the data to be rendered (typically vertices). 5. Create one or more matrices to define the transformation from vertex buffers to screen space. 6. Create one or more shaders to implement the drawing algorithm. 7. Initialize the shaders with parameters. 8. Draw. This section describes each of the aforementioned steps in some detail. The code snip pets included here are part of a full, working sample that draws a single white square on the WebGL canvas. See the file Chapter 1/example1-1.html for a full code listing.
The Canvas and Drawing Context All WebGL rendering takes place in a context, a JavaScript DOM object that provides the complete WebGL API. This structure mirrors the 2D drawing context provided in the HTML5 tag. To get WebGL into your web page, create a tag somewhere on the page, get the DOM object associated with it (say, using document.getElementById()), and then get a WebGL context for it. Example 1-1 shows how to get the WebGL context from a canvas DOM element.
10
|
Chapter 1: An Introduction to WebGL
www.it-ebooks.info
Example 1-1. Obtaining a WebGL context from a canvas function initWebGL(canvas) { var gl; try { gl = canvas.getContext("experimental-webgl"); } catch (e) { var msg = "Error creating WebGL Context!: " + e.toString(); alert(msg); throw Error(msg); } return gl; }
Note the try/catch block in the example. This is very important, be cause some browsers still do not support WebGL, or even if they do, the user may not have the most recent version of that browser that includes WebGL support. Further, even browsers that do support WebGL may be running on old hardware, and not be able to give you a valid WebGL rendering context. So, detection code like that in Example 1-1 will help you with deploying a fallback such as a rendering based on a 2D canvas—or at the very least, provide you with a graceful exit.
The Viewport Once you have obtained a valid WebGL drawing context from your canvas, you need to tell it the rectangular bounds of where to draw. In WebGL, this is called a viewport. Setting the viewport in WebGL is simple; just call the context’s viewport() method (see Example 1-2). Example 1-2. Setting the WebGL viewport function initViewport(gl, canvas) { gl.viewport(0, 0, canvas.width, canvas.height); }
Recall that the gl object used here was created by our helper function initWebGL(). In this case, we have initialized the WebGL viewport to take up the entire contents of the canvas’s display area.
The WebGL API
www.it-ebooks.info
|
11
Buffers, ArrayBuffer, and Typed Arrays Now we have a context ready for drawing. This is pretty much where the similarities to the 2D Canvas end. WebGL drawing is done with primitives—types of objects to draw such as triangle sets (arrays of triangles), triangle strips (described shortly), points, and lines. Primitives use arrays of data, called buffers, which define the positions of the vertices to be drawn. Example 1-3 shows how to create the vertex buffer data for a unit (1 × 1) square. The results are returned in a JavaScript object containing the vertex buffer data, the size of a vertex structure (in this case, three floating-point numbers to store x, y, and z), the number of vertices to be drawn, and the type of primitive that will be used to draw the square, in this example, a triangle strip. (A triangle strip is a rendering primitive that defines a sequence of triangles using the first three vertices for the first triangle, and each subsequent vertex in combination with the previous two for subsequent triangles.) Example 1-3. Creating vertex buffer data // Create the vertex data for a square to be drawn function createSquare(gl) { var vertexBuffer; vertexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); var verts = [ .5, .5, 0.0, -.5, .5, 0.0, .5, -.5, 0.0, -.5, -.5, 0.0 ]; gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verts), gl.STATIC_DRAW); var square = {buffer:vertexBuffer, vertSize:3, nVerts:4, primtype:gl.TRIANGLE_STRIP}; return square; }
Note the use of the type Float32Array. This is a new data type introduced into web browsers for use with WebGL. Float32Array is a type of ArrayBuffer, also known as a typed array. This is a JavaScript type that stores compact binary data. Typed arrays can be accessed from JavaScript using the same syntax as ordinary arrays, but are much faster and consume less memory. They are ideal for use with binary data where performance is critical. Typed arrays can be put to general use, but their introduction into web browsers was pioneered by the WebGL effort. The latest typed array specifi cation can be found on the Khronos website at http://www.khronos.org/registry/ typedarray/specs/latest/.
12
|
Chapter 1: An Introduction to WebGL
www.it-ebooks.info
Matrices Before we can draw our square, we must create a couple of matrices. First, we need a matrix to define where the square is positioned in our 3D coordinate system, relative to the camera. This is known as a ModelView matrix, because it combines transformations of the model (3D mesh) and the camera. In our example, we are transforming the square by translating it along the negative z-axis (i.e., moving it away from the camera by −3.333 units). The second matrix we need is the projection matrix, which will be required by our shader to convert the 3D space coordinates of the model in camera space into 2D coordinates drawn in the space of the viewport. In this example, the projection matrix defines a 45degree field of view perspective camera. This matrix is pretty ugly; most people do not code projection matrices by hand, but use a library instead. There is a great open source library called glMatrix for doing matrix math in JavaScript (https://github.com/toji/glmatrix). glMatrix is written by Brandon Jones, who is doing some wonderful WebGL work, including ports of Quake and other popular games. Example 1-4 shows the code for setting up the ModelView and projection matrices. Example 1-4. Setting up the ModelView and projection matrices function initMatrices() { // The transform matrix for the square - translate back in Z // for the camera modelViewMatrix = new Float32Array( [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, −3.333, 1]); // The projection matrix (for a 45 degree field of view) projectionMatrix = new Float32Array( [2.41421, 0, 0, 0, 0, 2.41421, 0, 0, 0, 0, −1.002002, −1, 0, 0, −0.2002002, 0]); }
The Shader We are almost ready to draw our scene. There is one more important piece of setup: the shader. As described earlier, shaders are small programs, written in a high-level C-like
The WebGL API
www.it-ebooks.info
|
13
language, that define how the pixels for 3D objects actually get drawn on the screen. WebGL requires the developer to supply a shader for each object that gets drawn. The shader can be used for multiple objects, so in practice it is often sufficient to supply one shader for the whole application, reusing it with different parameters each time. A shader is typically composed of two parts: the vertex shader and the fragment shad er (also known as the pixel shader). The vertex shader is responsible for transforming the coordinates of the object into 2D display space; the fragment shader is responsible for generating the final color output of each pixel for the transformed vertices, based on inputs such as color, texture, lighting, and material values. In our simple example, the vertex shader combines the modelViewMatrix and projectionMatrix values to create the final, transformed vertex for each input, and the fragment shader simply outputs a hardcoded white color. In WebGL, shader setup requires a sequence of steps, including compiling the individual pieces, then linking them together. For brevity, we will show only the GLSL ES source for our two sample shaders (see Example 1-5), not the entire setup code. You can see exactly how the shaders are set up in the full sample. Example 1-5. The vertex and fragment shaders var vertexShaderSource = " " " " " " "
attribute vec3 vertexPos;\n" + uniform mat4 modelViewMatrix;\n" + uniform mat4 projectionMatrix;\n" + void main(void) {\n" + // Return the transformed and projected vertex value\n" + gl_Position = projectionMatrix * modelViewMatrix * \n" + vec4(vertexPos, 1.0);\n" + }\n";
var fragmentShaderSource = " void main(void) {\n" + " // Return the pixel color: always output white\n" + " gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);\n" + "}\n";
Drawing Primitives Now we are ready to draw our square (see Example 1-6). Our context has been created; our viewport has been set; our vertex buffer, matrices, and shader have been created and initialized. We define a function, draw(), which takes the WebGL context and our previously created square object. First, the function clears the canvas with a black back ground color. Then, it sets (“binds”) the vertex buffer for the square to be drawn, sets (“uses”) the shader to use, and connects up the vertex buffer and matrices to the shader
14
|
Chapter 1: An Introduction to WebGL
www.it-ebooks.info
as inputs. Finally, we call the WebGL drawArrays() method to draw the square. We simply tell it which type of primitives and how many vertices in the primitive; WebGL knows everything else already because we have essentially set those other items (vertices, matrices, shaders) as state in the context. Example 1-6. The drawing code function draw(gl, obj) { // clear the background (with black) gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.clear(gl.COLOR_BUFFER_BIT); // set the vertex buffer to be drawn gl.bindBuffer(gl.ARRAY_BUFFER, obj.buffer); // set the shader to use gl.useProgram(shaderProgram); // connect up the shader parameters: vertex position and projection/model matrices gl.vertexAttribPointer(shaderVertexPositionAttribute, obj.vertSize, gl.FLOAT, false, 0, 0); gl.uniformMatrix4fv(shaderProjectionMatrixUniform, false, projectionMatrix); gl.uniformMatrix4fv(shaderModelViewMatrixUniform, false, modelViewMatrix); // draw the object gl.drawArrays(obj.primtype, 0, obj.nVerts); }
The final output is shown in Figure 1-5.
Chapter Summary Thus ends our nickel tour of a basic WebGL application. Whew! That was a lot of work. At this point, you might be thinking that was way too much work just to get a square on the screen. Heck, it’s not even a 3D object! Well, I would be inclined to agree with you. WebGL programming, when done at this level, is work. The designers of the stan dard made a conscious decision to trade size for power. The API is small and simple, at the cost of having to do a lot of coding on the application side. Obviously, in most cases, we won’t be using WebGL just to draw 2D objects. The HTML5 2D Canvas would do just as well, with far fewer lines of code. But even when you are developing a true 3D application, it’s a pretty tough slog if you code in this fashion. You
Chapter Summary
www.it-ebooks.info
|
15
will likely end up writing your own library on top of WebGL, or, better still, it would be really nice if other programmers had already done the hard work for you. Well, I have some good news: they have. In Chapter 2, we will build our first WebGL app using the Three.js library. Let’s get to it.
Figure 1-5. A square drawn with WebGL
16
|
Chapter 1: An Introduction to WebGL
www.it-ebooks.info
CHAPTER 2
Your First WebGL Program
Now that we have covered the core concepts and, I hope, developed a basic under standing of the workings of WebGL, it is time to put that knowledge to use. In the next several chapters, we will create a series of WebGL sample pages, leading up to the de velopment of a full application. Before we get going with that, we need to take a look at one more piece of our technology puzzle: Three.js.
Three.js—A JavaScript 3D Engine Necessity is the mother of invention. It couldn’t have been too long before somebody out there, dreading writing the same hundreds of lines of WebGL code over again, wrapped her work in a library that could be used for general-purpose 3D programming. In fact, several somebodies have done it. There are quite a few good open source libraries built for WebGL available, including GLGE (http://www.glge.org/), SceneJS (http:// www.scenejs.org/), and CubicVR (http://www.cubicvr.org/). Each library does things a bit differently, but they share the goal of implementing high-level, developer-friendly features on top of raw WebGL. The library we will use throughout this book is called Three.js, the creation of one Mr.doob, a.k.a. Ricardo Cabello Miguel, a programmer based in Barcelona, Spain. Three.js provides an easy, intuitive set of objects that are commonly found in 3D graph ics. It is fast, using many best-practice graphics engine techniques. It is powerful, with several built-in object types and handy utilities. It is open source, hosted on GitHub, and well maintained, with several authors helping Mr.doob. I chose Three.js to write the examples in this book for a couple of reasons. First, I am currently using it in my own development projects and really like it. Second, it is quite popular among these engines and is the perceived leader. You may find other libraries
17
www.it-ebooks.info
more to your liking, or better suited to the needs of your application. That’s OK. One size definitely does not fit all here. The other engines I mentioned are great and have their place. You may even want to build your own engine if that’s how you roll. But before you do, you should take a look at the great engine work already being done for WebGL. The fact that toolkits like Three.js exist at all is due, in no small part, to how powerful web browsers’ JavaScript virtual machines (VMs) have become in recent years. A few years back, VM performance would have made implementing such libraries prohibitive, and perhaps even made WebGL a nonstarter for practical use. Thankfully, today’s VMs scream, and, with libraries like Three.js, WebGL has been made accessible to the millions of web developers on the planet.
Throughout the book, you will get to know Three.js in detail. For now, here is a summary of what it has to offer: Three.js hides the details of 3D rendering Three.js abstracts out the details of the WebGL API, representing the 3D scene as meshes, materials, and lights (i.e., the object types graphics programmers typically work with). Three.js is object-oriented Programmers work with first-class JavaScript objects instead of just making Java Script function calls. Three.js is feature-rich More than just a wrapper around raw WebGL, Three.js contains many prebuilt objects useful for developing games, animations, presentations, high-resolution models, and special effects. Three.js is fast Three.js employs 3D graphics best practices to maintain high performance, without sacrificing usability. Three.js supports interaction WebGL provides no native support for picking (i.e., knowing when the mouse pointer is over an object). Three.js has solid picking support, making it easy to add interactivity to your applications. Three.js does the math Three.js has powerful, easy-to-use objects for 3D math, such as matrices, projec tions, and vectors. Three.js has built-in file format support You can load files in text formats exported by popular 3D modeling packages; there are also Three.js-specific JSON and binary formats. 18
|
Chapter 2: Your First WebGL Program
www.it-ebooks.info
Three.js is extensible It is fairly easy to add features and customize Three.js. If you don’t see a data type you need, write it and plug it in. Three.js also works with the HTML5 2D canvas As popular as WebGL has become, it is still not running everywhere. Three.js can also render most content into a 2D canvas, should the 3D canvas context not be available, allowing your code to gracefully fall back to another solution. It is important to note a few things Three.js doesn’t do. Three.js is not a game engine or virtual world platform. It lacks some of the commonly used features you would find in those systems, such as billboards, avatars, and physics. Nor does Three.js have the builtin network support you would expect if you were writing a multiplayer game. If you need those, you will have to build them yourself on top of Three.js. Still, its power and simplicity make Three.js a great choice for getting started on your WebGL journey. So, without further ado, let’s get going and write some code!
Setting Up Three.js The first thing you will need to do is get the latest Three.js package from GitHub. As of this writing, the Three.js repository URL is https://github.com/mrdoob/three.js/. Once you have cloned the Git repository, you will want to use the minified version of the JavaScript located in build/Three.js. Hang on to the full source located under the src folder, too. The API documentation is linked from the GitHub page, but it is pretty basic, so you might want to have the source handy for reference. Three.js is built with the Google Closure Compiler; this one file con tains the entire Three.js library built from several separate source files. If you are not familiar with Closure, and want to know more, go to http://code.google.com/closure/compiler/. If you don’t want to deal with that, you can treat Three.js like a black box for now.
Take a little time with the source tree and documentation in order to familiarize yourself with Three.js. Now, if you’re like me, you plan to ignore that recommendation because you are ready to jump right in. You’re sick of the preliminaries and you want to get down to coding! OK, I understand—but at least do this for me: browse the examples. Under the folder examples, there are nearly 100 WebGL demos and several 2D canvas demos, too, covering a range of features and effects. You won’t be sorry. Finally, get all of this onto a web server. You will need to serve up your pages in order for most of the samples in the book to work. I run a local version of a standard LAMP stack on my MacBook…but all you really need is the “A” part of LAMP (i.e., a web server such as Apache). Setting Up Three.js
www.it-ebooks.info
|
19
A Simple Three.js Page Now that you are set up, it’s time to write your first WebGL program. From this exercise, you will see that it’s pretty simple to get going with Three.js. Example 2-1 contains the complete code listing for a new version of that square-drawing program from Chap ter 1, but in 30 lines instead of 150. Now the whole sample is greatly condensed. Example 2-1. A simple page using Three.js A Simple Three.js Page function onLoad() { // Grab our container div var container = document.getElementById("container"); // Create the Three.js renderer, add it to our div var renderer = new THREE.WebGLRenderer(); renderer.setSize(container.offsetWidth, container.offsetHeight); container.appendChild( renderer.domElement ); // Create a new Three.js scene var scene = new THREE.Scene(); // Create a camera and add it to the scene var camera = new THREE.PerspectiveCamera( 45, container.offsetWidth / container.offsetHeight, 1, 4000 ); camera.position.set( 0, 0, 3.3333 ); scene.add( camera ); // Now, create a rectangle and add it to the scene var geometry = new THREE.PlaneGeometry(1, 1); var mesh = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial( ) ); scene.add( mesh ); // Render it renderer.render( scene, camera ); }
20
|
Chapter 2: Your First WebGL Program
www.it-ebooks.info
Let’s walk through how it works. First, we have a tag that includes the Three.js library. Then, we supply our script that draws the square. The entire program is contained in a single function, onLoad(), triggered by the page’s onLoad event. In the body of the function, we first find the page element that we are going to use to render the WebGL, and save that in the variable container. Then, we initialize the Three.js renderer object. The renderer is responsible for all Three.js drawing (via WebGL context, of course). We construct the renderer object, size it to the same size as the container, and add it as a DOM child element of the container. Next, we create a scene. The scene is the top-level object in the Three.js graphics hier archy. It contains all other graphical objects. (In Three.js, objects exist in a parent-child hierarchy. More on this in later chapters.) Once we have a scene, we are going to add a couple of objects to it: a camera and a mesh. The camera defines where we are viewing the scene from: in this example, we use a transform to set its position property to 3.3333 units (a little bit back) from the origin. Our mesh is composed of a geometry object and a material. For geometry, we are using a 1 × 1 rectangle created with the Three.js PlaneGeometry object. Our material tells Three.js how to shade the object. In this ex ample, our material is of type MeshBasicMaterial (i.e., just a single simple color with a default value of pure white). Three.js objects have a default position of 0, 0, 0, so our white rectangle will be placed at the origin. Finally, we need to render the scene. We do this by calling the renderer’s render() method, feeding it a scene and a camera. The output in Figure 2-1 should look familiar. Note how Three.js closely mirrors the graphics concepts introduced in Chapter 1: we are working with objects (instead of buffers full of numbers), viewing them with a cam era, moving them with transforms, and defining how they look with materials. In 30 lines of code, we have produced exactly the same graphic as our raw WebGL example that took 150 lines.
A Simple Three.js Page
www.it-ebooks.info
|
21
Figure 2-1. Square example, rewritten using Three.js The savvy web programmer may notice a few unpalatable morsels in this example. First, the use of the onLoad event; in later chapters, we will move away from this model of detecting page loads and instead use jQuery’s superior ready() method. Second, the entire program is contained in a single function; obviously we will not be building large programs this way. In later chapters, I will introduce a very simple framework for building modular programs with Three.js. Why all the kruft? For now, I am trying to keep the number of moving parts to a minimum, and the example as simple as possible. So, experienced en gineers: please be patient for one more chapter; structured code is on the way.
A Real Example At this point, you may be thinking, “Nice square,” and starting to wonder if we are ever going to draw any 3D graphics. Well, it’s time. Example 2-2 shows how to replace our simple square with more interesting content—a page that looks nice and shows off major features of WebGL while still keeping it simple.
22
|
Chapter 2: Your First WebGL Program
www.it-ebooks.info
Figure 2-2 shows the page. We have some heading text, a cube with an image wrapped onto its faces, and some text at the bottom of the page. As the prompt suggests, the page is interactive: clicking within the canvas element toggles an animation that spins the cube.
Figure 2-2. A more involved Three.js example image from http://www.openclipart.org (CC0 Public Domain Dedication) Let’s take a detailed look at how all this is done. Example 2-2 contains the entire code listing. It’s a little more involved than our first Three.js example, but still concise enough that we can walk through the whole example in short order. Example 2-2. Welcome to WebGL! Welcome to WebGL var renderer = null, scene = null, camera = null, cube = null,
A Real Example
www.it-ebooks.info
|
23
animating = false; function onLoad() { // Grab our container div var container = document.getElementById("container"); // Create the Three.js renderer, add it to our div renderer = new THREE.WebGLRenderer( { antialias: true } ); renderer.setSize(container.offsetWidth, container.offsetHeight); container.appendChild( renderer.domElement ); // Create a new Three.js scene scene = new THREE.Scene(); // Put in a camera camera = new THREE.PerspectiveCamera( 45, container.offsetWidth / container.offsetHeight, 1, 4000 ); camera.position.set( 0, 0, 3 ); // Create a directional light to show off the object var light = new THREE.DirectionalLight( 0xffffff, 1.5); light.position.set(0, 0, 1); scene.add( light ); // Create a shaded, texture-mapped cube and add it to the scene // First, create the texture map var mapUrl = "../images/molumen_small_funny_angry_monster.jpg"; var map = THREE.ImageUtils.loadTexture(mapUrl); // Now, create a Phong material to show shading; pass in the map var material = new THREE.MeshPhongMaterial({ map: map }); // Create the cube geometry var geometry = new THREE.CubeGeometry(1, 1, 1); // And put the geometry and material together into a mesh cube = new THREE.Mesh(geometry, material); // Turn it toward the scene, or we won't see the cube shape! cube.rotation.x = Math.PI / 5; cube.rotation.y = Math.PI / 5; // Add the cube to our scene scene.add( cube ); // Add a mouse up handler to toggle the animation addMouseHandler(); // Run our render loop run(); }
24
|
Chapter 2: Your First WebGL Program
www.it-ebooks.info
function run() { // Render the scene renderer.render( scene, camera ); // Spin the cube for next frame if (animating) { cube.rotation.y -= 0.01; } // Ask for another frame requestAnimationFrame(run); } function addMouseHandler() { var dom = renderer.domElement; dom.addEventListener( 'mouseup', onMouseUp, false); } function onMouseUp (event) { event.preventDefault(); animating = !animating; }
Welcome to WebGL!
Click to animate the cube
Other than a few setup details that I will talk about later on, and adding a stylesheet to control colors and fonts, this program starts out pretty much like the previous one. We create a Three.js renderer object and add its DOM element as a child of the container. This time, however, we pass a parameter in to the constructor, antialias, set to true, A Real Example
www.it-ebooks.info
|
25
which tells Three.js to use antialiased rendering. Antialiasing avoids nasty artifacts that would make some drawn edges look jagged. (Note that Three.js has a couple of different styles for passing parameters to methods. Typically, constructor parameters are passed in an object with named fields, as in this example.) Then, we create a perspective camera, just as in the previous example. This time, the camera will be moved in a bit, so we get a nice close-up of the cube.
Shading the Scene We are nearly ready to add our cube to the scene. If you peek ahead a few lines, you’ll see that we create a unit cube using the Three.js CubeGeometry object. But before we add the cube, we need to do a few other things. First, we need to incorporate some shading into our scene. Without shading, you won’t see the edges of the cube face. We will also need to create a texture map to render on the faces of the cube; we’ll talk about that in just a bit. To put shading into the scene, we will need to do two things: add a light source, and use a different kind of material for the cube. Lights come in a few different flavors in Three.js. In our example, we will use a directional light, a lighting source that illuminates in a specific direction but over an infinite distance (and doesn’t have any particular location). The Three.js syntax is a little weird; instead of setting the direction, we are going to use the position attribute of the light to set it out from the origin; the direction is then inferred to point into the origin of the scene (i.e., at our cube). The second thing we do is change the material we are using. The Three.js type Mesh BasicMaterial defines simple attributes such as a solid color and transparency. It will
not display any shading based on our light sources. So we need to change this material to another type: MeshPhongMaterial. This material type implements a simple, fairly realistic-looking shading model (called “Phong shading”) with high performance. (Three.js supports other, more sophisticated shading models that we will explore later.) With Phong shading in place, we are now able to see the edges of the cube. Cube faces that point more toward our light source are brightly lit; those that point away are less brightly lit. The edges are visible where any two faces meet. You may have noticed that, amid all the talk of shading in this section, I have made no mention of shaders, those pesky little C-like programs I mentioned in Chapter 1 that WebGL requires in order to show bits on the screen. That’s because Three.js implements them for us. We simply set up our lights and materials, and Three.js uses its built-in shaders to do the dirty work. Many thanks, Mr.doob!
26
|
Chapter 2: Your First WebGL Program
www.it-ebooks.info
Adding a Texture Map Texture maps, also known as textures, are bitmaps used to represent surface attributes of 3D meshes. They can be used in simple ways to define just the color of a surface, or they can be combined to create complex effects such as bumps or highlights. WebGL provides several API calls for working with textures, and the standard provides for im portant security features, such as limiting cross-domain texture use (see Chapter 7 for more information). Happily, Three.js gives us a simple API for loading textures and associating them with materials without too much fuss. We call upon THREE.ImageUtils.loadTexture() to load the texture from an image file, and then associate the resultant texture with our material by setting the map parameter of the material’s constructor. Three.js is doing a lot of work under the covers here. It maps the bits of the JPEG image onto the correct parts of each cube face; the image isn’t stretched around the cube, or upside down or backward on any of the faces. This might not seem like a big deal, but if you were to code something like this in raw WebGL, there would be a lot of details to get right. Three.js just knows. And once again, it is doing the hard work of the actual shading, with a built-in Phong shader program that combines the light values, material color, and pixels in the texture map to generate the correct color for each pixel and thus the finished image. There is a whole lot more we can do with textures in Three.js. We will be talking about it in more detail in subsequent chapters. Now we are ready to create our cube mesh. We construct the geometry, the material, and the texture, and then put it all together into a Three.js mesh that we save into a variable named cube. The listing in Example 2-3 shows the lines of code involved in creating the lit, textured, and Phong-shaded cube. Example 2-3. Creating the lit, textured, Phong-shaded cube // Create a directional light to show off the object var light = new THREE.DirectionalLight( 0xffffff, 1.5); light.position.set(0, 0, 1); scene.add( light ); // Create a shaded, texture-mapped cube and add it to the scene // First, create the texture map var mapUrl = "../images/molumen_small_funny_angry_monster.jpg"; var map = THREE.ImageUtils.loadTexture(mapUrl); // Now, create a Phong material to show shading; pass in the map var material = new THREE.MeshPhongMaterial({ map: map }); // Create the cube geometry var geometry = new THREE.CubeGeometry(1, 1, 1); // And put the geometry and material together into a mesh cube = new THREE.Mesh(geometry, material);
A Real Example
www.it-ebooks.info
|
27
Rotating the Object Before we can see the cube in action, we need to do one more thing: rotate it a bit, or we would never know it was a cube—it would look exactly like our square from the previous example but with an image on it. So let’s turn it on its x (horizontal)-axis toward the camera. We do that by setting the rotation property of the mesh. In Three.js, every object can have a position, a rotation, and a scale. (Remember how we used that in the first example to position the camera a little bit back from the square?) By assigning a nonzero value to rotation.x, we are telling Three.js to rotate by that amount around the object’s x-axis. We do the same for the y-axis, turning the cube a little to the left. With these two rotations in place, we can see three of the six cube faces. Note the value we set for the rotations. Most 3D graphics systems rep resent degrees in units known as radians. Radians measure angles as the distance around the circumference of a unit square (i.e., 2π radians equals 360 degrees). Math.PI is equivalent to 180 degrees, thus the as signment mesh.rotation.x = Math.PI / 12 provides a 15-degree rotation about the x-axis.
The Run Loop and requestAnimationFrame() You may have noticed a few structural changes between our first example and this one. First, we have added some helper functions. Second, we define a handful of global vari ables to tuck away information that will be used by the helper functions. (I know, I know: another hack. This will go away, as promised, when we move to a framework-centered approach in the next chapter.) We have also added a run loop (also known as a render loop or simulation loop). With a run loop, rather than rendering the scene only once, we render continually. This is not so important for a static scene, but if anything in the scene is animated or changes based on user input, we need to render continually. From here on, all our examples will render scenes using a run loop. There are a couple of ways to implement a run loop. One way is to use setTimeout() with a callback that renders the scene and then resets the timeout. This is the classic web approach to animating; however, it is falling out of favor, because newer browsers sup port something better: requestAnimationFrame(). This function has been designed specifically for page animation, including animation with WebGL. With requestAnimationFrame(), the browser can optimize performance because it will combine all such requests into a single redraw step. This function is not necessarily supported in all versions of all browsers, and—annoyingly—it has different names in different browsers. Therefore, I have incorporated a nice utility, RequestAnimation Frame.js, written by Paul Irish. This file supplies a cross-browser implementation of requestAnimationFrame().
28
|
Chapter 2: Your First WebGL Program
www.it-ebooks.info
We are ready to render our scene. We define a function, run(), that implements the run loop. As before, we call the renderer’s render() method, passing it the scene and the camera. Then, we have a little logic in there to support animating the cube. We will cover that part in the next section. Finally, we turn the crank by requesting another animation frame from the browser. See Example 2-4. Example 2-4. The run loop function run() { // Render the scene renderer.render( scene, camera ); // Spin the cube for next frame if (animating) { cube.rotation.y -= 0.01; } // Ask for another frame requestAnimationFrame(run); }
The final result is shown in Figure 2-2. Now you can see the front and top of the cube. We have 3D on our web page!
Bringing the Page to Life For a first full example, we could probably stop here. We have nice graphics on the page, and a true 3D object to look at. But at the end of the day, 3D graphics isn’t just about the rendering; it’s also about animation and interactivity. Otherwise, a web designer could just ask a 3D modeler friend to render a still image out of 3ds Max or Maya, stick it in the page with an
tag, and be done with it. But that’s not the whole story. With that in mind, we put animation and interactivity in even our simplest example. Here’s how. In the preceding section, we discussed the run loop. This is our opportunity to make changes to the scene before we render the next frame. To spin a cube, we need to change its rotation value with each frame. We don’t want the thing tumbling around randomly; rather, we would like a smooth rotation around its (vertical) y-axis, and so we just add a little to the rotation’s y value each time around. This style represents one of the simpler ways to animate WebGL content. There are several other approaches, each of varying complexity. We will get to those in a later chapter. Finally, it would be nice to control when the cube spins. We have added a click handler to the page, using straightforward DOM event methods. The only trick is to figure out where to add the click event handler. In this case, we use the DOM element associated with the Three.js renderer object. See the code listing in Example 2-5. A Real Example
www.it-ebooks.info
|
29
Example 2-5. Adding mouse interaction function addMouseHandler() { var dom = renderer.domElement; dom.addEventListener( 'mouseup', onMouseUp, false); } function onMouseUp (event) { event.preventDefault(); animating = !animating; }
Click on the cube. Watch it spin. Hypnotic, isn’t it?
Chapter Summary Well, there is our first real example. I have to say that it was pretty painless. In a few pages of code, we were able to create a standard HTML page with 3D content, pretty it up, give it life with animation, and make it interactive. In the past, there was no way to do that. But the pieces have all come together with WebGL. And with Three.js, you don’t need a graduate degree in computer graphics to do it. We’re up and running. The rest is just details.
30
|
Chapter 2: Your First WebGL Program
www.it-ebooks.info
CHAPTER 3
Graphics
At the heart of WebGL lies a high-performance system for rendering graphics using a computer’s 3D graphics processing unit, or GPU. As we saw in the first two chapters, the WebGL API gets pretty close to that metal to give developers full control and power, while libraries like Three.js provide a more intuitive layer for managing what gets ren dered. In the next several chapters, we are going to use Three.js to explore essential WebGL development concepts. Let’s begin that exploration by taking a close look at graphics. Graphics comprises several related topics: meshes and other drawn primitives, such as points and lines; matrices and the transformation hierarchy; texture maps; and shading, including materials and lights. It’s a big area to cover, and to do it justice we are going to create a series of examples as big as the topic itself: we’re going to build a model of the solar system. See Figure 3-1 for a sneak peek. There is a lot going on here: richly textured planets in orbit around a blazing sun; satellites and rings around planets; a star field background; and lines tracing the planetary orbits. In putting this model together, we will get a points-of-interest grand tour of developing graphics with WebGL. In case it’s not obvious, the solar system depicted combines a certain amount of photorealism—the planet texture maps come from real sat ellite and telescope pictures—plus annotative drawing, changes in scale, and other cheats (such as spheres for planets instead of ellipsoids) in order to depict the entire system on a page. The goal here is to illustrate, not to simulate reality with any precision. If you are interested in a more realistic space simulator, there are already some great ones on the market.
31
www.it-ebooks.info
Figure 3-1. Solar system model; planet texture maps courtesy NASA/JPL-Caltech and Bjorn Jonsson
Sim.js—A Simple Simulation Framework for WebGL Before we create our stellar sample, we are going to talk briefly about one other topic. Three.js provides a nice layer to insulate the gritty details of the WebGL API. However, in everyday use, you will quickly find yourself performing the same Three.js tasks over and over, and contemplate creating a framework to simplify your life. Have a look at the samples you downloaded with Three.js and you’ll see what I mean: line after line of code to create meshes, add them as children of other objects, set textures, add DOM event callbacks to handle clicking, and so on. Many of these tasks could be wrapped into a higher-level set of reusable objects. Understandably, this is outside the scope of the Three.js mission, so no criticism is intended of Mr.doob's toolkit; the job falls on us as application writers. The examples for this chapter and all chapters that follow are built using Sim.js, a sim ulation framework of my creation. Sim.js is a tiny library—just a few pages of code and a handful of classes—that wraps the more repetitive Three.js tasks such as renderer setup, the run loop, DOM event handling, basic parent-child hierarchy operations, and input. A copy of the source to Sim.js is included with this book, and can also be found on GitHub at https://github.com/tparisi/Sim.js. The code in Sim.js is almost selfexplanatory, but let’s take a minute to look at its key features to set the stage for our examples:
32
|
Chapter 3: Graphics
www.it-ebooks.info
The Publisher class (Sim.Publisher) This class is the base for any object that generates (“publishes”) events. Whenever an interesting event happens, Sim.Publisher iterates through its list of registered callbacks, calling each with the event data and the supplied object (“subscriber”). Sim.Publisher is the base class for most Sim.js objects. The Application class (Sim.App) This class wraps all the Three.js setup/teardown code for a page, such as the creation of the renderer, the top-level scene object, and the camera. It also adds DOM han dlers to the Three.js rendering canvas to handle resizing, mouse input, and other events. Sim.App also manages the list of individual objects for the application, and implements the run loop. The Object class (Sim.Object) This is the base class for most of the objects in an application. It manages the state for a single (application-defined) object, and handles a few Three.js basics, includ ing adding/removing the object from the scene, adding/removing children from the object hierarchy (more on this shortly), and setting/retrieving standard Three.js properties such as position, scale, and rotation. I built Sim.js primarily to illustrate WebGL concepts for this book. It is a bit too simplistic for building large-scale applications or games, but you may still find it useful in your development work. Feel free to im prove upon it and send me feedback.
That’s pretty much all there is to Sim.js. With this simple set of helper code in place, we are now ready to proceed to building the samples.
Creating Meshes The most commonly used type of rendered object in WebGL is the mesh. A mesh is composed of a set of polygons (typically triangles or quadrangles), each described by three or more 3D vertices. Meshes can be as simple as a single triangle, or extremely complex, with enough detail to fully depict a real-world object such as a car, airplane, or person. We will use several meshes in our solar system model, primarily spheres for the planet shapes. Three.js has a built-in sphere object, so there is no need to create spheres by hand. We are going to build our full solar system in several steps. Let’s start by creating a familiar object: the Earth. Example 3-1 shows the code listing for a first Earth applica tion. In this example, we create a sphere with a single high-resolution texture map based on satellite photographs of the planet. Launch the file Chapter 3/graphics-earthbasic.html. The result is shown in Figure 3-2.
Creating Meshes
www.it-ebooks.info
|
33
Example 3-1. Basic Earth example // Constructor EarthApp = function() { Sim.App.call(this); } // Subclass Sim.App EarthApp.prototype = new Sim.App(); // Our custom initializer EarthApp.prototype.init = function(param) { // Call superclass init code to set up scene, renderer, default camera Sim.App.prototype.init.call(this, param); // Create the Earth and add it to our sim var earth = new Earth(); earth.init(); this.addObject(earth); } // Custom Earth class Earth = function() { Sim.Object.call(this); } Earth.prototype = new Sim.Object(); Earth.prototype.init = function() { // Create our Earth with nice texture var earthmap = "./images/earth_surface_2048.jpg"; var geometry = new THREE.SphereGeometry(1, 32, 32); var texture = THREE.ImageUtils.loadTexture(earthmap); var material = new THREE.MeshBasicMaterial( { map: texture } ); var mesh = new THREE.Mesh( geometry, material ); // Let's work in the tilt mesh.rotation.z = Earth.TILT; // Tell the framework about our object this.setObject3D(mesh); } Earth.prototype.update = function() { // "I feel the Earth move..." this.object3D.rotation.y += Earth.ROTATION_Y;
34
|
Chapter 3: Graphics
www.it-ebooks.info
Figure 3-2. Your world, rendered } Earth.ROTATION_Y = 0.0025; Earth.TILT = 0.41;
The source for this example is in file Chapter 3/earth-basic.js. This file defines two objects: a custom subclass of Sim.App called EarthApp, and an Earth object derived from Sim.Object. These are all we need for our simple example. Let’s take a walk through it. The HTML file for this example (not shown in the listing) creates a new instance of EarthApp, calls its init() method to do all the setup, and then calls run() (i.e. the run loop). From there, the rest happens automatically, driven by each animation frame.
Creating Meshes
www.it-ebooks.info
|
35
The EarthApp object is quite simple. Its init() method creates an instance of an Earth, adds it to its list of objects, and returns. Everything else in EarthApp is inherited from Sim.App. Under the covers, in each frame of the run loop, we call update(), and then tell Three.js to render the scene. update() iterates through the application’s list of objects, calling update() on each of them in turn. Our Earth class defines a couple of custom methods. First, init() is responsible for creating the Earth mesh. In Three.js, a mesh is composed of a geometry object and a material. For the geometry, we create a highly detailed Three.js sphere (type THREE.SphereGeometry) of radius 1. The second and third parameters tell Three.js how many triangles to generate by providing a number of cross sections and triangles per cross section. In this example, we are looking at Earth up close, so we want a highresolution version. (Play around with these parameters, and you will see that if you make the values low enough, your spheres will look more like golf balls. For some applications, this can be fine, especially if the object is far away from the camera.) To get a sense of the vertex data that underlies a sphere, Figure 3-3 shows our geometry rendered as a wireframe (i.e. with the polygon outlines drawn instead of the surface). You can clearly see the quadrangles (“quads”) that comprise our sphere geometry. In order to actually see our sphere mesh, we also have to define a material, an object that describes the surface properties of the rendered mesh. In Three.js, materials can have several parameters, including a base color, transparency, and reflectivity. Materials may also include one or more texture maps (also known simply as textures), bitmaps used to represent various surface attributes. Textures can be used in simple ways to define just the color of a surface, or they can be combined to create complex effects such as bumps or highlights. In this first example, we will keep it simple and use a highresolution satellite image of the Earth for our texture. We create a texture object from the image file using the Three.js utility function THREE.ImageUtils.loadTexture(). The texture map for our Earth’s surface is shown in Figure 3-4. Note that this image is a projection of 3D surface data into a 2D bitmap. It has been designed for use as a texture wrapped onto a sphere. Three.js contains the logic for mapping locations within the 2D texture (known as UV coordinates; see later in this chapter) to vertex positions on the sphere mesh, so we don’t have to. You may have noticed that our sample Earth looks a bit flat. That is because of the type of material we are using. In this example, we use THREE.MeshBasicMaterial, a type that does not employ any shading at all. This material type simply paints the pixels supplied in the texture map, or if there is none, it uses the color value of the material. We know we can do better than this in rendering our planet—and we will in the next section. I showed you this method here by way of illustrating WebGL’s great flexibility in rendering styles. You can render with fully realistic lighting, or if you choose, you can render your models as in Example 3-1, a rendering style known as unlit, or alternatively, prelit. (Confusing terminology, I know. But the idea behind prelit rendering is that the 36
|
Chapter 3: Graphics
www.it-ebooks.info
Figure 3-3. Wireframe rendering of a sphere
Figure 3-4. Texture map of Earth’s surface Creating Meshes
www.it-ebooks.info
|
37
lighting has been precomputed into the texture prior to runtime and thus does not have to be computed by the renderer; it simply renders the precomputed values with no additional lighting effects needing to be calculated at runtime.) In our example, pre lighting the model doesn’t look so great, but for some applications, it is just what you want. To complete the setup, our init() method sets a rotation value on the mesh (for the tilt of the Earth’s axis), and calls the inherited setObject3D() method so that the framework knows what Three.js object it is dealing with. Finally, our Earth object’s custom update() method rotates the sphere by a predefined amount each time, simulating the Earth’s rotation so that we can get a good look at the whole planet.
Using Materials, Textures, and Lights The preceding example illustrated the simplest way that we can render a textured model in Three.js, using a basic material, a single texture, and no lighting. In this section, we are going to explore using materials, textures, and lights in various ways to add more realism to our Earth. First, we are going to light the model, to simulate how the sun’s rays reach the Earth across space. Figure 3-5 shows a lit version of the Earth in which we can see gradations of shading across the surface of the sphere. To run this example, load Chapter 3/graphics-earth-lit.html in your browser. On the left side, we see the Arabian Peninsula and Africa brightly lit; toward the front, the Indian Ocean shows shiny highlights; on the right, East Asia and Australia recede into darkness. We create this effect by placing a light source a bit out of the scene and to the left. Let’s talk about the light and material types Three.js provides for doing this.
Types of Lights Three.js supports several different types of lights. There are ambient lights, which provide constant illumination throughout the scene, regardless of position; point lights, which emanate from a particular position in all directions, and illuminate over a certain dis tance; spot lights, which emanate from a particular position and in a specific direction, over a certain distance (and look a lot like spotlights you see in the movies); and finally, directional lights, which illuminate in a specific direction but over an infinite distance (and don’t have any particular position). Our real sun emits light in all directions, so for our simulated sun, we will choose a point light. Because we have a nice framework for making new Three.js objects and adding them to the scene, let’s take advantage of it by creating another Sim.Object subclass called Sun. The listing in Example 3-2 contains excerpts from the file earth-lit.js that implement this new class and add it to the EarthApp simulation.
38
|
Chapter 3: Graphics
www.it-ebooks.info
Figure 3-5. Earth with lighting Example 3-2. Lighting the Earth // Our custom initializer EarthApp.prototype.init = function(param) { // Call superclass init code to set up scene, renderer, default camera Sim.App.prototype.init.call(this, param); // Create the Earth and add it to our sim var earth = new Earth(); earth.init(); this.addObject(earth); // Let there be light! var sun = new Sun(); sun.init(); this.addObject(sun); } ... Earth.prototype.init = function() {
Using Materials, Textures, and Lights
www.it-ebooks.info
|
39
// Create our Earth with nice texture var earthmap = "./images/earth_surface_2048.jpg"; var geometry = new THREE.SphereGeometry(1, 32, 32); var texture = THREE.ImageUtils.loadTexture(earthmap); var material = new THREE.MeshPhongMaterial( { map: texture } ); var mesh = new THREE.Mesh( geometry, material ); // Let's work in the tilt mesh.rotation.z = Earth.TILT; // Tell the framework about our object this.setObject3D(mesh); } ... // Custom Sun class Sun = function() { Sim.Object.call(this); } Sun.prototype = new Sim.Object(); Sun.prototype.init = function() { // Create a point light to show off the earth - set the light out back and to left a bit var light = new THREE.PointLight( 0xffffff, 2, 100); light.position.set(-10, 0, 20); // Tell the framework about our object this.setObject3D(light); }
In EarthApp.init(), we add the lines of code to create our sun, shown in boldface. In the Earth initialization, we substitute a new kind of material: namely, THREE.Mesh PhongMaterial. This material type implements a simple, fairly realistic-looking shading model (called “Phong shading”) with high performance. With this in place, we can now light our model. (In fact, we have to light our model: the Three.js built-in Phong material requires a light source, or we won’t see anything.) The remainder of the listing shows the implementation of the Sun class. We create a point light, and locate it to the left (negative x) and out of the screen a bit (positive z). Then, we add it to the scene, just like we did with our Earth mesh. (In Three.js, lights are first-class objects, just like meshes. That makes it easy to move and otherwise trans form their properties.) Now our Earth is lit and is starting to look more like the real thing.
40
|
Chapter 3: Graphics
www.it-ebooks.info
At this point, it might be worth reminding you that lights and materials are not built-in features of WebGL; rather, all the so-called material properties rendered in a WebGL scene are the result of executing one or more shaders. Materials and lights are, however, common concepts in 3D graphics, and much of the value provided in a toolkit like Three.js lies in its built-in shaders that implement material in a way that most developers can use them out of the box.
Creating Serious Realism with Multiple Textures Our Earth model is looking pretty good, but we can do even better. The real Earth has elevation—mountains and such that can be seen from a distance. It also has a wide range of bright and dark areas. And of course, the Earth has an atmosphere with clouds that are continually in motion. With just a bit more work, we can add these features and experience a much more realistic Earth, as depicted in Figure 3-6 (file Chapter 3/ graphics-earth-shader.html).
Figure 3-6. Earth rendered using multiple textures
Using Materials, Textures, and Lights
www.it-ebooks.info
|
41
Note the apparent elevations on the Eurasian land mass and Indonesia. Also, in the live example—not so much in the screenshot—you can see that some spots are much shinier than in the preceding version, especially inland bodies of water. Finally, we see a cloud layer that, when you are running the live example, moves slowly against the backdrop of the Earth’s surface. Now that is one beautiful planet! Knowing what you know now about meshes and polygons, you might be tempted to think that this detailed goodness comes from heaping on more polygons—perhaps thousands of them. But it doesn’t. We can achieve this high level of realism by simply using a new material type and a few more textures. In addition to the built-in materials we have seen thus far, Three.js provides a general-purpose type called THREE.Shader Material. As the name implies, this material allows the developer to supply a shader program along with other parameters. But don’t freak out—we don’t have to write a shader program ourselves; Three.js provides a small library of prebuilt ones that im plement some of the more common effects. To create our highly realistic view of the Earth, including elevations and shiny highlights, we are going to employ three texture maps: A color map (also known as a diffuse map) This provides the base pixel color (i.e., the satellite imagery of the Earth’s surface we used in the previous version; see Figure 3-3). A normal map (otherwise known as a bump map) Normal maps are essentially encodings of additional mesh attributes, known as normals, into bitmap data as RBG values. Normals determine how much light bounces off a mesh’s surface, resulting in lighter or darker areas depending on the value. Normals create the “bumpiness” of the Earth in a much more compact and high-performance format than defining another vertex for each elevation. The normal map for our Earth is shown in Figure 3-7. Compare this map to the quads depicted in our wireframe drawing of the Earth sphere in Figure 3-3; it is of a much higher resolution. By encoding elevations in normal maps, we can save vertex memory in our meshes, and improve rendering performance. A specular map The term specular highlights, or specular reflection, refers to how much and what color light bounces off a mesh’s surface. Similar to normal maps, the specular map is a more efficient encoding of these values. The specular map we use to define shiny areas of the Earth is shown in Figure 3-8. Lighter RBG values represent shinier areas; darker values, less shiny.
42
|
Chapter 3: Graphics
www.it-ebooks.info
Figure 3-7. Earth elevations using a normal map
Figure 3-8. Earth shininess using a specular map Let’s take a look at the changes we made to our Earth class to get these effects. The code listing in Example 3-3 shows the relevant bits from source file Chapter 3/earth-shader.js. Example 3-3. Earth rendered with multiple textures Earth.prototype.init = function() { // Create a group to contain Earth and Clouds var earthGroup = new THREE.Object3D();
Using Materials, Textures, and Lights
www.it-ebooks.info
|
43
// Tell the framework about our object this.setObject3D(earthGroup); // Add the earth globe and clouds this.createGlobe(); this.createClouds(); } Earth.prototype.createGlobe = function() { // Create our Earth with nice texture - normal map for elevation, specular highlights var surfaceMap = THREE.ImageUtils.loadTexture( "./images/earth_surface_2048.jpg" ); var normalMap = THREE.ImageUtils.loadTexture( "./images/earth_normal_2048.jpg" ); var specularMap = THREE.ImageUtils.loadTexture( "./images/earth_specular_2048.jpg" ); var shader = THREE.ShaderUtils.lib[ "normal" ], uniforms = THREE.UniformsUtils.clone( shader.uniforms ); uniforms[ "tNormal" ].texture = normalMap; uniforms[ "tDiffuse" ].texture = surfaceMap; uniforms[ "tSpecular" ].texture = specularMap; uniforms[ "enableDiffuse" ].value = true; uniforms[ "enableSpecular" ].value = true; var shaderMaterial = new THREE.ShaderMaterial({ fragmentShader: shader.fragmentShader, vertexShader: shader.vertexShader, uniforms: uniforms, lights: true }); var globeGeometry = new THREE.SphereGeometry(1, 32, 32); // We'll need these tangents for our shader globeGeometry.computeTangents(); var globeMesh = new THREE.Mesh( globeGeometry, shaderMaterial ); // Let's work in the tilt globeMesh.rotation.z = Earth.TILT; // Add it to our group this.object3D.add(globeMesh); // Save it away so we can rotate it this.globeMesh = globeMesh; }
44
|
Chapter 3: Graphics
www.it-ebooks.info
Earth.prototype.createClouds = function() { // Create our clouds var cloudsMap = THREE.ImageUtils.loadTexture( "./images/earth_clouds_1024.png" ); var cloudsMaterial = new THREE.MeshLambertMaterial( { color: 0xffffff, map: cloudsMap, transparent:true } ); var cloudsGeometry = new THREE.SphereGeometry(Earth.CLOUDS_SCALE, 32, 32); cloudsMesh = new THREE.Mesh( cloudsGeometry, cloudsMaterial ); cloudsMesh.rotation.z = Earth.TILT; // Add it to our group this.object3D.add(cloudsMesh); // Save it away so we can rotate it this.cloudsMesh = cloudsMesh; } Earth.prototype.update = function() { // "I feel the Earth move..." this.globeMesh.rotation.y += Earth.ROTATION_Y; // "Clouds, too..." this.cloudsMesh.rotation.y += Earth.CLOUDS_ROTATION_Y; Sim.Object.prototype.update.call(this); } Earth.ROTATION_Y = 0.001; Earth.TILT = 0.41; Earth.CLOUDS_SCALE = 1.005; Earth.CLOUDS_ROTATION_Y = Earth.ROTATION_Y * 0.95;
First, we have broken out the initialization into a few helper functions because it’s getting a bit more involved. We have also changed our base Three.js object from a single mesh to a different type: THREE.Object3D. This class is a base type inherited by other Three.js objects we have already seen, such as meshes, lights, and cameras. THREE.Object3D contains position, orientation, and scale properties used by all of these types; in addition, it can contain a list of children: other Three.js objects that will move, rotate, and scale along with it when those properties are changed. We are going to talk about this in more detail when we discuss the transform hierarchy a little later. For now, we are using Object3D just to contain a couple of other objects, namely the Earth sphere and a second sphere used to depict the cloud layer. Now on to the exciting part: the shader that implements normal mapping and specular highlights. For this, we call on THREE.ShaderUtils. This utility class contains a library Using Materials, Textures, and Lights
www.it-ebooks.info
|
45
of prebuilt shader programs. In this case, we will use the normal mapping shader con tained in THREE.ShaderUtils.lib["normal"]. We then create an object called uniforms that contains several named properties; these are the parameters to the shader program. (The terminology uniforms has a very specific meaning with shaders; we will cover this and other shader topics a little later on.) The normal mapping shader requires at least a normal map texture to compute the bumps; we supply that texture, plus the diffuse and specular texture maps. We also set a couple of flags to tell the shader to also use the diffuse and specular values in the computed result. (These flags are off by default; try setting them to false and see what happens.) With our shader program in place, we can create the THREE.ShaderMaterial for our mesh, using our normal mapping shader program, the uniform parameters, and telling the shader to use lighting. We must do one more thing for the shader to work properly: compute tangents. Tangents are additional vectors required for each vertex in order to compute normal mapping values. Three.js does not calculate tangents for geometries by default, so we have to take this explicit step in this case. Once our geometry is set up, we create a mesh and add it as a child of our group. And that’s it—we have a normalmapped Earth with specular highlights. Sweet!
Textures and Transparency Continuing with Example 3-3, we want to add our cloud layer. This involves employing another weapon in our arsenal: using textures with transparency. Just as with 2D web graphics, WebGL supports textures with an alpha channel in PNG format. But in WebGL, that texture can be mapped onto any kind of object, and drawn so that the transparent bits expose other objects behind it (i.e., WebGL supports alpha blending). Three.js makes this pretty easy to deal with by simply supplying a PNG with alpha and setting the transparent flag of the material being used. Let’s have a look. The helper method createClouds() builds a second sphere, this one just a bit bigger than the Earth. Then, we create a material with transparency turned on. Note that we are using a material type we haven’t seen before: MeshLambertMaterial. In Lambert shading, the apparent brightness of the surface to an observer is the same regardless of the observer’s angle of view. This works really well for clouds, which broadly diffuse the light that strikes them. We put these together, and add them as a second child of our group. Finally, during update(), we spin our cloud mesh at a slightly slower rate than the globe and—voilà!—a cloud layer moving gently across the Earth.
Building a Transform Hierarchy Now that we know how to create meshes and make them look nice, let’s take a look at how to move them around using transforms. Transforms allow us to position, orient,
46
|
Chapter 3: Graphics
www.it-ebooks.info
and scale objects without having to operate directly on their vertices. Recall our Earth examples thus far. To spin the globe, we didn’t loop through the sphere geometry’s vertex positions, moving each one; rather, we changed a single rotation property on the mesh, and the entire Earth rotated. Take this concept a step further: many graphics systems, including Three.js, support the concept of a transform hierarchy, wherein transforming an object transforms all its chil dren, too. We can think of the transform hierarchy as analogous to the DOM parentchild hierarchy, though the comparison is imprecise. As with DOM elements, adding an object to a scene adds all its children; likewise, removing an object removes its chil dren. However, a classic transform hierarchy in 3D graphics does not have the various layout capabilities of a DOM hierarchy, such as document-relative and absolute posi tioning. Instead, 3D transform hierarchies typically use parent-relative positioning, and we will exploit this capability to our advantage. In Three.js, every instance of Object3D has position, rotation, and scale properties. Under the covers, these values are converted into an internally stored matrix that is used to calculate the screen-space positions of the vertices at rendering time. For any child of an Object3D, its matrix values are multiplied together with its parent’s, and so on, all the way up the parent-child hierarchy to the root. So, whenever you move, rotate, or scale an object in Three.js, it moves, rotates, or scales all its children, too. We are going to create one last, small example before we dive into building the full solar system. Let’s add a moon orbiting our Earth, using a transform hierarchy. You will see that setting it up this way makes simple work of an otherwise hard problem. In addition, this structure prepares us so that we can drop our Earth model into a full planetary system, and, thanks to the magic of the transform hierarchy, it will just work. Example 3-4 shows an excerpt from the Earth/moon system implemented in Chapter 3/earth-moon.js. You can run the example by loading the file Chapter 3/graphics-earthmoon.html. Example 3-4. Earth and moon transform hierarchy Earth.prototype.init = function() { // Create a group to contain Earth and Clouds var earthGroup = new THREE.Object3D(); // Tell the framework about our object this.setObject3D(earthGroup); // Add the earth globe and clouds this.createGlobe(); this.createClouds(); // Add the moon this.createMoon(); }
Building a Transform Hierarchy
www.it-ebooks.info
|
47
... Earth.prototype.createMoon = function() { var moon = new Moon(); moon.init(); this.addChild(moon); } ... // Custom Moon class Moon = function() { Sim.Object.call(this); } Moon.prototype = new Sim.Object(); Moon.prototype.init = function() { var MOONMAP = "./images/moon_1024.jpg"; var geometry = new THREE.SphereGeometry(Moon.SIZE_IN_EARTHS, 32, 32); var texture = THREE.ImageUtils.loadTexture(MOONMAP); var material = new THREE.MeshPhongMaterial( { map: texture, ambient:0x888888 } ); var mesh = new THREE.Mesh( geometry, material ); // Let's get this into earth-sized units (earth is a unit sphere) var distance = Moon.DISTANCE_FROM_EARTH / Earth.RADIUS; mesh.position.set(Math.sqrt(distance / 2), 0, -Math.sqrt(distance / 2)); // Rotate the moon so it shows its moon-face toward earth mesh.rotation.y = Math.PI; // Create a group to contain Earth and Satellites var moonGroup = new THREE.Object3D(); moonGroup.add(mesh); // Tilt to the ecliptic moonGroup.rotation.x = Moon.INCLINATION; // Tell the framework about our object this.setObject3D(moonGroup); // Save away our moon mesh so we can rotate it this.moonMesh = mesh; } Moon.prototype.update = function() {
48
|
Chapter 3: Graphics
www.it-ebooks.info
// Moon orbit this.object3D.rotation.y += (Earth.ROTATION_Y / Moon.PERIOD); Sim.Object.prototype.update.call(this); } Moon.DISTANCE_FROM_EARTH = 356400; Moon.PERIOD = 28; Moon.EXAGGERATE_FACTOR = 1.2; Moon.INCLINATION = 0.089; Moon.SIZE_IN_EARTHS = 1 / 3.7 * Moon.EXAGGERATE_FACTOR;
First, we create a new object class, Moon, to represent our moon. The Earth’s initialization method calls a helper, createMoon(), to create an instance of the moon and add it as a child of itself. Under the covers, Sim.Object.addChild() adds the object’s private Three.js Object3D as a child of its own Object3D—that is, the Three.js magic for wiring up the transform hierarchy. In addition, our moon has itself created another internal Three.js object, moonGroup, into which it adds the moon mesh. This might seem like overkill—the group is only going to contain one mesh, so why create an extra group? But in fact, we are going to make use of this extra object in a moment. To place the moon where it belongs, we position the mesh. Note that in our previous examples, we have never set the Earth’s position explicitly. By default, objects are placed at the origin (0, 0, 0), so in this case, the Earth is centered at the origin. We position the moon mesh at its proper distance from the Earth, in Earth-sized units. We also set up a few rotations so that the moon sits at its proper angle to the Earth, with its well-known face showing toward us. Now, we are ready to discuss how the transform hierarchy comes into play. First, by making the moon a child of the Earth, we can reuse this code in other simulations such as a full solar system: whenever the Earth wanders its way around the solar system, because it is a child, the moon will follow. Second, because we have created an internal transform hierarchy for the moon itself, we can easily accomplish a fairly complex op eration, namely having the moon orbit around the Earth. Here’s how it works: the moonGroup object contains a mesh; the mesh is positioned at a certain distance from the Earth using the mesh’s transform properties. By rotating the group, not the mesh, we can move the moon in a circular orbit around the Earth, with its familiar face always facing toward Earth, just as it does in real life. (Well, almost; we all know that the true orbit of the moon is quite elliptical. But that’s a bigger kettle of fish. For our purposes here, we are happy to cheat with a circle.) If we had simply created the mesh and not the containing group, then when we rotated the mesh it would literally rotate (i.e., spin in place) and not orbit the Earth. Figure 3-9 shows our nearest celestial neighbor, caught in the act.
Building a Transform Hierarchy
www.it-ebooks.info
|
49
Figure 3-9. The moon in orbit around the Earth
Creating Custom Geometry Let’s continue our journey outward past the moon. For the remainder of this chapter, we will be using the full solar system example, which can be found in Chapter 3/graphicssolar-system.html and its associated files. Our simple solar system model incorporates the Earth-moon system but no other plan etary satellites. We can live without those, and other distinguishing bodies in the solar system such as the asteroid belt—but it would be hard to build a believable orrery without troubling with one additional bauble: Saturn’s rings. While Three.js has a rich set of prebuilt geometry types, it does not include something that can pass for Saturn’s rings. Essentially we need a disc with a hole in it, that we can texture with some transparency to emulate the look of the rings. Three.js has a built-in torus object, but no matter how you try to flatten using scaling or other tricks, it looks like a donut, not a disc. So, it appears that we will need to make our own geometry. We could go about this in one of two ways: either (1) create a model using a modeling package such as Blender or 3ds Max and export it to one of several formats Three.js
50
|
Chapter 3: Graphics
www.it-ebooks.info
knows how to load; or (2) create the geometry in code by building a custom subclass of
THREE.Geometry. Because we are going to cover 3D modeling packages in a later chapter,
let’s have a look at how to customize Three.js by building our own geometry class. Example 3-5 shows the code, which can be found in the file Chapter 3/saturn.js. Example 3-5. Custom Geometry class for Saturn’s rings // The rings Saturn.Rings = function ( innerRadius, outerRadius, nSegments ) { THREE.Geometry.call( this ); var outerRadius = outerRadius || 1, innerRadius = innerRadius || .5, gridY = nSegments || 10; var i, twopi = 2 * Math.PI; var iVer = Math.max( 2, gridY ); var origin = new THREE.Vector3(0, 0, 0); //this.vertices.push(new THREE.Vertex(origin)); for ( i = 0; i < ( iVer + 1 ) ; i++ ) { var var var var var var var var var var
fRad1 fRad2 fX1 = fY1 = fX2 = fY2 = fX4 = fY4 = fX3 = fY3 =
= i / iVer; = (i + 1) / innerRadius innerRadius outerRadius outerRadius innerRadius innerRadius outerRadius outerRadius
iVer; * Math.cos( * Math.sin( * Math.cos( * Math.sin( * Math.cos( * Math.sin( * Math.cos( * Math.sin(
fRad1 fRad1 fRad1 fRad1 fRad2 fRad2 fRad2 fRad2
var v1 = new THREE.Vector3( fX1, fY1, var v2 = new THREE.Vector3( fX2, fY2, var v3 = new THREE.Vector3( fX3, fY3, var v4 = new THREE.Vector3( fX4, fY4, this.vertices.push( new THREE.Vertex( this.vertices.push( new THREE.Vertex( this.vertices.push( new THREE.Vertex( this.vertices.push( new THREE.Vertex(
* * * * * * * *
0 ); 0 ); 0 ); 0 ); v1 ) v2 ) v3 ) v4 )
twopi twopi twopi twopi twopi twopi twopi twopi
); ); ); ); ); ); ); );
); ); ); );
} for ( i = 0; i < iVer ; i++ ) { this.faces.push(new THREE.Face3( i * 4, i * 4 + 1, i * 4 + 2)); this.faces.push(new THREE.Face3( i * 4, i * 4 + 2, i * 4 + 3)); this.faceVertexUvs[ 0 ].push( [ new THREE.UV(0, 1),
Creating Custom Geometry
www.it-ebooks.info
|
51
new THREE.UV(1, 1), new THREE.UV(1, 0) ] ); this.faceVertexUvs[ 0 ].push( [ new THREE.UV(0, 1), new THREE.UV(1, 0), new THREE.UV(0, 0) ] ); } this.computeCentroids(); this.computeFaceNormals(); this.boundingSphere = { radius: outerRadius }; }; Saturn.Rings.prototype = new THREE.Geometry(); Saturn.Rings.prototype.constructor = Saturn.Rings;
Saturn.Rings subclasses THREE.Geometry. Its entire job is to create the vertices and faces of our ring geometry. We pass in an inner and outer radius, as well as a number of segments to generate, in order to control the level of resolution. First, we do a little trigonometry to generate vertex positions and push them onto the object’s array of vertices. Then, we use that list to create the polygons, or faces, of the mesh (in this case, triangles). This defines the shape of our ring. See Figure 3-10 for a wireframe rendering.
Figure 3-10. Wireframe rendering of ring geometry In order to properly texture the mesh, we also need to supply Three.js with texture coordinates, also known as UV coordinates. UV coordinates define the mapping of pixels in a texture to vertices in a face. UV coordinates are typically specified in a space ranging from 0 to 1 horizontally and vertically, with U values increasing to the right and V values 52
|
Chapter 3: Graphics
www.it-ebooks.info
increasing upward. In our example, each pair of triangles defines a quad that is going to be textured with the simple map depicted in Figure 3-11. This is a PNG file with some transparency, to give us the effect of the separation visible in some of Saturn’s rings. The pixels of this texture are mapped in such a way that the leftmost part of the texture starts at the inside of the ring, and the rightmost part ends at the outer part of the ring.
Figure 3-11. Texture map for Saturn’s rings Once we have set up the vertices, faces, and texture coordinates, we just need to do a little bookkeeping required by all Three.js geometry types. We call upon computeCent roids() and set the boundingSphere to provide information necessary for picking and culling (topics we’ll cover later), and call computeFaceNormals() so that Three.js knows how to light the object. Finally, we set the object’s constructor property so that we are playing nice with the Three.js framework. The final result, a fair approximation of Sat urn’s familiar rings, is shown in Figure 3-12.
Figure 3-12. Saturn’s rings with PNG texture
Creating Custom Geometry
www.it-ebooks.info
|
53
Rendering Points and Lines Thus far, all of our talk about rendering has been confined to polygonal meshes, ulti mately based on triangles. Triangles are the fundamental rendering primitive available in WebGL, and by far the most commonly used. WebGL eats triangles for breakfast—a good implementation on good hardware can process millions of them per second in real time. But there are two other important primitives available to us: points and lines. We’ll make use of both to add a bit of polish to our model.
Point Rendering with Particle Systems The solar system would look pretty lonely without some stars in the background. So let’s create some by rendering points. Three.js has built-in support for WebGL point ren dering using its ParticleSystem object. In general, particle systems are extremely pow erful—they can be used to create effects such as fire, explosions, rain, snow, confetti, and so on. We will be discussing them in more detail in a later chapter. For now, we are going to use a particle system to simply render a static set of points that don’t move around on the screen. A particle system is described by a set of points—Three.js vector objects—that define positions in space, plus a material. The material defines a base color, a size for drawing the point (in pixels), and other optional parameters such as a texture map. We are going to keep it and just draw points with a color and size. Have a look at the listing in Example 3-6 (file Chapter 3/stars.js). Example 3-6. Rendering stars using a particle system (WebGL point primitive) Stars.prototype.init = function(minDistance) { // Create a group to hold our Stars particles var starsGroup = new THREE.Object3D(); var i; var starsGeometry = new THREE.Geometry(); // Create random particle locations for ( i = 0; i < Stars.NVERTICES; i++) { var vector = new THREE.Vector3( (Math.random() * 2 - 1) * minDistance, (Math.random() * 2 - 1) * minDistance, (Math.random() * 2 - 1) * minDistance); if (vector.length() < minDistance) { vector = vector.setLength(minDistance); }
54
|
Chapter 3: Graphics
www.it-ebooks.info
starsGeometry.vertices.push( new THREE.Vertex( vector ) ); } // Create a range of sizes and colors for the stars var starsMaterials = []; for (i = 0; i < Stars.NMATERIALS; i++) { starsMaterials.push( new THREE.ParticleBasicMaterial( { color: 0x101010 * (i + 1), size: i % 2 + 1, sizeAttenuation: false } ) ); } // Create several particle systems spread around in a circle, cover the sky for ( i = 0; i < Stars.NPARTICLESYSTEMS; i ++ ) { var stars = new THREE.ParticleSystem( starsGeometry, starsMaterials[ i % Stars.NMATERIALS ] ); stars.rotation.y = i / (Math.PI * 2); starsGroup.add( stars ); }
// Tell the framework about our object this.setObject3D(starsGroup); } Stars.NVERTICES = 667; Stars.NMATERIALS = 8; Stars.NPARTICLESYSTEMS = 24;
First, we create a group to hold the particle systems. Then, we create an empty
THREE.Geometry object that is going to be used to hold the vertices we generate for the
point locations. Next, we generate several hundred random points, making sure they lie at a minimum distance away from our sun, past Pluto—we don’t want any points over lapping our planet geometry and ruining the illusion! Now, we make a few different materials of type THREE.ParticleBasicMaterial. This object defines the point size and color for the particle system; we want a range of colors
Rendering Points and Lines
www.it-ebooks.info
|
55
(grayscale) and sizes to emulate stars of different magnitude. Note the third parameter, sizeAttenuation. By setting it to false, we are telling Three.js not to bother trying to resize each particle as the camera moves. We want our stars to look far away no matter what. Now we are ready to create our ParticleSystem objects. We are actually going to create several and spread them around in a circle, picking a random material for each one. We add each particle system to the containing group and we’re done. The combination of random sizes and positions and gray/white color values makes for a convincing—if not scientifically accurate—stellar background.
Line Rendering Orbit lines are a classic feature of solar system illustrations and computer models. In my opinion, no simulation would be complete without them. Happily, Three.js has a builtin line type that does WebGL rendering for us in a snap. Let’s look at how it’s done. The code listing for file Chapter 3/orbit.js can be found in Example 3-7. Example 3-7. Rendering orbits with lines // Custom Orbit class Orbit = function() { Sim.Object.call(this); } Orbit.prototype = new Sim.Object(); Orbit.prototype.init = function(distance) { // Create an empty geometry object to hold the line vertex data var geometry = new THREE.Geometry(); // Create points along the circumference of a circle with radius == distance var i, len = 60, twopi = 2 * Math.PI; for (i = 0; i