physics2d_constraints.js
  1 /*{# Copyright (c) 2012 Turbulenz Limited #}*/
  2 /*
  3 * @title: 2D Physics constraints
  4 * @description:
  5 * This sample shows how to create each of the 2D physics constraints
  6 * (point, weld, distance, line, angle, motor, pulley and custom).
  7 * Each object in the scene can be manipulated with the mouse to see how the constraints work.
  8 */
  9 /*{{ javascript("jslib/observer.js") }}*/
 10 /*{{ javascript("jslib/requesthandler.js") }}*/
 11 /*{{ javascript("jslib/utilities.js") }}*/
 12 /*{{ javascript("jslib/services/turbulenzservices.js") }}*/
 13 /*{{ javascript("jslib/services/turbulenzbridge.js") }}*/
 14 /*{{ javascript("jslib/services/gamesession.js") }}*/
 15 /*{{ javascript("jslib/services/mappingtable.js") }}*/
 16 /*{{ javascript("jslib/shadermanager.js") }}*/
 17 /*{{ javascript("jslib/physics2ddevice.js") }}*/
 18 /*{{ javascript("jslib/draw2d.js") }}*/
 19 /*{{ javascript("jslib/boxtree.js") }}*/
 20 /*{{ javascript("jslib/physics2ddebugdraw.js") }}*/
 21 /*{{ javascript("jslib/fontmanager.js") }}*/
 22 /*{{ javascript("scripts/htmlcontrols.js") }}*/
 23 /*global TurbulenzEngine: true */
 24 /*global TurbulenzServices: false */
 25 /*global RequestHandler: false */
 26 /*global Physics2DDevice: false */
 27 /*global Draw2D: false */
 28 /*global FontManager: false */
 29 /*global ShaderManager: false */
 30 /*global Physics2DDebugDraw: false */
 31 /*global HTMLControls: false */
 32 TurbulenzEngine.onload = function onloadFn() {
 33     //==========================================================================
 34     // HTML Controls
 35     //==========================================================================
 36     var htmlControls;
 37 
 38     var elasticConstraints = false;
 39     var frequency = 1;
 40     var damping = 0.1;
 41 
 42     //==========================================================================
 43     // Turbulenz Initialization
 44     //==========================================================================
 45     var graphicsDevice = TurbulenzEngine.createGraphicsDevice({});
 46     var mathDevice = TurbulenzEngine.createMathDevice({});
 47     var requestHandler = RequestHandler.create({});
 48 
 49     var fontManager = FontManager.create(graphicsDevice, requestHandler);
 50     var shaderManager = ShaderManager.create(graphicsDevice, requestHandler);
 51 
 52     var font, shader, gameSession;
 53     function sessionCreated() {
 54         TurbulenzServices.createMappingTable(requestHandler, gameSession, function (mappingTable) {
 55             var urlMapping = mappingTable.urlMapping;
 56             var assetPrefix = mappingTable.assetPrefix;
 57             shaderManager.setPathRemapping(urlMapping, assetPrefix);
 58             fontManager.setPathRemapping(urlMapping, assetPrefix);
 59             fontManager.load('fonts/hero.fnt', function (fontObject) {
 60                 font = fontObject;
 61             });
 62             shaderManager.load('shaders/font.cgfx', function (shaderObject) {
 63                 shader = shaderObject;
 64             });
 65         });
 66     }
 67     gameSession = TurbulenzServices.createGameSession(requestHandler, sessionCreated);
 68 
 69     //==========================================================================
 70     // Physics2D/Draw2D (Use Draw2D to define viewport scalings)
 71     //==========================================================================
 72     // set up.
 73     var phys2D = Physics2DDevice.create();
 74 
 75     // size of physics stage.
 76     var stageWidth = 40;
 77     var stageHeight = 20;
 78 
 79     var draw2D = Draw2D.create({
 80         graphicsDevice: graphicsDevice
 81     });
 82     var debug = Physics2DDebugDraw.create({
 83         graphicsDevice: graphicsDevice
 84     });
 85 
 86     // Configure draw2D viewport to the physics stage.
 87     // As well as the physics2D debug-draw viewport.
 88     draw2D.configure({
 89         viewportRectangle: [0, 0, stageWidth, stageHeight],
 90         scaleMode: 'scale'
 91     });
 92     debug.setPhysics2DViewport([0, 0, stageWidth, stageHeight]);
 93 
 94     var world = phys2D.createWorld({
 95         gravity: [0, 20]
 96     });
 97 
 98     // Create a static body at (0, 0) with no rotation
 99     // which we add to the world to use as the first body
100     // in hand constraint. We set anchor for this body
101     // as the cursor position in physics coordinates.
102     var staticReferenceBody = phys2D.createRigidBody({
103         type: 'static'
104     });
105     world.addRigidBody(staticReferenceBody);
106     var handConstraint = null;
107 
108     function reset() {
109         // Remove all bodies and constraints from world.
110         world.clear();
111         handConstraint = null;
112 
113         // Create a static body around the stage to stop objects leaving the viewport.
114         // And walls between each constraint section.
115         var border = phys2D.createRigidBody({
116             type: 'static'
117         });
118 
119         var thickness = 0.01;
120         var i;
121         for (i = 0; i <= 4; i += 1) {
122             var x = (stageWidth / 4) * i;
123             border.addShape(phys2D.createPolygonShape({
124                 vertices: phys2D.createRectangleVertices(x - thickness, 0, x + thickness, stageHeight)
125             }));
126         }
127         for (i = 0; i <= 2; i += 1) {
128             var y = (stageHeight / 2) * i;
129             border.addShape(phys2D.createPolygonShape({
130                 vertices: phys2D.createRectangleVertices(0, y - thickness, stageWidth, y + thickness)
131             }));
132         }
133 
134         world.addRigidBody(border);
135 
136         function circle(x, y, radius, pinned) {
137             var body = phys2D.createRigidBody({
138                 shapes: [
139                     phys2D.createCircleShape({
140                         radius: radius
141                     })
142                 ],
143                 position: [x, y]
144             });
145             world.addRigidBody(body);
146 
147             if (pinned) {
148                 var pin = phys2D.createPointConstraint({
149                     bodyA: staticReferenceBody,
150                     bodyB: body,
151                     anchorA: [x, y],
152                     anchorB: [0, 0],
153                     userData: "pin"
154                 });
155                 world.addConstraint(pin);
156             }
157 
158             return body;
159         }
160 
161         var bodyA, bodyB, worldAnchor;
162 
163         // ------------------------------------
164         // Point Constraint
165         bodyA = circle(3.3, 5, 1);
166         bodyB = circle(6.6, 5, 1);
167 
168         worldAnchor = [5, 5];
169         var pointConstraint = phys2D.createPointConstraint({
170             bodyA: bodyA,
171             bodyB: bodyB,
172             anchorA: bodyA.transformWorldPointToLocal(worldAnchor),
173             anchorB: bodyB.transformWorldPointToLocal(worldAnchor),
174             stiff: (!elasticConstraints),
175             frequency: frequency,
176             damping: damping
177         });
178         world.addConstraint(pointConstraint);
179 
180         // ------------------------------------
181         // Weld Constraint
182         bodyA = circle(13.3, 5, 1);
183         bodyB = circle(16.6, 5, 1);
184 
185         worldAnchor = [15, 5];
186         var weldConstraint = phys2D.createWeldConstraint({
187             bodyA: bodyA,
188             bodyB: bodyB,
189             anchorA: bodyA.transformWorldPointToLocal(worldAnchor),
190             anchorB: bodyB.transformWorldPointToLocal(worldAnchor),
191             phase: 0,
192             stiff: (!elasticConstraints),
193             frequency: frequency,
194             damping: damping
195         });
196         world.addConstraint(weldConstraint);
197 
198         // ------------------------------------
199         // Distance Constraint
200         bodyA = circle(23.3, 5, 1);
201         bodyB = circle(26.6, 5, 1);
202 
203         var distanceConstraint = phys2D.createDistanceConstraint({
204             bodyA: bodyA,
205             bodyB: bodyB,
206             anchorA: [1, 0],
207             anchorB: [-1, 0],
208             lowerBound: 1,
209             upperBound: 3,
210             stiff: (!elasticConstraints),
211             frequency: frequency,
212             damping: damping
213         });
214         world.addConstraint(distanceConstraint);
215 
216         // ------------------------------------
217         // Line Constraint
218         bodyA = circle(33.3, 5, 1);
219         bodyB = circle(36.6, 5, 1);
220 
221         worldAnchor = [35, 5];
222         var lineConstraint = phys2D.createLineConstraint({
223             bodyA: bodyA,
224             bodyB: bodyB,
225             anchorA: bodyA.transformWorldPointToLocal(worldAnchor),
226             anchorB: bodyB.transformWorldPointToLocal(worldAnchor),
227             axis: [0, 1],
228             lowerBound: -1,
229             upperBound: 1,
230             stiff: (!elasticConstraints),
231             frequency: frequency,
232             damping: damping
233         });
234         world.addConstraint(lineConstraint);
235 
236         // ------------------------------------
237         // Angle Constraint
238         bodyA = circle(3, 15, 1.5, true);
239         bodyB = circle(7, 15, 1.5, true);
240 
241         var angleConstraint = phys2D.createAngleConstraint({
242             bodyA: bodyA,
243             bodyB: bodyB,
244             ratio: 3,
245             lowerBound: -Math.PI * 2,
246             upperBound: Math.PI * 2,
247             stiff: (!elasticConstraints),
248             frequency: frequency,
249             damping: damping
250         });
251         world.addConstraint(angleConstraint);
252 
253         // ------------------------------------
254         // Motor Constraint
255         bodyA = circle(13, 15, 1.5, true);
256         bodyB = circle(17, 15, 1.5, true);
257 
258         var motorConstraint = phys2D.createMotorConstraint({
259             bodyA: bodyA,
260             bodyB: bodyB,
261             ratio: 4,
262             rate: 20
263         });
264         world.addConstraint(motorConstraint);
265 
266         // ------------------------------------
267         // Pulley Constraint
268         var bodyC;
269         bodyA = circle(23.3, 16.6, 0.5);
270         bodyB = circle(25, 13.3, 1, true);
271         bodyC = circle(26.6, 16.6, 0.5);
272 
273         // Additional distance constraints to prevent pulley
274         // becoming degenerate when one side becomes 0 length.
275         var distanceA = phys2D.createDistanceConstraint({
276             bodyA: bodyA,
277             bodyB: bodyB,
278             lowerBound: 0.25,
279             upperBound: Number.POSITIVE_INFINITY,
280             anchorA: [0, -0.5],
281             anchorB: [-1, 0],
282             userData: 'pin'
283         });
284         world.addConstraint(distanceA);
285 
286         var distanceB = phys2D.createDistanceConstraint({
287             bodyA: bodyC,
288             bodyB: bodyB,
289             lowerBound: 0.25,
290             upperBound: Number.POSITIVE_INFINITY,
291             anchorA: [0, -0.5],
292             anchorB: [1, 0],
293             userData: 'pin'
294         });
295         world.addConstraint(distanceB);
296 
297         var pulleyConstraint = phys2D.createPulleyConstraint({
298             bodyA: bodyA,
299             bodyB: bodyB,
300             bodyC: bodyB,
301             bodyD: bodyC,
302             anchorA: [0, -0.5],
303             anchorB: [-1, 0],
304             anchorC: [1, 0],
305             anchorD: [0, -0.5],
306             ratio: 2,
307             lowerBound: 6,
308             upperBound: 8,
309             stiff: (!elasticConstraints),
310             frequency: frequency,
311             damping: damping
312         });
313         world.addConstraint(pulleyConstraint);
314 
315         // ------------------------------------
316         // Custom Constraint
317         bodyA = circle(35, 13.3, 1);
318         bodyB = circle(35, 16.6, 1, true);
319 
320         // Additional line constraint to pin upper body to rack.
321         var line = phys2D.createLineConstraint({
322             bodyA: staticReferenceBody,
323             bodyB: bodyA,
324             anchorA: [35, 13.3],
325             anchorB: [0, 0],
326             axis: [1, 0],
327             lowerBound: -5,
328             upperBound: 5,
329             userData: 'pin'
330         });
331         world.addConstraint(line);
332 
333         // Custom constraint defined so that the x-position of
334         // the first body, is equal to the rotation of the
335         // second body.
336         //
337         // Constraint equation:
338         //    (pi / 5) * (bodyA.posX - 35) - bodyB.rotation = 0
339         //
340         // Time Derivative (Velocity constraint):
341         //    (pi / 5) * bodyA.velX - bodyB.angularVel = 0
342         //
343         // Partial derivatives of velocity constraint (Jacobian)
344         //        velAx   velAy  angVelA  velBx  velBy  angVelB
345         //    [ (pi / 5),   0,      0,      0,     0,     -1    ]
346         //
347         var user = phys2D.createCustomConstraint({
348             bodies: [bodyA, bodyB],
349             dimension: 1,
350             position: function positionFn(data, index) {
351                 var bodyA = this.bodies[0];
352                 var bodyB = this.bodies[1];
353                 data[index] = (Math.PI / 5 * (bodyA.getPosition()[0] - 35)) - bodyB.getRotation();
354             },
355             jacobian: function jacobianFn(data, index) {
356                 data[index] = (Math.PI / 5);
357                 data[index + 1] = 0;
358                 data[index + 2] = 0;
359 
360                 data[index + 3] = 0;
361                 data[index + 4] = 0;
362                 data[index + 5] = -1;
363             },
364             debugDraw: function debugDrawFn(debug, stiff) {
365                 if (stiff) {
366                     return;
367                 }
368 
369                 var bodyA = this.bodies[0];
370                 var bodyB = this.bodies[1];
371 
372                 var posA = bodyA.getPosition();
373                 var posB = bodyB.getPosition();
374 
375                 // target for x-position of bodyA
376                 var targetX = ((bodyB.getRotation()) / (Math.PI / 5)) + 35;
377 
378                 // target for rotation of bodyB
379                 var targetR = (Math.PI / 5 * (posA[0] - 35));
380 
381                 // 3 pixel spring radius
382                 var radius = 3 * debug.screenToPhysics2D;
383                 debug.drawLinearSpring(posA[0], posA[1], targetX, posA[1], 3, radius, [1, 0, 0, 1]);
384                 debug.drawSpiralSpring(posB[0], posB[1], targetR, bodyB.getRotation(), radius, radius * 2, [0, 0, 1, 1]);
385             },
386             stiff: (!elasticConstraints),
387             frequency: frequency,
388             damping: damping
389         });
390         world.addConstraint(user);
391     }
392     reset();
393 
394     function invalidateConstraints() {
395         var constraints = world.constraints;
396         var limit = constraints.length;
397         var i;
398         for (i = 0; i < limit; i += 1) {
399             var con = constraints[i];
400 
401             if (con === handConstraint || con.userData === "pin") {
402                 continue;
403             }
404 
405             con.configure({
406                 stiff: (!elasticConstraints),
407                 frequency: frequency,
408                 damping: damping
409             });
410         }
411     }
412 
413     //==========================================================================
414     // Mouse/Keyboard controls
415     //==========================================================================
416     var inputDevice = TurbulenzEngine.createInputDevice({});
417     var keyCodes = inputDevice.keyCodes;
418     var mouseCodes = inputDevice.mouseCodes;
419 
420     var mouseX = 0;
421     var mouseY = 0;
422     var onMouseOver = function mouseOverFn(x, y) {
423         mouseX = x;
424         mouseY = y;
425     };
426     inputDevice.addEventListener('mouseover', onMouseOver);
427 
428     var onKeyUp = function onKeyUpFn(keynum) {
429         if (keynum === keyCodes.R) {
430             reset();
431         }
432     };
433     inputDevice.addEventListener('keyup', onKeyUp);
434 
435     var onMouseDown = function onMouseDownFn(code, x, y) {
436         mouseX = x;
437         mouseY = y;
438 
439         if (handConstraint) {
440             return;
441         }
442 
443         var point = draw2D.viewportMap(x, y);
444         var body;
445         if (code === mouseCodes.BUTTON_0) {
446             var bodies = [];
447             var numBodies = world.bodyPointQuery(point, bodies);
448             var i;
449             for (i = 0; i < numBodies; i += 1) {
450                 body = bodies[i];
451                 if (body.isDynamic()) {
452                     handConstraint = phys2D.createPointConstraint({
453                         bodyA: staticReferenceBody,
454                         bodyB: body,
455                         anchorA: point,
456                         anchorB: body.transformWorldPointToLocal(point),
457                         stiff: false,
458                         maxForce: 1e5
459                     });
460                     world.addConstraint(handConstraint);
461                 }
462             }
463         }
464     };
465     inputDevice.addEventListener('mousedown', onMouseDown);
466 
467     var onMouseLeaveUp = function onMouseLeaveUpFn() {
468         if (handConstraint) {
469             world.removeConstraint(handConstraint);
470             handConstraint = null;
471         }
472     };
473     inputDevice.addEventListener('mouseleave', onMouseLeaveUp);
474     inputDevice.addEventListener('mouseup', onMouseLeaveUp);
475 
476     //==========================================================================
477     // Main loop.
478     //==========================================================================
479     var realTime = 0;
480     var prevTime = TurbulenzEngine.time;
481 
482     var fontTechnique, fontTechniqueParameters;
483     function mainLoop() {
484         if (!graphicsDevice.beginFrame()) {
485             return;
486         }
487 
488         inputDevice.update();
489         graphicsDevice.clear([0.3, 0.3, 0.3, 1.0]);
490 
491         if (handConstraint) {
492             handConstraint.setAnchorA(draw2D.viewportMap(mouseX, mouseY));
493         }
494 
495         var curTime = TurbulenzEngine.time;
496         var timeDelta = (curTime - prevTime);
497 
498         if (timeDelta > (1 / 20)) {
499             timeDelta = (1 / 20);
500         }
501         realTime += timeDelta;
502         prevTime = curTime;
503 
504         while (world.simulatedTime < realTime) {
505             world.step(1 / 60);
506         }
507 
508         // physics2D debug drawing.
509         debug.setScreenViewport(draw2D.getScreenSpaceViewport());
510 
511         debug.begin();
512         debug.drawWorld(world);
513         debug.end();
514 
515         // Draw fonts.
516         graphicsDevice.setTechnique(fontTechnique);
517         fontTechniqueParameters.clipSpace = mathDevice.v4Build(2 / graphicsDevice.width, -2 / graphicsDevice.height, -1, 1, fontTechniqueParameters.clipSpace);
518         graphicsDevice.setTechniqueParameters(fontTechniqueParameters);
519 
520         function segmentFont(x, y, text, height) {
521             var topLeft = draw2D.viewportUnmap(x, y);
522             var bottomRight = draw2D.viewportUnmap(x + 10, y + height);
523             font.drawTextRect(text, {
524                 rect: [topLeft[0], topLeft[1], bottomRight[0] - topLeft[0], bottomRight[1] - topLeft[1]],
525                 scale: 1.0,
526                 spacing: 0,
527                 alignment: 1
528             });
529         }
530 
531         var titleHeight = 0.75;
532         segmentFont(0, 0, "Point", titleHeight);
533         segmentFont(10, 0, "Weld", titleHeight);
534         segmentFont(20, 0, "Distance", titleHeight);
535         segmentFont(30, 0, "Line", titleHeight);
536         segmentFont(0, 10, "Angle", titleHeight);
537         segmentFont(10, 10, "Motor", titleHeight);
538         segmentFont(20, 10, "Pulley", titleHeight);
539         segmentFont(30, 10, "Custom", titleHeight);
540 
541         graphicsDevice.endFrame();
542     }
543 
544     var intervalID = 0;
545     function loadingLoop() {
546         if (font && shader) {
547             fontTechnique = shader.getTechnique('font');
548             fontTechniqueParameters = graphicsDevice.createTechniqueParameters({
549                 clipSpace: mathDevice.v4BuildZero(),
550                 alphaRef: 0.01,
551                 color: mathDevice.v4BuildOne()
552             });
553 
554             TurbulenzEngine.clearInterval(intervalID);
555             intervalID = TurbulenzEngine.setInterval(mainLoop, 1000 / 60);
556         }
557     }
558     intervalID = TurbulenzEngine.setInterval(loadingLoop, 100);
559 
560     //==========================================================================
561     function loadHtmlControls() {
562         htmlControls = HTMLControls.create();
563         htmlControls.addCheckboxControl({
564             id: "elasticConstraints",
565             value: "elasticConstraints",
566             isSelected: elasticConstraints,
567             fn: function () {
568                 elasticConstraints = !elasticConstraints;
569                 invalidateConstraints();
570                 return elasticConstraints;
571             }
572         });
573         htmlControls.addSliderControl({
574             id: "frequencySlider",
575             value: (frequency),
576             max: 10,
577             min: 0.25,
578             step: 0.25,
579             fn: function () {
580                 frequency = this.value;
581                 htmlControls.updateSlider("frequencySlider", frequency);
582                 if (elasticConstraints) {
583                     invalidateConstraints();
584                 }
585             }
586         });
587         htmlControls.addSliderControl({
588             id: "dampingSlider",
589             value: (damping),
590             max: 2,
591             min: 0,
592             step: 0.25,
593             fn: function () {
594                 damping = this.value;
595                 htmlControls.updateSlider("dampingSlider", damping);
596                 if (elasticConstraints) {
597                     invalidateConstraints();
598                 }
599             }
600         });
601         htmlControls.register();
602     }
603 
604     loadHtmlControls();
605 
606     // Create a scene destroy callback to run when the window is closed
607     TurbulenzEngine.onunload = function destroyScene() {
608         if (intervalID) {
609             TurbulenzEngine.clearInterval(intervalID);
610         }
611 
612         if (gameSession) {
613             gameSession.destroy();
614             gameSession = null;
615         }
616     };
617 };