My favorites | Sign in
o3d
Project Home Downloads Wiki Issues Source
Search
for
SampleCodeWalkthrough  
This page discusses a sample application for the WebGL implementation of O3D.
Updated May 7, 2010 by jos...@google.com

Sample Application: Online Pool

Pool is a demo that shows how the WebGL implementation of O3D can be used to make a playable interactive game of pool. All models for the game are constructed using the o3djs utility library without loading geometry from files. The game loads a single image of pool ball patterns and separates that image into sixteen bitmaps to make sixteen textures, one for each ball. Physics for the game are done in JavaScript. The balls are confined to the plane of the table, but the collision resolution is done using 3D angular momentum for each ball. You can see the effect of this if you make a nice hard break shot a little off center and watch how the balls spin on the table. If you do it just right, some will spin around vertical axes.

Models

The models for Pool are generated using the o3djs.primitives library. The o3djs prism object must be convex, but the table has parts that are not convex, so the table is subdivided into convex pieces. The function flatMesh() was written to aide in making the cushions is generated by hand. flatMesh() creates a shape given a list of vertex positions and a list of lists of indices to describe the faces. It triangulates each face and computes normals perpendicular to each face.

Although a prefabricated artistic model of a pool table loaded from a file might look more realistic, generating the model procedurally has the advantage that dimensions of the table are adjustable constants in the code. For instance, you can make the pockets a different size by changing the global variable g_pocketRadius.

Materials

Fragment shader code for the models in Pool is stored in one big string. All 3D models use the same vertex shader. When the shader is compiled, the fragment shader used is determined by appending a main function to the end of the shader string. The shader compiler is expected to do eliminate dead code.

   var mainString =
     'void main() {' +
     '  gl_FragColor = ' + name + 'PixelShader();' +
     '}';

   effect.loadVertexShaderFromString(vertexShaderString);
   effect.loadPixelShaderFromString(pixelShaderString + mainString);

This way, pixel shaders can call any other functions inside the shader string. The function lighting(), which does a variant of the standard Phong lighting calculation, gets called by most of the other pixel shaders. The color of the felt pigment is determined by a function. To change the color of the felt, therefore, you only have to change it in one place.

 vec4 feltPigment(vec3 p) {
   return vec4(0.1, 0.45, 0.15, 1.0);
 }

If you cange the color of the felt in that function, the rails which use a different shader and the subtle reflection in the bottom of the ball will change too.

To add a new material, you only have to add its pixel main function to the shader code and add the name of the material here:

 g_materials = {
     'solid':{},
     'felt':{},
     'wood':{},
     'cushion':{},
     'billiard':{},
     'ball':{},
     'shadowPlane':{}};

This method of putting all shaders in one string would probably not be practical for a complicated game with lots of shaders, but for Pool, with just a handful of shaders, we get away with it.

Shadows

Each ball's shadow is accomplished by drawing the top face of the table to a render target using a shader that takes the position of the ball as a uniform parameter.

 vec4 shadowPlanePixelShader() {
   vec2 p = vworldPosition.xy - ballCenter;
   vec2 q = (vworldPosition.xy / lightWorldPosition.z);

   vec2 offset = (1.0 - 1.0 / (vec2(1.0, 1.0) + abs(q))) * sign(q);
   float t = mix(smoothstep(0.9, 0.0, length(p - length(p) * offset) / 2.0),
                 smoothstep(1.0, 0.0, length(p) / 10.0), 0.15);
   return shadowOn * vec4(t, t, t, t);
 }

The preceding code draws two soft white blobs originating from the center of the ball. The call to lerp blends the two blobs together. The first blob is meant to be the shadow cast by the light onto the table. For a touch of realism, that blob is skewed away from the light source. The second blob is meant to be a subtle ambient occlusion effect, so it falls off evenly. You can see the white blobs that get drawn by setting the variable SHADOWPOV to true.

When the table is rendered to the screen, the shader for the felt samples the render target of of the shadow pass and darkens the felt where that render target is bright. Because the render target is in screen space, the shadow is still smooth even if you zoom in a lot.

Physics calculations are done in a JavaScript object of type pool.Physics. This includes collision detection, collision resolution, and determining which balls have sunk in which pockets. Each ball has a 3D vector velocity (with z component = 0), and a 3D angular velocity vector. Physics are computed in five time steps per frame. You can see this in the function step(), which is called once per animation frame.

 this.step = function() {
   for (var i = 0; i < 5; ++i) {
     this.ballsLoseEnergy();
     this.ballsImpactFloor();
     this.move(1);
     while (this.collide()) {
       this.move(-1);
       this.handleCollisions();
       this.move(1);
     }
   }
   this.sink();
   this.handleFalling();
   this.placeBalls();
 };

In each time step, each ball is moved according to its velocity vector and then tested for collision. Information about each collision is recorded into a list. Then (and this is important) the balls move back to where they were at the beginning of the time step, and after that, the collisions are resolved. This is repeated until moving the balls forward results in no collisions. In this way, it is held invariant that no balls overlap. Once a position for the next time step is determined that sees no balls overlapping, the time step can advance.


Sign in to add a comment
Powered by Google Project Hosting