physics2d.js
  1 /*{# Copyright (c) 2012 Turbulenz Limited #}*/
  2 /*
  3 * @title: 2D Physics
  4 * @description:
  5 * This sample shows how to create and use the Turbulenz 2D physics device.
  6 * The sample creates several 2D static objects with surface velocity,
  7 * two animated kinematic objects to push and lift objects around,
  8 * and a hundred rigid bodies with different material properties.
  9 * Left click to pick up and move the rigid bodies and right click to add new ones.
 10 */
 11 /*{{ javascript("jslib/observer.js") }}*/
 12 /*{{ javascript("jslib/requesthandler.js") }}*/
 13 /*{{ javascript("jslib/utilities.js") }}*/
 14 /*{{ javascript("jslib/services/turbulenzservices.js") }}*/
 15 /*{{ javascript("jslib/services/turbulenzbridge.js") }}*/
 16 /*{{ javascript("jslib/services/gamesession.js") }}*/
 17 /*{{ javascript("jslib/services/mappingtable.js") }}*/
 18 /*{{ javascript("jslib/shadermanager.js") }}*/
 19 /*{{ javascript("jslib/physics2ddevice.js") }}*/
 20 /*{{ javascript("jslib/draw2d.js") }}*/
 21 /*{{ javascript("jslib/boxtree.js") }}*/
 22 /*{{ javascript("jslib/physics2ddebugdraw.js") }}*/
 23 /*{{ javascript("jslib/textureeffects.js") }}*/
 24 /*{{ javascript("scripts/htmlcontrols.js") }}*/
 25 /*global TurbulenzEngine: true */
 26 /*global TurbulenzServices: false */
 27 /*global RequestHandler: false */
 28 /*global Physics2DDevice: false */
 29 /*global Draw2D: false */
 30 /*global Draw2DSprite: false */
 31 /*global Physics2DDebugDraw: false */
 32 /*global HTMLControls: false */
 33 TurbulenzEngine.onload = function onloadFn() {
 34     //==========================================================================
 35     // HTML Controls
 36     //==========================================================================
 37     var htmlControls;
 38 
 39     var debugEnabled = false;
 40     var contactsEnabled = false;
 41 
 42     //==========================================================================
 43     // Turbulenz Initialization
 44     //==========================================================================
 45     var graphicsDevice = TurbulenzEngine.createGraphicsDevice({});
 46     var mathDevice = TurbulenzEngine.createMathDevice({});
 47     var requestHandler = RequestHandler.create({});
 48 
 49     var draw2DTexture;
 50     var gameSession;
 51     function sessionCreated(gameSession) {
 52         TurbulenzServices.createMappingTable(requestHandler, gameSession, function (table) {
 53             graphicsDevice.createTexture({
 54                 src: table.getURL("textures/physics2d.png"),
 55                 mipmaps: true,
 56                 onload: function (texture) {
 57                     if (texture) {
 58                         draw2DTexture = texture;
 59                     }
 60                 }
 61             });
 62         });
 63     }
 64     gameSession = TurbulenzServices.createGameSession(requestHandler, sessionCreated);
 65 
 66     //==========================================================================
 67     // Physics2D/Draw2D
 68     //==========================================================================
 69     // set up.
 70     var phys2D = Physics2DDevice.create();
 71 
 72     // size of physics stage.
 73     var stageWidth = 30;
 74     var stageHeight = 22;
 75 
 76     var draw2D = Draw2D.create({
 77         graphicsDevice: graphicsDevice
 78     });
 79     var debug = Physics2DDebugDraw.create({
 80         graphicsDevice: graphicsDevice
 81     });
 82 
 83     // Configure draw2D viewport to the physics stage.
 84     // As well as the physics2D debug-draw viewport.
 85     draw2D.configure({
 86         viewportRectangle: [0, 0, stageWidth, stageHeight],
 87         scaleMode: 'scale'
 88     });
 89     debug.setPhysics2DViewport([0, 0, stageWidth, stageHeight]);
 90 
 91     var world = phys2D.createWorld({
 92         gravity: [0, 20]
 93     });
 94 
 95     var rubberMaterial = phys2D.createMaterial({
 96         elasticity: 0.9,
 97         staticFriction: 6,
 98         dynamicFriction: 4,
 99         rollingFriction: 0.001
100     });
101 
102     var heavyMaterial = phys2D.createMaterial({
103         density: 3
104     });
105 
106     var conveyorBeltMaterial = phys2D.createMaterial({
107         elasticity: 0,
108         staticFriction: 10,
109         dynamicFriction: 8,
110         rollingFriction: 0.1
111     });
112 
113     var shapeSize = 0.8;
114     var shapeFactory = [
115         phys2D.createCircleShape({
116             radius: (shapeSize / 2),
117             material: rubberMaterial
118         }),
119         phys2D.createPolygonShape({
120             vertices: phys2D.createBoxVertices(shapeSize, shapeSize),
121             material: heavyMaterial
122         }),
123         phys2D.createPolygonShape({
124             vertices: phys2D.createRegularPolygonVertices(shapeSize, shapeSize, 3),
125             material: heavyMaterial
126         }),
127         phys2D.createPolygonShape({
128             vertices: phys2D.createRegularPolygonVertices(shapeSize, shapeSize, 6),
129             material: rubberMaterial
130         })
131     ];
132 
133     // texture rectangles for above shapes.
134     var textureRectangles = [
135         [130, 130, 255, 255],
136         [5, 132, 125, 252],
137         [131, 3, 251, 123],
138         [5, 5, 125, 125]
139     ];
140 
141     // Create a static body at (0, 0) with no rotation
142     // which we add to the world to use as the first body
143     // in hand constraint. We set anchor for this body
144     // as the cursor position in physics coordinates.
145     var handReferenceBody = phys2D.createRigidBody({
146         type: 'static'
147     });
148     world.addRigidBody(handReferenceBody);
149     var handConstraint = null;
150 
151     var animationState = 0;
152     var lift;
153     var pusher;
154 
155     function reset() {
156         // Remove all bodies and constraints from world.
157         world.clear();
158         handConstraint = null;
159 
160         // Create a static border body around the stage to stop objects leaving the viewport.
161         var thickness = 0.01;
162         var border = phys2D.createRigidBody({
163             type: 'static',
164             shapes: [
165                 phys2D.createPolygonShape({
166                     vertices: phys2D.createRectangleVertices(0, 0, thickness, stageHeight)
167                 }),
168                 phys2D.createPolygonShape({
169                     vertices: phys2D.createRectangleVertices(0, 0, stageWidth, thickness)
170                 }),
171                 phys2D.createPolygonShape({
172                     vertices: phys2D.createRectangleVertices((stageWidth - thickness), 0, stageWidth, stageHeight)
173                 }),
174                 phys2D.createPolygonShape({
175                     vertices: phys2D.createRectangleVertices(0, (stageHeight - thickness), stageWidth, stageHeight)
176                 })
177             ]
178         });
179         world.addRigidBody(border);
180 
181         var createBelt = function createBeltFn(x1, y1, x2, y2, radius, speed) {
182             var normal = mathDevice.v2Build(y2 - y1, x1 - x2);
183             mathDevice.v2ScalarMul(normal, radius / mathDevice.v2Length(normal), normal);
184 
185             var shapes = [
186                 phys2D.createPolygonShape({
187                     vertices: [
188                         [x1 + normal[0], y1 + normal[1]],
189                         [x2 + normal[0], y2 + normal[1]],
190                         [x2 - normal[0], y2 - normal[1]],
191                         [x1 - normal[0], y1 - normal[1]]
192                     ],
193                     material: conveyorBeltMaterial
194                 }),
195                 phys2D.createCircleShape({
196                     radius: radius,
197                     origin: [x1, y1],
198                     material: conveyorBeltMaterial
199                 }),
200                 phys2D.createCircleShape({
201                     radius: radius,
202                     origin: [x2, y2],
203                     material: conveyorBeltMaterial
204                 })
205             ];
206             var body = phys2D.createRigidBody({
207                 type: 'static',
208                 surfaceVelocity: [speed, 0],
209                 shapes: shapes
210             });
211 
212             return body;
213         };
214 
215         var belt;
216         belt = createBelt(0, 11, 7, 14, 0.5, 2);
217         world.addRigidBody(belt);
218 
219         belt = createBelt(7, 14, 14, 11, 0.5, 2);
220         world.addRigidBody(belt);
221 
222         belt = createBelt(12, 19, 20.5, 17, 0.5, 2);
223         world.addRigidBody(belt);
224 
225         belt = createBelt(20.5, 10.5, 10, 5, 0.5, -2);
226         world.addRigidBody(belt);
227 
228         belt = createBelt(10, 5, 5, 5, 0.5, -2);
229         world.addRigidBody(belt);
230 
231         // Create lift and pusher bodies.
232         lift = phys2D.createRigidBody({
233             shapes: [
234                 phys2D.createPolygonShape({
235                     vertices: phys2D.createBoxVertices(9, 0.01)
236                 })
237             ],
238             type: 'kinematic',
239             position: [stageWidth - 4.5, stageHeight]
240         });
241         pusher = phys2D.createRigidBody({
242             shapes: [
243                 phys2D.createPolygonShape({
244                     vertices: phys2D.createBoxVertices(9, 10)
245                 })
246             ],
247             type: 'kinematic',
248             position: [stageWidth + 4.5, 5]
249         });
250         world.addRigidBody(lift);
251         world.addRigidBody(pusher);
252         animationState = 0;
253 
254         // Create piles of each factory shape.
255         var x, y;
256         var xCount = Math.floor(stageWidth / shapeSize);
257         for (x = 0; x < xCount; x += 1) {
258             for (y = 0; y < 4; y += 1) {
259                 var index = (y % shapeFactory.length);
260                 var shape = shapeFactory[index];
261                 var body = phys2D.createRigidBody({
262                     shapes: [shape.clone()],
263                     position: [
264                         (x + 0.5) * (stageWidth / xCount),
265                         (y + 0.5) * shapeSize
266                     ],
267                     userData: Draw2DSprite.create({
268                         width: shapeSize,
269                         height: shapeSize,
270                         origin: [shapeSize / 2, shapeSize / 2],
271                         textureRectangle: textureRectangles[index],
272                         texture: draw2DTexture
273                     })
274                 });
275                 world.addRigidBody(body);
276             }
277         }
278     }
279 
280     //==========================================================================
281     // Mouse/Keyboard controls
282     //==========================================================================
283     var inputDevice = TurbulenzEngine.createInputDevice({});
284     var keyCodes = inputDevice.keyCodes;
285     var mouseCodes = inputDevice.mouseCodes;
286 
287     var mouseX = 0;
288     var mouseY = 0;
289     var onMouseOver = function mouseOverFn(x, y) {
290         mouseX = x;
291         mouseY = y;
292     };
293     inputDevice.addEventListener('mouseover', onMouseOver);
294 
295     var onKeyUp = function onKeyUpFn(keynum) {
296         if (keynum === keyCodes.R) {
297             reset();
298         }
299     };
300     inputDevice.addEventListener('keyup', onKeyUp);
301 
302     var onMouseDown = function onMouseDownFn(code, x, y) {
303         mouseX = x;
304         mouseY = y;
305 
306         if (handConstraint) {
307             return;
308         }
309 
310         var point = draw2D.viewportMap(x, y);
311         var body;
312         if (code === mouseCodes.BUTTON_0) {
313             var bodies = [];
314             var numBodies = world.bodyPointQuery(point, bodies);
315             var i;
316             for (i = 0; i < numBodies; i += 1) {
317                 body = bodies[i];
318                 if (body.isDynamic()) {
319                     handConstraint = phys2D.createPointConstraint({
320                         bodyA: handReferenceBody,
321                         bodyB: body,
322                         anchorA: point,
323                         anchorB: body.transformWorldPointToLocal(point),
324                         stiff: false,
325                         maxForce: 1e5
326                     });
327                     world.addConstraint(handConstraint);
328                     break;
329                 }
330             }
331         } else if (code === mouseCodes.BUTTON_1) {
332             var index = Math.floor(Math.random() * shapeFactory.length);
333             body = phys2D.createRigidBody({
334                 shapes: [shapeFactory[index].clone()],
335                 position: point,
336                 userData: Draw2DSprite.create({
337                     width: shapeSize,
338                     height: shapeSize,
339                     origin: [shapeSize / 2, shapeSize / 2],
340                     textureRectangle: textureRectangles[index],
341                     texture: draw2DTexture
342                 })
343             });
344             world.addRigidBody(body);
345         }
346     };
347     inputDevice.addEventListener('mousedown', onMouseDown);
348 
349     var onMouseLeaveUp = function onMouseLeaveUpFn() {
350         if (handConstraint) {
351             world.removeConstraint(handConstraint);
352             handConstraint = null;
353         }
354     };
355     inputDevice.addEventListener('mouseleave', onMouseLeaveUp);
356     inputDevice.addEventListener('mouseup', onMouseLeaveUp);
357 
358     //==========================================================================
359     // Main loop.
360     //==========================================================================
361     var fpsElement = document.getElementById("fpscounter");
362     var lastFPS = "";
363 
364     var bodiesElement = document.getElementById("bodiescounter");
365     var lastNumBodies = 0;
366 
367     var realTime = 0;
368     var prevTime = TurbulenzEngine.time;
369 
370     function mainLoop() {
371         if (!graphicsDevice.beginFrame()) {
372             return;
373         }
374 
375         inputDevice.update();
376         graphicsDevice.clear([0.3, 0.3, 0.3, 1.0]);
377 
378         var body;
379         if (handConstraint) {
380             body = handConstraint.bodyB;
381             handConstraint.setAnchorA(draw2D.viewportMap(mouseX, mouseY));
382 
383             // Additional angular dampening of body being dragged.
384             // Helps it to settle quicker instead of spinning around
385             // the cursor.
386             body.setAngularVelocity(body.getAngularVelocity() * 0.9);
387         }
388 
389         var curTime = TurbulenzEngine.time;
390         var timeDelta = (curTime - prevTime);
391 
392         if (timeDelta > (1 / 20)) {
393             timeDelta = (1 / 20);
394         }
395         realTime += timeDelta;
396         prevTime = curTime;
397 
398         while (world.simulatedTime < realTime) {
399             if (animationState === 0) {
400                 // Start of animatino, set velocity of lift to move up to the target
401                 // in 3 seconds.
402                 lift.setVelocityFromPosition([stageWidth - 4.5, 10], 0, 3);
403                 animationState = 1;
404             } else if (animationState === 1) {
405                 if (lift.getPosition()[1] <= 10) {
406                     // Reached target position for lift.
407                     // Set position incase it over-reached and zero velocity.
408                     lift.setPosition([stageWidth - 4.5, 10]);
409                     lift.setVelocity([0, 0]);
410 
411                     // Start pusher animation to move left.
412                     pusher.setVelocityFromPosition([stageWidth - 4.5, 5], 0, 1.5);
413                     animationState = 2;
414                 }
415             } else if (animationState === 2) {
416                 if (pusher.getPosition()[0] <= (stageWidth - 4.5)) {
417                     // Reached target position for pusher.
418                     // Set velocities of pusher and lift to both move right off-screen.
419                     pusher.setVelocityFromPosition([stageWidth + 4.5, 5], 0, 1);
420                     lift.setVelocityFromPosition([stageWidth + 4.5, 10], 0, 1);
421                     animationState = 3;
422                 }
423             } else if (animationState === 3) {
424                 if (pusher.getPosition()[0] >= stageWidth + 4.5) {
425                     // Reached target.
426                     // Reset positions and velocities and begin animation afresh.
427                     pusher.setPosition([stageWidth + 4.5, 5]);
428                     pusher.setVelocity([0, 0]);
429                     lift.setPosition([stageWidth - 4.5, stageHeight]);
430                     lift.setVelocity([0, 0]);
431                     animationState = 0;
432                 }
433             }
434 
435             world.step(1 / 60);
436         }
437 
438         // draw2D sprite drawing.
439         var bodies = world.rigidBodies;
440         var limit = bodies.length;
441         var i;
442         if (!debugEnabled) {
443             draw2D.begin('alpha', 'deferred');
444             var pos = [];
445             for (i = 0; i < limit; i += 1) {
446                 body = bodies[i];
447                 if (body.userData) {
448                     body.getPosition(pos);
449                     var sprite = body.userData;
450                     sprite.x = pos[0];
451                     sprite.y = pos[1];
452                     sprite.rotation = body.getRotation();
453                     draw2D.drawSprite(sprite);
454                 }
455             }
456             draw2D.end();
457         }
458 
459         // physics2D debug drawing.
460         debug.setScreenViewport(draw2D.getScreenSpaceViewport());
461         debug.showRigidBodies = debugEnabled;
462         debug.showContacts = contactsEnabled;
463 
464         debug.begin();
465         if (!debugEnabled) {
466             for (i = 0; i < limit; i += 1) {
467                 body = bodies[i];
468                 if (!body.userData) {
469                     debug.drawRigidBody(body);
470                 }
471             }
472         }
473         debug.drawWorld(world);
474         debug.end();
475 
476         graphicsDevice.endFrame();
477 
478         if (fpsElement) {
479             var fpsText = (graphicsDevice.fps).toFixed(2);
480             if (lastFPS !== fpsText) {
481                 lastFPS = fpsText;
482 
483                 fpsElement.innerHTML = fpsText + " fps";
484             }
485         }
486 
487         if (bodiesElement) {
488             if (lastNumBodies !== limit) {
489                 lastNumBodies = limit;
490 
491                 bodiesElement.innerHTML = lastNumBodies + "";
492             }
493         }
494     }
495 
496     var intervalID;
497     function loadingLoop() {
498         if (!draw2DTexture) {
499             return;
500         }
501 
502         reset();
503         TurbulenzEngine.clearInterval(intervalID);
504         intervalID = TurbulenzEngine.setInterval(mainLoop, 1000 / 60);
505     }
506     intervalID = TurbulenzEngine.setInterval(loadingLoop, 10);
507 
508     //==========================================================================
509     function loadHtmlControls() {
510         htmlControls = HTMLControls.create();
511         htmlControls.addCheckboxControl({
512             id: "enableDebug",
513             value: "debugEnabled",
514             isSelected: debugEnabled,
515             fn: function () {
516                 debugEnabled = !debugEnabled;
517                 return debugEnabled;
518             }
519         });
520         htmlControls.addCheckboxControl({
521             id: "enableContacts",
522             value: "contactsEnabled",
523             isSelected: contactsEnabled,
524             fn: function () {
525                 contactsEnabled = !contactsEnabled;
526                 return contactsEnabled;
527             }
528         });
529         htmlControls.register();
530     }
531 
532     loadHtmlControls();
533 
534     // Create a scene destroy callback to run when the window is closed
535     TurbulenzEngine.onunload = function destroyScene() {
536         if (intervalID) {
537             TurbulenzEngine.clearInterval(intervalID);
538         }
539 
540         if (gameSession) {
541             gameSession.destroy();
542             gameSession = null;
543         }
544     };
545 };