Friday, September 23, 2011

PMRPC.js 3D

PMRPC.js for Physics Web Workers

This stream of ideas began with a simple prototype which moved ammo.js physics into a worker. Rendering with three.js/webgl library was smoother with ammo.js running in the worker but the code was a mess. see live demo of original non-pmrpc.js version here
pmrpc .js was used to clean up the code by wrapping up communication between the main (rendering thread) and the worker (physics thread). This post is to explain the current state of code as an example of use of pmrps.js for communication with workers and as a useful test-bench for further experimentation with physics in workers.

The example has three files:
  • index.html: loads libraries used by the main thread
  • client.js: the main thread which renders the scene with three.js
  • worker.js: a single worker which runs ammo.js
Index.html is very simple so we're gong to begin by explaining how client.js works:

client.js (1) sets globals, (2) initializes the rendering of the scene, (3) sets up the mouse listeners, (4) sends an initialization message to the worker to start up physics simulation with ammo.js, (5) receives and processes updates from physics and (6) passes user commands to the worker.

(1) setting globals: these are mostly properties needed either by the rendering engine (such as window size) or by the physics engine (such as number of blocks to simulate).  The array "boxes" will be used for the keeping track and moving around all the dynamic objects in the scene.

var shots = 5;
var NUM = 30;  //number of blocks to simulate
var container, stats;
var camera, scene, renderer, objects;
var pointlight;
var dt;
var myprojectiletype = "sphere";  //"box" //"cone"
var myprojectilespeed = 150;
var w = window.innerWidth;
var h = window.innerHeight;
var windowHalfX = window.innerWidth / 2;
var windowHalfY = window.innerHeight / 2;
var workerCount = 1;
var delta = 40;  //physics time step in ms
var boxes = [];

(2) initialize the rendering of the scene: camera, lights, floor, objects and the webgl renderer are all created.

function init() { 
container = document.createElement("div"); 
document.body.appendChild(container); 
scene = new THREE.Scene(); 
addCamera(); 
addLights(); 
addGrid(); 
for (var i = 0; i <= NUM; i++)  //add cubes and projectile to the scene  
  createCube(i); 
renderer = new THREE.WebGLRenderer();  //set up for rendering 
renderer.setSize(window.innerWidth, window.innerHeight); 
renderer.setClearColor(new THREE.Color(0x99CCFF), 1); 
container.appendChild(renderer.domElement);
creating the camera: this is a standard three.js "trackball camera" which allows the user to use the three mouse buttons to rotate, zoom and pan through the scene.
function addCamera() { 
camera = new THREE.TrackballCamera({  
   fov: 60,  
   aspect: window.innerWidth / window.innerHeight,  
   near: 1,  far: 1e3,  
   rotateSpeed: 1.0,  zoomSpeed: 1.2,  panSpeed: 0.8,  
   noZoom: false,  noPan: false,  staticMoving: true,  
   dynamicDampingFactor: 0.3,  
   keys: [65, 83, 68] }); 
   camera.position.x = -15; 
   camera.position.y = 6; 
   camera.position.z = 15; 
   camera.target.position.y = 6.0;}

adding lights: we add a point light which will provide nice shading for our Lambert textures and ambient light so the back sides of the cubes don't look too dark.

function addLights() { 
   var pointLight = new THREE.PointLight(0xffffff); 
   pointLight.position.set(20, 30, 20); 
   scene.addLight(pointLight); 
   var ambientLight = new THREE.AmbientLight(0x909090); 
   scene.addLight(ambientLight);}

adding the floor: we add a simple plane and add a texture with grass to provide a ground plane for our scene. The physical ground plane upon which objects bounce will be added separately in the worker later. Here is an example jsfiddle with a tilted physical ground plane here 
function addGrid() { 
   var geometry = new THREE.PlaneGeometry(100, 100); 
   var xm = []; 
   xm.push(new THREE.MeshBasicMaterial({  map: THREE.ImageUtils.loadTexture('textures/bigGrass.jpg') })); 
   /*  xm.push(new THREE.MeshBasicMaterial({  color: 0x000000,  wireframe: true  }));*/  
   geometry = new THREE.PlaneGeometry(100, 100, 40, 40);  
   var ground = new THREE.Mesh(geometry, xm);  
   ground.position.set(0, 0, 0);  
   ground.rotation.x = -1.57;  
   scene.addObject(ground);}

adding the dynamic objects: boxes[0] is the projectile so it gets different treatment. In general a material is created (baseball for the projectile, bricks for the wall) and then a geometry is created (sphere for the ball, cubes for the wall). Material and Geometry are combined to make a three.js mesh which is then added to the scene. Before physics takes effect you will see a stack of objects at the origin - this is the default position of the objects before physics takes over.

function createCube(i) { 
   var material = []; 
   material.push(new THREE.MeshLambertMaterial({  map: THREE.ImageUtils.loadTexture('textures/redbrick.jpg') })); 
   if((myprojectiletype == "sphere") && (i == 0)){  
      var geometry = new THREE.SphereGeometry(1.4, 16, 8);  
      material.push(new THREE.MeshLambertMaterial({   map:       THREE.ImageUtils.loadTexture('textures/baseball.jpg')  })); 
   } 
   else if ((myprojectiletype == "cone") && (i == 0)){  
      var geometry = new THREE.CylinderGeometry(16, 0.1, 1.0, 1.4);  
      material.push(new THREE.MeshBasicMaterial({"color":0x000000,"wireframe":true})); 
   } 
   else{  
      var geometry = new THREE.CubeGeometry(2, 2, 2); 
   } 
   boxes[i] = new THREE.Mesh(geometry, material); 
   boxes[i].position.x = 0;
   boxes[i].position.y = (i * 10) + 5;
   boxes[i].position.z = 0; 
   if ((myprojectiletype == "cone") && (i == 0)){  
       boxes[i].rotation.x = Math.Pi/2; 
   } 
   scene.addObject(boxes[i]);
}
creating the renderer: boilerplate code for adding the webgl div to the body of the html page and attaching the three.js renderer to the div. 0x99CCFF is for the baby blue sky background.

container = document.createElement("div"); 
   document.body.appendChild(container); 
   renderer = new THREE.WebGLRenderer();  //set up for rendering        
   renderer.setSize(window.innerWidth, window.innerHeight);
   renderer.setClearColor(new THREE.Color(0x99CCFF), 1);     
   container.appendChild(renderer.domElement);

(3) create mouse listeners: the camera has built-in listeners for the mouse so we just need to add a "double-click" listener which we will use to trigger calls the physics worker .
function setEventListeners() { 
   document.addEventListener('dblclick', onDocumentMouseDown, false);
}
function onDocumentMouseDown(event) { 
   if(shots == 0){  
       wall();  //5 shots completed - tell worker to rebuild the wall  
       shots = 5; 
    } else{    
       fire(event.clientX,event.clientY);  //tell the worker to fire the ball 
       shots--; 
}}

(4) initialize the worker: this code creates a worker with the code worker.js and uses a pmrpc call to start up the ammo.js physics engine on the worker.
function startworkers(){ 
    workers = new Array(workerCount);  //in this example 1 worker 
    for (var i = 0; i < workerCount; i++) {  
       workers[i] = new Worker("js/worker.js");  //create the worker 
    } 
    for (var i = 0; i < workerCount; i++) {  //call "initWorker" on the worker  
       pmrpc.call( {   destination : workers[i],   
                       publicProcedureName : "initWorker",  
                       params : [delta,NUM] } ); //send the worker time steps to take and # blocks } 
(5) Listen for updates from the physics worker: this code registers the "update" function which the worker will call to set the positions of the ball and blocks: NOTE- update changes the position of one item for each call - it would seem logical that updating all positions in one call would have less overhead but it proved to be slower (?).
pmrpc.register( {  
      publicProcedureName : "update",  
      procedure : function(i,position,quaternion) 
     {   
          boxes[i].position.x = position[0];  
          boxes[i].position.y = position[1];   
          boxes[i].position.z = position[2];   
          boxes[i].quaternion.x = quaternion[0];  
          boxes[i].quaternion.y = quaternion[1];   
          boxes[i].quaternion.z = quaternion[2];   
          boxes[i].quaternion.w = quaternion[3];   
          boxes[i].useQuaternion = true;  }});
     }
(6) pass user commands to the physics worker: the main thread has two functions, wall and fire which are calls to the worker to make changes in the physical scene.
//pmrpc calls to various physics functions
function wall(){
 for (var i = 0; i < workerCount; i++) {
  //workers[i]. postMessage('{"type":"wall"}');
  pmrpc.call( {
   destination : workers[i],
   publicProcedureName : "wall",
  params : [] } );
 }
}

function fire(x,y) {
 var vector = new THREE.Vector3((x / window.innerWidth) * 2 - 1,
 -(y / window.innerHeight) * 2 + 1,
 1);
 var projector = new THREE.Projector();
 projector.unprojectVector(vector, camera);
 vector.normalize();
 vector.multiplyScalar(myprojectilespeed);
 for (var i = 0; i < workerCount; i++) {
  pmrpc.call( {
   destination : workers[i],
   publicProcedureName : "fire",
  params : [camera.position,vector] } );
 }
}


In this view, web worker is used in a 3D graphic engine. The following code shows us how to declare and make a window for a stack of boxes
on a plane.
Using Web Worker with pmrpc
init(); startworkers(); animate();  function init() { container = document.createElement("div"); document.body.appendChild(container); scene = new THREE.Scene();  addCamera(); addLights(); addGrid(); for (var i = 0; i <= NUM; i++)  //add cubes and projectile to the scene createCube(i); renderer = new THREE.WebGLRenderer(); //set up for rendering renderer.setSize(window.innerWidth, window.innerHeight); renderer.setClearColor(new THREE.Color(0xffff99), 1); container.appendChild(renderer.domElement); setEventListeners(); //double click to fire cannon }


The init function renders the scene, camera, lights, and the container, which has a div element. We are using a library from a software called THREE.js and WebGL. Notice the instantiation for the scene and renderer when we initialize our scene and renderer variable. In the For loop, Strelz's team are creating the number of boxes that was declared in the previous section.

function addCamera() { camera = new THREE.TrackballCamera({ fov: 60, aspect: window.innerWidth / window.innerHeight, near: 1, far: 1e3, rotateSpeed: 1.0, zoomSpeed: 1.2, panSpeed: 0.8, noZoom: false, noPan: false, staticMoving: true, dynamicDampingFactor: 0.3, keys: [65, 83, 68] }); camera.position.x = -15; camera.position.y = 6; camera.position.z = 15; camera.target.position.y = 6.0; } 


Capturing the graphics at a different angle shows some of the attributes that are embedded in the THREE.js library. We instantiate an object called TrackballCamera which provides several attributes. For example, we can modify the window, rotate speed, zoom speed, camera position, etc.

//worker setup - starts worker(s) and registers pmrpc function for physics updates function startworkers()  { workers = new Array(workerCount); for (var i = 0; i < workerCount; i++) { workers[i] = new Worker("worker.js"); }  for (var i = 0; i < workerCount; i++) { pmrpc.call( {  destination : workers[i], publicProcedureName : "initWorker", params : [delta,NUM] } ); }  pmrpc.register( {  publicProcedureName : "update",  procedure : function(i,position,quaternion) {  boxes[i].position.x = position[0]; boxes[i].position.y = position[1]; boxes[i].position.z = position[2]; boxes[i].quaternion.x = quaternion[0]; boxes[i].quaternion.y = quaternion[1]; boxes[i].quaternion.z = quaternion[2]; boxes[i].quaternion.w = quaternion[3]; boxes[i].useQuaternion = true; }}); }   //pmrpc calls to various physics functions function wall(){  for (var i = 0; i < workerCount; i++)  {      //workers [i]. postMessage('{"type":"wall"}'); pmrpc.call( {  destination : workers[i], publicProcedureName : "wall", params : [] } ); } } 


The function startworkers starts the worker(s) and registers the pmrpc function for physics updates. The For loop loops the number of workers that is being used. The pmrpc "call" object has a destination function, which calls the web worker. The publicProcedureName's function calls the initWorker function, which acts as the main for the web worker to communicate. The params function takes in the delta variable to starts the physics clock in milli-seconds, and NUM is the number of boxes you want to create. The pmrpc register object has a publicProceduralName variable defined as "update" that waits for an update of positions from the web worker, which is in the "worker.js" file.

function fire(x,y) { var vector = new THREE.Vector3((x / window.innerWidth) * 2 - 1, -(y / window.innerHeight) * 2 + 1, 1); var projector = new THREE.Projector(); projector.unprojectVector(vector, camera); vector.normalize(); vector.multiplyScalar(myprojectilespeed);   for (var i = 0; i < workerCount; i++) { pmrpc.call( { destination : workers[i], publicProcedureName : "fire", params : [camera.position,vector] } ); } } 


In this section, we are looking at a sphere instantiated by an object called Vector3. The variable "vector" is combine with the attribute "projector" to simulate a ball traveling at a certain speed toward the background.

//creates and renders the animation
function animate() {
requestAnimationFrame(animate);
render();
}

1 comment:

  1. Again -- very cool!

    I may have missed it - but you should add a link to the live demo of the example. It's a shame not to see the action live :)

    Well done!

    ReplyDelete