As we were getting closer to Christmas, I thought it could be useful to write a little demo in order to simulate distribution of Santa’s presents along a conveyor :) … More seriously, I was looking for a good JavaScript physics library so I could integrate it with the Autodesk Viewer.
I stumbled across one post in particular: JavaScript Physics Engines Comparison by Chandler Prall, and I have to say, reused some of the big lines along which the code was written to create the sample below.
From the four libraries presented in that review, two were 2d-only, so as I was looking for a fully 3d physic simulation, those were ruled out straight from the start. I gave a quick try at Cannon.js to realize that performances and capabilities were not yet satisfying enough, and finally focused on Ammo.js which is an emscripten port of Bullet, a well-established C++ Physics library used in many commercial games already.
The sample is producing some pseudo-randomized three.js default shapes dropped above a conveyor, gravity is the only force applied to the solids and the physics is doing the rest…
Controls:
. P + Mouse: Pan
. R + Mouse: Rotate
. Z + Mouse: Zoom
1 2 /////////////////////////////////////////////////////////////// 3 // Ammo.js sample, by Philippe Leefsma 4 // December 2014 5 // 6 /////////////////////////////////////////////////////////////// 7 var Autodesk = Autodesk || {}; 8 Autodesk.ADN = Autodesk.ADN || {}; 9 10 /////////////////////////////////////////////////////////////// 11 // A stopwatch! 12 // 13 /////////////////////////////////////////////////////////////// 14 Autodesk.ADN.Stopwatch = function() { 15 16 var _startTime = new Date().getTime(); 17 18 this.start = function (){ 19 20 _startTime = new Date().getTime(); 21 }; 22 23 this.getElapsedMs = function(){ 24 25 var elapsedMs = new Date().getTime() - _startTime; 26 27 _startTime = new Date().getTime(); 28 29 return elapsedMs; 30 } 31 } 32 33 /////////////////////////////////////////////////////////////// 34 // Simulation Manager 35 // 36 /////////////////////////////////////////////////////////////// 37 Autodesk.ADN.AdnSimulationManager = function (canvasId) { 38 39 var _world = null; 40 41 var _scene = null; 42 43 var _camera = null; 44 45 var _renderer = null; 46 47 var _trackball = null; 48 49 var _physicBodies = {}; 50 51 var _intervalId = null; 52 53 var _animationId = null; 54 55 var _stopWatch = new Autodesk.ADN.Stopwatch(); 56 57 /////////////////////////////////////////////////////////// 58 // Random number between (min, max) 59 // 60 /////////////////////////////////////////////////////////// 61 function _random(min, max) { 62 63 return min + (max - min) * Math.random(); 64 } 65 66 function _randomInt(min, max) { 67 68 return Math.floor(Math.random() * (max - min)) + min; 69 } 70 71 /////////////////////////////////////////////////////////// 72 // Unique GUID 73 // 74 /////////////////////////////////////////////////////////// 75 function _newGuid () { 76 77 var d = new Date().getTime(); 78 79 var guid = 'xxxx-xxxx-xxxx-xxxx'.replace( 80 /[xy]/g, 81 function (c) { 82 var r = (d + Math.random() * 16) % 16 | 0; 83 d = Math.floor(d / 16); 84 85 return (c == 'x' ? r : (r & 0x7 | 0x8)). 86 toString(16); 87 }); 88 89 return guid; 90 }; 91 92 /////////////////////////////////////////////////////////// 93 // Initializes Physics 94 // 95 /////////////////////////////////////////////////////////// 96 function _initializePhysics() { 97 98 var collisionConfiguration = 99 new Ammo.btDefaultCollisionConfiguration; 100 101 _world = new Ammo.btDiscreteDynamicsWorld( 102 new Ammo.btCollisionDispatcher( 103 collisionConfiguration), 104 new Ammo.btDbvtBroadphase, 105 new Ammo.btSequentialImpulseConstraintSolver, 106 collisionConfiguration); 107 108 _world.setGravity(new Ammo.btVector3(0, -9.8, 0)); 109 } 110 111 /////////////////////////////////////////////////////////// 112 // Creates collision shape from mesh 113 // 114 /////////////////////////////////////////////////////////// 115 function _createCollisionShape(mesh) { 116 117 var geometry = mesh.geometry; 118 119 var hull = new Ammo.btConvexHullShape(); 120 121 geometry.vertices.forEach(function(vertex){ 122 123 hull.addPoint(new Ammo.btVector3( 124 vertex.x, 125 vertex.y, 126 vertex.z)); 127 }); 128 129 return hull; 130 } 131 132 /////////////////////////////////////////////////////////// 133 // Creates physic rigid body from mesh 134 // 135 /////////////////////////////////////////////////////////// 136 function _addRigidBody(mesh, mass) { 137 138 var localInertia = new Ammo.btVector3(0, 0, 0); 139 140 var shape = _createCollisionShape(mesh); 141 142 shape.calculateLocalInertia(mass, localInertia); 143 144 var transform = new Ammo.btTransform; 145 146 transform.setIdentity(); 147 148 transform.setOrigin(new Ammo.btVector3( 149 mesh.position.x, 150 mesh.position.y, 151 mesh.position.z)); 152 153 transform.setRotation(new Ammo.btQuaternion( 154 mesh.quaternion.x, 155 mesh.quaternion.y, 156 mesh.quaternion.z, 157 mesh.quaternion.w 158 )); 159 160 var motionState = 161 new Ammo.btDefaultMotionState(transform); 162 163 var rbInfo = new Ammo.btRigidBodyConstructionInfo( 164 mass, 165 motionState, 166 shape, 167 localInertia); 168 169 var body = new Ammo.btRigidBody(rbInfo); 170 171 body.mesh = mesh; 172 173 _world.addRigidBody(body); 174 175 _physicBodies[_newGuid()] = body; 176 } 177 178 /////////////////////////////////////////////////////////// 179 // Updates mesh transform from physic body 180 // 181 /////////////////////////////////////////////////////////// 182 function _updateMeshTransform(body) { 183 184 var mesh = body.mesh; 185 186 var transform = body.getCenterOfMassTransform(); 187 188 var origin = transform.getOrigin(); 189 190 mesh.position.set( 191 origin.x(), 192 origin.y(), 193 origin.z()); 194 195 var rotation = transform.getRotation(); 196 197 mesh.quaternion.set( 198 rotation.x(), 199 rotation.y(), 200 rotation.z(), 201 rotation.w()); 202 } 203 204 /////////////////////////////////////////////////////////// 205 // Creates ramp mesh 206 // 207 /////////////////////////////////////////////////////////// 208 function _createRamp(position, rotations) { 209 210 var material = new THREE.MeshLambertMaterial({ 211 color: 0xF74F4F 212 }) 213 214 var mesh = new THREE.Mesh( 215 new THREE.BoxGeometry(50, 2, 10), 216 material); 217 218 mesh.position.copy(position); 219 220 rotations.forEach(function(rotation){ 221 222 var q = new THREE.Quaternion(); 223 224 q.setFromAxisAngle( 225 rotation.axis, 226 rotation.angle); 227 228 mesh.quaternion.multiply(q); 229 }); 230 231 return mesh; 232 } 233 234 /////////////////////////////////////////////////////////// 235 // Initializes three.js scene 236 // 237 /////////////////////////////////////////////////////////// 238 function _initializeScene(id) { 239 240 var viewport = document.getElementById(id) 241 242 _renderer = 243 new THREE.WebGLRenderer({canvas: viewport}) 244 245 _renderer.setSize( 246 viewport.clientWidth, 247 viewport.clientHeight); 248 249 _scene = new THREE.Scene 250 _camera = new THREE.PerspectiveCamera( 251 35, 1, 1, 1000) 252 253 _camera.position.set(-50, 90, -150); 254 _camera.lookAt(_scene.position); 255 256 _scene.add(_camera); 257 258 _trackball = new THREE.TrackballControls( 259 _camera, viewport); 260 261 _trackball.noPan = false; 262 _trackball.panSpeed = 0.5; 263 _trackball.noZoom = false; 264 _trackball.zoomSpeed = 2.0; 265 _trackball.minDistance = 1; 266 _trackball.maxDistance = 300; 267 _trackball.rotateSpeed = 3.5; 268 _trackball.staticMoving = true; 269 _trackball.dynamicDampingFactor = 0.3; 270 271 // [r:rotate, z:zoom, p:pan] 272 _trackball.keys = [82, 90, 80]; 273 274 var ambientLight = new THREE.AmbientLight(0x555555); 275 _scene.add(ambientLight); 276 277 var directionalLight = 278 new THREE.DirectionalLight(0xffffff); 279 280 directionalLight.position.set( 281 -.5, .5, -1.5 ).normalize(); 282 283 _scene.add(directionalLight); 284 285 var material = new THREE.MeshLambertMaterial({ 286 color: 0xdd0000 287 }) 288 289 var ramp11 = _createRamp( 290 291 new THREE.Vector3(-20, 25, 0), 292 [{ 293 axis: new THREE.Vector3(0, 0, 1), 294 angle: - Math.PI / 10 295 }, { 296 axis: new THREE.Vector3(1, 0, 0), 297 angle: - Math.PI / 10 298 }]); 299 300 var ramp12 = _createRamp( 301 302 new THREE.Vector3(-20, 25, -10), 303 [{ 304 axis: new THREE.Vector3(0, 0, 1), 305 angle: - Math.PI / 10 306 }, { 307 axis: new THREE.Vector3(1, 0, 0), 308 angle: Math.PI / 10 309 }]); 310 311 312 var ramp21 = _createRamp( 313 314 new THREE.Vector3(25, 5, 0), 315 [{ 316 axis: new THREE.Vector3(0, 0, 1), 317 angle: Math.PI / 10 318 }, { 319 axis: new THREE.Vector3(1, 0, 0), 320 angle: - Math.PI / 10 321 }]); 322 323 var ramp22 = _createRamp( 324 325 new THREE.Vector3(25, 5, -10), 326 [{ 327 axis: new THREE.Vector3(0, 0, 1), 328 angle: Math.PI / 10 329 }, { 330 axis: new THREE.Vector3(1, 0, 0), 331 angle: Math.PI / 10 332 }]); 333 334 335 var ramp31 = _createRamp( 336 337 new THREE.Vector3(-20, -10, 0), 338 [{ 339 axis: new THREE.Vector3(0, 0, 1), 340 angle: -Math.PI / 10 341 }, { 342 axis: new THREE.Vector3(1, 0, 0), 343 angle: - Math.PI / 10 344 }]); 345 346 var ramp32 = _createRamp( 347 348 new THREE.Vector3(-20, -10, -10), 349 [{ 350 axis: new THREE.Vector3(0, 0, 1), 351 angle: -Math.PI / 10 352 }, { 353 axis: new THREE.Vector3(1, 0, 0), 354 angle: Math.PI / 10 355 }]); 356 357 _scene.add(ramp11); 358 _scene.add(ramp12); 359 360 _scene.add(ramp21); 361 _scene.add(ramp22); 362 363 _scene.add(ramp31); 364 _scene.add(ramp32); 365 366 _addRigidBody(ramp11, 0); 367 _addRigidBody(ramp12, 0); 368 369 _addRigidBody(ramp21, 0); 370 _addRigidBody(ramp22, 0); 371 372 _addRigidBody(ramp31, 0); 373 _addRigidBody(ramp32, 0); 374 } 375 376 /////////////////////////////////////////////////////////// 377 // Creates random geometry 378 // 379 /////////////////////////////////////////////////////////// 380 function _createRandomGeometry(size) { 381 382 switch(_randomInt(1, 9)) { 383 384 case 1: 385 return THREE.BoxGeometry( 386 size, size, size); 387 388 case 2: 389 return THREE.SphereGeometry( 390 size, 32, 32); 391 392 case 3: 393 return new THREE.IcosahedronGeometry( 394 size, 0); 395 396 case 4: 397 return new THREE.OctahedronGeometry( 398 size, 0); 399 400 case 5: 401 return new THREE.TetrahedronGeometry( 402 size, 0); 403 404 case 6: 405 return new THREE.CylinderGeometry( 406 0, size, size, 20, 4); 407 408 case 7: 409 return new THREE.CylinderGeometry( 410 size, size, size, 20, 4); 411 412 case 8: 413 return new THREE.CylinderGeometry( 414 size * 0.5, size, size, 20, 4); 415 416 case 9: 417 return new THREE.CylinderGeometry( 418 size * 0.5, size * 0.5, size, 20, 4); 419 420 default: 421 return null; 422 } 423 } 424 425 /////////////////////////////////////////////////////////// 426 // Add new elements to the scene 427 // 428 /////////////////////////////////////////////////////////// 429 function _newElements() { 430 431 function newElement() { 432 433 var size = _random(1, 4); 434 435 var mass = size; 436 437 var color = Math.floor(Math.random() * 16777215); 438 439 var material = new THREE.MeshLambertMaterial({ 440 color: color 441 }); 442 443 var mesh = new THREE.Mesh( 444 _createRandomGeometry(size), 445 material); 446 447 mesh.position.x = _random(-40, -20); 448 mesh.position.y = 50; 449 450 _scene.add(mesh); 451 452 _addRigidBody(mesh, mass); 453 } 454 455 // adds a bunch ... 456 457 var nbElems = _random(1, 5); 458 459 for(var i=0; i < nbElems; ++i) { 460 461 newElement(); 462 } 463 } 464 465 /////////////////////////////////////////////////////////// 466 // Update loop 467 // 468 /////////////////////////////////////////////////////////// 469 function _update() { 470 471 _animationId = requestAnimationFrame(_update); 472 473 _world.stepSimulation( 474 _stopWatch.getElapsedMs() * 0.002, 475 10); 476 477 for(var key in _physicBodies) { 478 479 _updateMeshTransform(_physicBodies[key]); 480 } 481 482 _trackball.update(); 483 484 _renderer.render(_scene, _camera); 485 } 486 487 /////////////////////////////////////////////////////////// 488 // Starts simulation 489 // 490 /////////////////////////////////////////////////////////// 491 this.start = function() { 492 493 _stopWatch.getElapsedMs(); 494 495 _update(); 496 497 _intervalId = setInterval( 498 _newElements, 499 1500); 500 } 501 502 /////////////////////////////////////////////////////////// 503 // Stops simulation 504 // 505 /////////////////////////////////////////////////////////// 506 this.stop = function() { 507 508 cancelAnimationFrame(_animationId); 509 510 clearInterval(_intervalId); 511 512 _animationId = null; 513 514 _intervalId = null; 515 } 516 517 /////////////////////////////////////////////////////////// 518 // 519 // 520 /////////////////////////////////////////////////////////// 521 _initializePhysics(); 522 523 _initializeScene(canvasId); 524 } 525 526 var simulationManager = 527 new Autodesk.ADN.AdnSimulationManager('ammojs'); 528 529 simulationManager.start();