Following my preliminary work on Ammo.js - for those who do not click links right away, Ammo.js is an emscripten port of Bullet, a well-established C++ Physics library - this week post features my latest viewer extension, “Physics”.
You can test the live sample from that link and below here how it looks like:1 /////////////////////////////////////////////////////////////////////////////// 2 // Ammo.js Physics viewer extension 3 // by Philippe Leefsma, December 2014 4 // 5 // Dependencies: 6 // 7 // https://rawgit.com/kripken/ammo.js/master/builds/ammo.js 8 // https://rawgit.com/darsain/fpsmeter/master/dist/fpsmeter.min.js 9 // https://rawgit.com/vitalets/angular-xeditable/master/dist/js/xeditable.min.js 10 /////////////////////////////////////////////////////////////////////////////// 11 12 AutodeskNamespace("Autodesk.ADN.Viewing.Extension"); 13 14 Autodesk.ADN.Viewing.Extension.Physics = function (viewer, options) { 15 16 Autodesk.Viewing.Extension.call(this, viewer, options); 17 18 var _fps = null; 19 20 var _self = this; 21 22 var _panel = null; 23 24 var _world = null; 25 26 var _meshMap = {}; 27 28 var _viewer = viewer; 29 30 var _started = false; 31 32 var _running = false; 33 34 var _animationId = null; 35 36 var _selectedEntry = null; 37 38 /////////////////////////////////////////////////////////////////////////// 39 // A stopwatch! 40 // 41 /////////////////////////////////////////////////////////////////////////// 42 var Stopwatch = function() { 43 44 var _startTime = new Date().getTime(); 45 46 this.start = function (){ 47 48 _startTime = new Date().getTime(); 49 }; 50 51 this.getElapsedMs = function(){ 52 53 var elapsedMs = new Date().getTime() - _startTime; 54 55 _startTime = new Date().getTime(); 56 57 return elapsedMs; 58 } 59 } 60 61 var _stopWatch = new Stopwatch(); 62 63 String.prototype.replaceAll = function (find, replace) { 64 return this.replace( 65 new RegExp(find.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'g'), 66 replace); 67 }; 68 69 /////////////////////////////////////////////////////////////////////////// 70 // Extension load callback 71 // 72 /////////////////////////////////////////////////////////////////////////// 73 _self.load = function () { 74 75 console.log('Autodesk.ADN.Viewing.Extension.Physics loading ...'); 76 77 $('<link/>', { 78 rel: 'stylesheet', 79 type: 'text/css', 80 href: 'https://rawgit.com/vitalets/angular-xeditable/master/dist/css/xeditable.css' 81 }).appendTo('head'); 82 83 require([ 84 'https://rawgit.com/kripken/ammo.js/master/builds/ammo.js', 85 'https://rawgit.com/darsain/fpsmeter/master/dist/fpsmeter.min.js', 86 'https://rawgit.com/vitalets/angular-xeditable/master/dist/js/xeditable.min.js' 87 ], function() { 88 89 _self.initialize(function() { 90 91 _panel = _self.loadPanel(); 92 93 _viewer.addEventListener( 94 Autodesk.Viewing.SELECTION_CHANGED_EVENT, 95 _self.onItemSelected); 96 97 console.log('Autodesk.ADN.Viewing.Extension.Physics loaded'); 98 }); 99 }); 100 101 return true; 102 }; 103 104 /////////////////////////////////////////////////////////////////////////// 105 // Extension unload callback 106 // 107 /////////////////////////////////////////////////////////////////////////// 108 _self.unload = function () { 109 110 $('#physicsDivId').remove(); 111 112 _panel.setVisible(false, true); 113 114 _panel = null; 115 116 _self.stop(); 117 118 console.log('Autodesk.ADN.Viewing.Extension.Physics unloaded'); 119 120 return true; 121 }; 122 123 /////////////////////////////////////////////////////////////////////////// 124 // Initializes meshes and grab initial properties 125 // 126 /////////////////////////////////////////////////////////////////////////// 127 _self.initialize = function(callback) { 128 129 _viewer.getObjectTree(function (rootComponent) { 130 131 rootComponent.children.forEach(function(component) { 132 133 var fragIdsArray = (Array.isArray(component.fragIds) ? 134 component.fragIds : 135 [component.fragIds]); 136 137 fragIdsArray.forEach(function(subFragId) { 138 139 var mesh = _viewer.impl.getRenderProxy( 140 _viewer, 141 subFragId); 142 143 _viewer.getPropertyValue( 144 component.dbId, 145 "Mass", function(mass) { 146 147 mass = (mass !== 'undefined' ? mass : 1.0); 148 149 _viewer.getPropertyValue( 150 component.dbId, 151 "vInit", 152 function (vInit) { 153 154 vInit = 155 (vInit !== 'undefined' ? vInit : "0;0;0"); 156 157 vInit = parseArray(vInit, ';'); 158 159 _meshMap[subFragId] = { 160 transform: mesh.matrixWorld.clone(), 161 component: component, 162 163 vAngularInit: [0,0,0], 164 vAngular: [0,0,0], 165 166 vLinearInit: vInit, 167 vLinear: vInit, 168 169 mass: mass, 170 mesh: mesh, 171 body: null 172 } 173 }); 174 }); 175 }); 176 }); 177 178 //done 179 callback(); 180 }); 181 } 182 183 /////////////////////////////////////////////////////////////////////////// 184 // 185 // 186 /////////////////////////////////////////////////////////////////////////// 187 _self.displayVelocity = function(vLinear, vAngular) { 188 189 var editable = angular.element($("#editableDivId")).scope(); 190 191 editable.editables.vx = parseFloat(vLinear[0].toFixed(3)); 192 editable.editables.vy = parseFloat(vLinear[1].toFixed(3)); 193 editable.editables.vz = parseFloat(vLinear[2].toFixed(3)); 194 195 editable.editables.ax = parseFloat(vAngular[0].toFixed(3)); 196 editable.editables.ay = parseFloat(vAngular[1].toFixed(3)); 197 editable.editables.az = parseFloat(vAngular[2].toFixed(3)); 198 } 199 200 /////////////////////////////////////////////////////////////////////////// 201 // item selected callback 202 // 203 /////////////////////////////////////////////////////////////////////////// 204 _self.onItemSelected = function (event) { 205 206 var dbId = event.dbIdArray[0]; 207 208 if(typeof dbId === 'undefined') { 209 $('#editableDivId').css('visibility','collapse'); 210 return; 211 } 212 213 $('#editableDivId').css('visibility','visible'); 214 215 var fragId = event.fragIdsArray[0] 216 217 var fragIdsArray = (Array.isArray(fragId) ? 218 fragId : 219 [fragId]); 220 221 var subFragId = fragIdsArray[0]; 222 223 var vLinear = _meshMap[subFragId].vLinear; 224 225 var vAngular = _meshMap[subFragId].vAngular; 226 227 _self.displayVelocity(vLinear, vAngular); 228 229 _selectedEntry = _meshMap[subFragId]; 230 } 231 232 /////////////////////////////////////////////////////////////////////////// 233 // Creates control panel 234 // 235 /////////////////////////////////////////////////////////////////////////// 236 _self.loadPanel = function() { 237 238 Autodesk.ADN.Viewing.Extension.Physics.ControlPanel = function( 239 parentContainer, 240 id, 241 title, 242 content, 243 x, y) 244 { 245 this.content = content; 246 247 Autodesk.Viewing.UI.DockingPanel.call( 248 this, 249 parentContainer, 250 id, '', 251 {shadow:true}); 252 253 // Auto-fit to the content and don't allow resize. 254 // Position at the given coordinates 255 256 this.container.style.top = y + "px"; 257 this.container.style.left = x + "px"; 258 259 this.container.style.width = "auto"; 260 this.container.style.height = "auto"; 261 this.container.style.resize = "none"; 262 }; 263 264 Autodesk.ADN.Viewing.Extension.Physics. 265 ControlPanel.prototype = Object.create( 266 Autodesk.Viewing.UI.DockingPanel.prototype); 267 268 Autodesk.ADN.Viewing.Extension.Physics. 269 ControlPanel.prototype.constructor = 270 Autodesk.ADN.Viewing.Extension.Physics.ControlPanel; 271 272 Autodesk.ADN.Viewing.Extension.Physics. 273 ControlPanel.prototype.initialize = function() { 274 275 // Override DockingPanel initialize() to: 276 // - create a standard title bar 277 // - click anywhere on the panel to move 278 // - create a close element at the bottom right 279 // 280 this.title = this.createTitleBar( 281 this.titleLabel || 282 this.container.id); 283 284 this.container.appendChild(this.title); 285 this.container.appendChild(this.content); 286 287 //this.initializeMoveHandlers(this.container); 288 289 this.closer = document.createElement("div"); 290 291 this.closer.className = "AdnPanelClose"; 292 //this.closer.textContent = "Close"; 293 294 this.initializeCloseHandler(this.closer); 295 296 this.container.appendChild(this.closer); 297 }; 298 299 var content = document.createElement('div'); 300 301 content.id = 'physicsDivId'; 302 303 var panel = new Autodesk.ADN.Viewing.Extension.Physics. 304 ControlPanel( 305 _viewer.clientContainer, 306 'Physics', 307 'Physics', 308 content, 309 0, 0); 310 311 $('#physicsDivId').css('color', 'white'); 312 313 panel.setVisible(true); 314 315 var appScope = angular.element($("#appBodyId")).scope(); 316 317 var format = '<a href="#" editable-number="editables.%1" ' + 318 'e-step="any" e-style="width:100px" ' + 319 'onaftersave="afterSave()">{{editables.%1}}</a>' 320 321 var html = 322 '<button id="startBtnId" type="button" style="color:#000000;width:100px">Start</button>' + 323 '<button id="resetBtnId" type="button" style="color:#000000;width:100px">Reset</button>' + 324 '<div id="editableDivId" ng-controller="editableController" style="visibility: collapse">' + 325 '<br>' + 326 '<br>  Linear Velocity: ' + 327 '<br>   Vx = ' + format.replaceAll('%1', 'vx') + 328 '<br>   Vy = ' + format.replaceAll('%1', 'vy') + 329 '<br>   Vz = ' + format.replaceAll('%1', 'vz') + 330 '<br><br>  Angular Velocity: ' + 331 '<br>   Ax = ' + format.replaceAll('%1', 'ax') + 332 '<br>   Ay = ' + format.replaceAll('%1', 'ay') + 333 '<br>   Az = ' + format.replaceAll('%1', 'az') + 334 '</div>' 335 336 var element = appScope.compile(html); 337 338 $('#physicsDivId').append(element); 339 340 _self.displayVelocity([0,0,0], [0,0,0]); 341 342 var editable = angular.element($("#editableDivId")).scope(); 343 344 editable.onAfterSave = function () { 345 346 var editables = editable.editables; 347 348 _selectedEntry.vAngular = [ 349 editables.ax, 350 editables.ay, 351 editables.az 352 ]; 353 354 _selectedEntry.vLinear = [ 355 editables.vx, 356 editables.vy, 357 editables.vz 358 ]; 359 360 if(!_started) { 361 362 _selectedEntry.vAngularInit = 363 _selectedEntry.vAngular; 364 365 _selectedEntry.vLinearInit = 366 _selectedEntry.vLinear; 367 } 368 } 369 370 _fps = new FPSMeter(content, { 371 smoothing: 10, 372 show: 'fps', 373 toggleOn: 'click', 374 decimals: 1, 375 zIndex: 999, 376 left: '5px', 377 top: '60px', 378 theme: 'transparent', 379 heat: 1, 380 graph: 1, 381 history: 32}); 382 383 $('#startBtnId').click(function () { 384 385 if (_animationId) { 386 387 $("#startBtnId").text('Start'); 388 389 _self.stop(); 390 } 391 else { 392 393 $("#startBtnId").text('Stop'); 394 395 _self.start(); 396 } 397 }) 398 399 $('#resetBtnId').click(function () { 400 401 if(_running) { 402 403 $("#startBtnId").text('Start'); 404 405 _self.stop(); 406 } 407 408 _self.reset(); 409 }) 410 411 return panel; 412 } 413 414 /////////////////////////////////////////////////////////////////////////// 415 // Creates physics world 416 // 417 /////////////////////////////////////////////////////////////////////////// 418 _self.createWorld = function() { 419 420 var collisionConfiguration = 421 new Ammo.btDefaultCollisionConfiguration; 422 423 var world = new Ammo.btDiscreteDynamicsWorld( 424 new Ammo.btCollisionDispatcher(collisionConfiguration), 425 new Ammo.btDbvtBroadphase, 426 new Ammo.btSequentialImpulseConstraintSolver, 427 collisionConfiguration); 428 429 world.setGravity(new Ammo.btVector3(0, 0, -9.8)); 430 431 return world; 432 } 433 434 /////////////////////////////////////////////////////////////////////////// 435 // Starts simulation 436 // 437 /////////////////////////////////////////////////////////////////////////// 438 _self.start = function() { 439 440 _viewer.select([]); 441 442 // force update 443 _viewer.setView(_viewer.getCurrentView()); 444 445 _world = _self.createWorld(); 446 447 for(var key in _meshMap){ 448 449 var entry = _meshMap[key]; 450 451 var body = createRigidBody( 452 entry); 453 454 _world.addRigidBody(body); 455 456 entry.body = body; 457 } 458 459 _running = true; 460 461 _started = true; 462 463 _stopWatch.getElapsedMs(); 464 465 _self.update(); 466 } 467 468 /////////////////////////////////////////////////////////////////////////// 469 // Stops simulation 470 // 471 /////////////////////////////////////////////////////////////////////////// 472 _self.stop = function() { 473 474 // save current velocities 475 for(var key in _meshMap){ 476 477 var entry = _meshMap[key]; 478 479 var va = entry.body.getAngularVelocity(); 480 var vl = entry.body.getLinearVelocity(); 481 482 entry.vAngular = [va.x(), va.y(), va.z()] 483 entry.vLinear = [vl.x(), vl.y(), vl.z()] 484 } 485 486 _running = false; 487 } 488 489 /////////////////////////////////////////////////////////////////////////// 490 // Update loop 491 // 492 /////////////////////////////////////////////////////////////////////////// 493 _self.update = function() { 494 495 if(!_running) { 496 497 cancelAnimationFrame(_animationId); 498 499 _animationId = null; 500 501 return; 502 } 503 504 _animationId = requestAnimationFrame( 505 _self.update); 506 507 var dt = _stopWatch.getElapsedMs() * 0.002; 508 509 dt = (dt > 0.5 ? 0.5 : dt); 510 511 _world.stepSimulation( 512 dt, 10); 513 514 for(var key in _meshMap) { 515 516 updateMeshTransform(_meshMap[key].body); 517 } 518 519 _viewer.impl.invalidate(true); 520 521 _fps.tick(); 522 } 523 524 /////////////////////////////////////////////////////////////////////////// 525 // Reset simulation 526 // 527 /////////////////////////////////////////////////////////////////////////// 528 _self.reset = function() { 529 530 for(var key in _meshMap) { 531 532 var entry = _meshMap[key]; 533 534 entry.mesh.matrixWorld = 535 entry.transform.clone(); 536 537 entry.vAngular = entry.vAngularInit; 538 539 entry.vLinear = entry.vLinearInit; 540 } 541 542 _viewer.impl.invalidate(true); 543 544 _started = false; 545 } 546 547 /////////////////////////////////////////////////////////////////////////// 548 // Parses string to array: a1;a2;a3 -> [a1, a2, a3] 549 // 550 /////////////////////////////////////////////////////////////////////////// 551 function parseArray(str, separator) { 552 553 var array = str.split(separator); 554 555 var result = []; 556 557 array.forEach(function(element){ 558 559 result.push(parseFloat(element)); 560 }); 561 562 return result; 563 } 564 565 /////////////////////////////////////////////////////////////////////////// 566 // Updates mesh transform according to physic body 567 // 568 /////////////////////////////////////////////////////////////////////////// 569 function updateMeshTransform(body) { 570 571 var mesh = body.mesh; 572 573 var transform = body.getCenterOfMassTransform(); 574 575 var origin = transform.getOrigin(); 576 577 var q = transform.getRotation(); 578 579 mesh.matrixWorld.makeRotationFromQuaternion({ 580 x: q.x(), 581 y: q.y(), 582 z: q.z(), 583 w: q.w() 584 }); 585 586 mesh.matrixWorld.setPosition( 587 new THREE.Vector3( 588 origin.x(), 589 origin.y(), 590 origin.z())); 591 } 592 593 /////////////////////////////////////////////////////////////////////////// 594 // Returns mesh position 595 // 596 /////////////////////////////////////////////////////////////////////////// 597 function getMeshPosition(mesh) { 598 599 var pos = new THREE.Vector3(); 600 601 pos.setFromMatrixPosition(mesh.matrixWorld); 602 603 return pos; 604 } 605 606 /////////////////////////////////////////////////////////////////////////// 607 // Creates collision shape based on mesh vertices 608 // 609 /////////////////////////////////////////////////////////////////////////// 610 function createCollisionShape(mesh) { 611 612 var geometry = mesh.geometry; 613 614 var hull = new Ammo.btConvexHullShape(); 615 616 var vertexBuffer = geometry.vb; 617 618 for(var i=0; i < vertexBuffer.length; i += geometry.vbstride) { 619 620 hull.addPoint(new Ammo.btVector3( 621 vertexBuffer[i], 622 vertexBuffer[i+1], 623 vertexBuffer[i+2])); 624 } 625 626 return hull; 627 } 628 629 /////////////////////////////////////////////////////////////////////////// 630 // Creates physic rigid body from mesh 631 // 632 /////////////////////////////////////////////////////////////////////////// 633 function createRigidBody(entry) { 634 635 var localInertia = new Ammo.btVector3(0, 0, 0); 636 637 var shape = createCollisionShape(entry.mesh); 638 639 shape.calculateLocalInertia(entry.mass, localInertia); 640 641 var transform = new Ammo.btTransform; 642 643 transform.setIdentity(); 644 645 var position = getMeshPosition(entry.mesh); 646 647 transform.setOrigin(new Ammo.btVector3( 648 position.x, 649 position.y, 650 position.z)); 651 652 var q = new THREE.Quaternion(); 653 654 q.setFromRotationMatrix(entry.mesh.matrixWorld); 655 656 transform.setRotation(new Ammo.btQuaternion( 657 q.x, q.y, q.z, q.w 658 )); 659 660 var motionState = new Ammo.btDefaultMotionState(transform); 661 662 var rbInfo = new Ammo.btRigidBodyConstructionInfo( 663 entry.mass, 664 motionState, 665 shape, 666 localInertia); 667 668 var body = new Ammo.btRigidBody(rbInfo); 669 670 body.setLinearVelocity( 671 new Ammo.btVector3( 672 entry.vLinear[0], 673 entry.vLinear[1], 674 entry.vLinear[2])); 675 676 body.setAngularVelocity( 677 new Ammo.btVector3( 678 entry.vAngular[0], 679 entry.vAngular[1], 680 entry.vAngular[2])); 681 682 body.mesh = entry.mesh; 683 684 return body; 685 } 686 }; 687 688 Autodesk.ADN.Viewing.Extension.Physics.prototype = 689 Object.create(Autodesk.Viewing.Extension.prototype); 690 691 Autodesk.ADN.Viewing.Extension.Physics.prototype.constructor = 692 Autodesk.ADN.Viewing.Extension.Physics; 693 694 Autodesk.Viewing.theExtensionManager.registerExtension( 695 'Autodesk.ADN.Viewing.Extension.Physics', 696 Autodesk.ADN.Viewing.Extension.Physics); 697
Comments