├── .gitignore ├── Makefile ├── README.md ├── TODO.md ├── examples ├── basic.html ├── demo.html ├── images │ └── screenshot-threex-colliders-512x512.jpg ├── manual-debug.html ├── manual-object.html └── vendor │ └── three.js │ ├── build │ ├── three.js │ └── three.min.js │ └── examples │ └── js │ └── controls │ └── OrbitControls.js ├── package.require.js ├── threex.collider.js ├── threex.colliderhelper.js └── threex.collidersystem.js /.gitignore: -------------------------------------------------------------------------------- 1 | .betterjs -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # makefile to automatize simple operations 2 | 3 | server: 4 | python -m SimpleHTTPServer 5 | 6 | deploy: 7 | # assume there is something to commit 8 | # use "git diff --exit-code HEAD" to know if there is something to commit 9 | # so two lines: one if no commit, one if something to commit 10 | git commit -a -m "New deploy" && git push -f origin HEAD:gh-pages && git reset HEAD~ 11 | 12 | 13 | ################################################### 14 | # Support betterjs cache dir - http://betterjs.org 15 | buildBetterjs: 16 | jsdoc2betterjs -s -p -d .betterjs *.js 17 | 18 | watchBetterjs: buildBetterjs 19 | # fswatch is available at https://github.com/emcrisostomo/fswatch 20 | fswatch *.js | xargs -n1 jsdoc2betterjs -s -p -d .betterjs 21 | 22 | cleanBetterjs: 23 | rm -rf .betterjs 24 | 25 | serverBetterjs: buildBetterjs 26 | jsdoc2betterjs servecachedir .betterjs 27 | 28 | ################################################### 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 3threex.colliders 2 | ============= 3 | 4 | threex.colliders is a 5 | [threex game extension for three.js](http://www.threejsgames.com/extensions/). 6 | It provides an collider system. Each ```THREE.Object3D``` may be attached to a ```THREEx.Collider``` for AABB. Sphere will be added when time allow. 7 | Then you add those in a ```THREEx.ColliderSystem``` and ```.computeAndNotify()``` all the collisions at this time. 8 | When 2 colliders start colliding with each other, the event 'contactEnter' is sent to each listener. When those colliders keep colliding, the event 'contactStay' is sent. When those colliders are no more colliding, the event sent is 'contactExit'. 9 | 10 | Show Don't Tell 11 | =============== 12 | * [examples/basic.html](http://jeromeetienne.github.io/threex.colliders/examples/basic.html) 13 | \[[view source](https://github.com/jeromeetienne/threex.colliders/blob/master/examples/basic.html)\] : 14 | It shows a basic usage of this extension. 15 | * [examples/demo.html](http://jeromeetienne.github.io/threex.colliders/examples/demo.html) 16 | \[[view source](https://github.com/jeromeetienne/threex.colliders/blob/master/examples/demo.html)\] : 17 | It shows all the cases of collisions. 18 | 19 | A Screenshot 20 | ============ 21 | [![screenshot](https://raw.githubusercontent.com/jeromeetienne/threex.colliders/master/examples/images/screenshot-threex-colliders-512x512.jpg)](http://jeromeetienne.github.io/threex.colliders/examples/basic.html) 22 | 23 | How To Install It 24 | ================= 25 | 26 | You can install it via script tag 27 | 28 | ```html 29 | 30 | ``` 31 | 32 | Or you can install with [bower](http://bower.io/), as you wish. 33 | 34 | ```bash 35 | bower install threex.colliders 36 | ``` 37 | 38 | How To Use It 39 | ============= 40 | 41 | First you need to create a ```THREEx.ColliderSystem```. It gonna handle the whole thing for you 42 | 43 | ``` 44 | var colliderSystem = new THREEx.ColliderSystem() 45 | ``` 46 | 47 | Every time you wish to compute collision and notify associated events among colliders, just do the following 48 | 49 | ``` 50 | colliderSystem.computeAndNotify(colliders) 51 | ```` 52 | 53 | ### How To Add Box3 Collider ? (or call it [AABB](http://en.wikipedia.org/wiki/Axis-aligned_bounding_box#Axis-aligned_minimum_bounding_box)) 54 | 55 | You need a ```THREE.Box3``` to define the shape of your collider. 56 | Say you take default boundingBox from your object geometry, Or another it is all up to you. 57 | 58 | ``` 59 | object3d.geometry.computeBoundingBox() 60 | var box3 = object3d.geometry.boundingBox.clone() 61 | ``` 62 | 63 | Now with your ```THREEx.Box3``` you create your controller 64 | 65 | ``` 66 | var collider = new THREEx.ColliderBox3(object3d, box3) 67 | ``` 68 | 69 | ### Helpers for easier creation 70 | 71 | If you dont want to handle all those cases yourself, i create a small helper 72 | 73 | ``` 74 | var collider = THREEx.Collider.createFromObject3d(object3d) 75 | ``` 76 | 77 | ### How to receive event from colliders ? 78 | 79 | There are 3 kind of events 80 | - **contactEnter(otherCollider)** which is triggered when an object start colliding with another 81 | - **contactExit(otherCollider)** which is notified when the object is no more colliding with another 82 | - **contactStay(otherCollider)** which is notified when the object is still colliding with another 83 | - **contactRemoved(otherColliderId)** which is notified when the other collider has been removed 84 | 85 | To start listening on a event, just do 86 | 87 | ``` 88 | var onCollideEnter = collider.addEventListener('contactEnter', function(otherCollider){ 89 | console.log('contactEnter with', otherCollider.id) 90 | }) 91 | ``` 92 | 93 | To remove the event listener, do the following 94 | 95 | ``` 96 | collider.removeEventListener(onColliderEnter) 97 | ``` 98 | 99 | TODO 100 | ==== 101 | * a THREEx.ColliderGroup. it is a group of other colliders shape 102 | * a collider for sphere, oriented bounding box etc... 103 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | * to a nice visual test demo 2 | 3 | ### File hierachy 4 | threex.colliderSystem.js 5 | threex.collider.js 6 | 7 | 8 | ### What need to be tested 9 | * creation during contact 10 | * removal during contact 11 | * during non contact 12 | 13 | 14 | - do a static object in the middle 15 | - a moving object from left to right 16 | - by tunning where the moving object start and finish 17 | - you can recreate all the case 18 | - there is like 4 of theml -------------------------------------------------------------------------------- /examples/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 201 | -------------------------------------------------------------------------------- /examples/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 19 | 28 |
251 | -------------------------------------------------------------------------------- /examples/images/screenshot-threex-colliders-512x512.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeromeetienne/threex.colliders/725591b2769916bf96a2e5663191a8aba27e1a93/examples/images/screenshot-threex-colliders-512x512.jpg -------------------------------------------------------------------------------- /examples/manual-debug.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 19 | 28 |
258 | -------------------------------------------------------------------------------- /examples/manual-object.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 246 | -------------------------------------------------------------------------------- /examples/vendor/three.js/examples/js/controls/OrbitControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author qiao / https://github.com/qiao 3 | * @author mrdoob / http://mrdoob.com 4 | * @author alteredq / http://alteredqualia.com/ 5 | * @author WestLangley / http://github.com/WestLangley 6 | * @author erich666 / http://erichaines.com 7 | */ 8 | /*global THREE, console */ 9 | 10 | // This set of controls performs orbiting, dollying (zooming), and panning. It maintains 11 | // the "up" direction as +Y, unlike the TrackballControls. Touch on tablet and phones is 12 | // supported. 13 | // 14 | // Orbit - left mouse / touch: one finger move 15 | // Zoom - middle mouse, or mousewheel / touch: two finger spread or squish 16 | // Pan - right mouse, or arrow keys / touch: three finter swipe 17 | // 18 | // This is a drop-in replacement for (most) TrackballControls used in examples. 19 | // That is, include this js file and wherever you see: 20 | // controls = new THREE.TrackballControls( camera ); 21 | // controls.target.z = 150; 22 | // Simple substitute "OrbitControls" and the control should work as-is. 23 | 24 | THREE.OrbitControls = function ( object, domElement ) { 25 | 26 | this.object = object; 27 | this.domElement = ( domElement !== undefined ) ? domElement : document; 28 | 29 | // API 30 | 31 | // Set to false to disable this control 32 | this.enabled = true; 33 | 34 | // "target" sets the location of focus, where the control orbits around 35 | // and where it pans with respect to. 36 | this.target = new THREE.Vector3(); 37 | 38 | // center is old, deprecated; use "target" instead 39 | this.center = this.target; 40 | 41 | // This option actually enables dollying in and out; left as "zoom" for 42 | // backwards compatibility 43 | this.noZoom = false; 44 | this.zoomSpeed = 1.0; 45 | 46 | // Limits to how far you can dolly in and out 47 | this.minDistance = 0; 48 | this.maxDistance = Infinity; 49 | 50 | // Set to true to disable this control 51 | this.noRotate = false; 52 | this.rotateSpeed = 1.0; 53 | 54 | // Set to true to disable this control 55 | this.noPan = false; 56 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 57 | 58 | // Set to true to automatically rotate around the target 59 | this.autoRotate = false; 60 | this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 61 | 62 | // How far you can orbit vertically, upper and lower limits. 63 | // Range is 0 to Math.PI radians. 64 | this.minPolarAngle = 0; // radians 65 | this.maxPolarAngle = Math.PI; // radians 66 | 67 | // Set to true to disable use of the keys 68 | this.noKeys = false; 69 | 70 | // The four arrow keys 71 | this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; 72 | 73 | //////////// 74 | // internals 75 | 76 | var scope = this; 77 | 78 | var EPS = 0.000001; 79 | 80 | var rotateStart = new THREE.Vector2(); 81 | var rotateEnd = new THREE.Vector2(); 82 | var rotateDelta = new THREE.Vector2(); 83 | 84 | var panStart = new THREE.Vector2(); 85 | var panEnd = new THREE.Vector2(); 86 | var panDelta = new THREE.Vector2(); 87 | var panOffset = new THREE.Vector3(); 88 | 89 | var offset = new THREE.Vector3(); 90 | 91 | var dollyStart = new THREE.Vector2(); 92 | var dollyEnd = new THREE.Vector2(); 93 | var dollyDelta = new THREE.Vector2(); 94 | 95 | var phiDelta = 0; 96 | var thetaDelta = 0; 97 | var scale = 1; 98 | var pan = new THREE.Vector3(); 99 | 100 | var lastPosition = new THREE.Vector3(); 101 | 102 | var STATE = { NONE : -1, ROTATE : 0, DOLLY : 1, PAN : 2, TOUCH_ROTATE : 3, TOUCH_DOLLY : 4, TOUCH_PAN : 5 }; 103 | 104 | var state = STATE.NONE; 105 | 106 | // for reset 107 | 108 | this.target0 = this.target.clone(); 109 | this.position0 = this.object.position.clone(); 110 | 111 | // so camera.up is the orbit axis 112 | 113 | var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) ); 114 | var quatInverse = quat.clone().inverse(); 115 | 116 | // events 117 | 118 | var changeEvent = { type: 'change' }; 119 | var startEvent = { type: 'start'}; 120 | var endEvent = { type: 'end'}; 121 | 122 | this.rotateLeft = function ( angle ) { 123 | 124 | if ( angle === undefined ) { 125 | 126 | angle = getAutoRotationAngle(); 127 | 128 | } 129 | 130 | thetaDelta -= angle; 131 | 132 | }; 133 | 134 | this.rotateUp = function ( angle ) { 135 | 136 | if ( angle === undefined ) { 137 | 138 | angle = getAutoRotationAngle(); 139 | 140 | } 141 | 142 | phiDelta -= angle; 143 | 144 | }; 145 | 146 | // pass in distance in world space to move left 147 | this.panLeft = function ( distance ) { 148 | 149 | var te = this.object.matrix.elements; 150 | 151 | // get X column of matrix 152 | panOffset.set( te[ 0 ], te[ 1 ], te[ 2 ] ); 153 | panOffset.multiplyScalar( - distance ); 154 | 155 | pan.add( panOffset ); 156 | 157 | }; 158 | 159 | // pass in distance in world space to move up 160 | this.panUp = function ( distance ) { 161 | 162 | var te = this.object.matrix.elements; 163 | 164 | // get Y column of matrix 165 | panOffset.set( te[ 4 ], te[ 5 ], te[ 6 ] ); 166 | panOffset.multiplyScalar( distance ); 167 | 168 | pan.add( panOffset ); 169 | 170 | }; 171 | 172 | // pass in x,y of change desired in pixel space, 173 | // right and down are positive 174 | this.pan = function ( deltaX, deltaY ) { 175 | 176 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 177 | 178 | if ( scope.object.fov !== undefined ) { 179 | 180 | // perspective 181 | var position = scope.object.position; 182 | var offset = position.clone().sub( scope.target ); 183 | var targetDistance = offset.length(); 184 | 185 | // half of the fov is center to top of screen 186 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); 187 | 188 | // we actually don't use screenWidth, since perspective camera is fixed to screen height 189 | scope.panLeft( 2 * deltaX * targetDistance / element.clientHeight ); 190 | scope.panUp( 2 * deltaY * targetDistance / element.clientHeight ); 191 | 192 | } else if ( scope.object.top !== undefined ) { 193 | 194 | // orthographic 195 | scope.panLeft( deltaX * (scope.object.right - scope.object.left) / element.clientWidth ); 196 | scope.panUp( deltaY * (scope.object.top - scope.object.bottom) / element.clientHeight ); 197 | 198 | } else { 199 | 200 | // camera neither orthographic or perspective 201 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 202 | 203 | } 204 | 205 | }; 206 | 207 | this.dollyIn = function ( dollyScale ) { 208 | 209 | if ( dollyScale === undefined ) { 210 | 211 | dollyScale = getZoomScale(); 212 | 213 | } 214 | 215 | scale /= dollyScale; 216 | 217 | }; 218 | 219 | this.dollyOut = function ( dollyScale ) { 220 | 221 | if ( dollyScale === undefined ) { 222 | 223 | dollyScale = getZoomScale(); 224 | 225 | } 226 | 227 | scale *= dollyScale; 228 | 229 | }; 230 | 231 | this.update = function () { 232 | 233 | var position = this.object.position; 234 | 235 | offset.copy( position ).sub( this.target ); 236 | 237 | // rotate offset to "y-axis-is-up" space 238 | offset.applyQuaternion( quat ); 239 | 240 | // angle from z-axis around y-axis 241 | 242 | var theta = Math.atan2( offset.x, offset.z ); 243 | 244 | // angle from y-axis 245 | 246 | var phi = Math.atan2( Math.sqrt( offset.x * offset.x + offset.z * offset.z ), offset.y ); 247 | 248 | if ( this.autoRotate ) { 249 | 250 | this.rotateLeft( getAutoRotationAngle() ); 251 | 252 | } 253 | 254 | theta += thetaDelta; 255 | phi += phiDelta; 256 | 257 | // restrict phi to be between desired limits 258 | phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, phi ) ); 259 | 260 | // restrict phi to be betwee EPS and PI-EPS 261 | phi = Math.max( EPS, Math.min( Math.PI - EPS, phi ) ); 262 | 263 | var radius = offset.length() * scale; 264 | 265 | // restrict radius to be between desired limits 266 | radius = Math.max( this.minDistance, Math.min( this.maxDistance, radius ) ); 267 | 268 | // move target to panned location 269 | this.target.add( pan ); 270 | 271 | offset.x = radius * Math.sin( phi ) * Math.sin( theta ); 272 | offset.y = radius * Math.cos( phi ); 273 | offset.z = radius * Math.sin( phi ) * Math.cos( theta ); 274 | 275 | // rotate offset back to "camera-up-vector-is-up" space 276 | offset.applyQuaternion( quatInverse ); 277 | 278 | position.copy( this.target ).add( offset ); 279 | 280 | this.object.lookAt( this.target ); 281 | 282 | thetaDelta = 0; 283 | phiDelta = 0; 284 | scale = 1; 285 | pan.set( 0, 0, 0 ); 286 | 287 | if ( lastPosition.distanceToSquared( this.object.position ) > EPS ) { 288 | 289 | this.dispatchEvent( changeEvent ); 290 | 291 | lastPosition.copy( this.object.position ); 292 | 293 | } 294 | 295 | }; 296 | 297 | 298 | this.reset = function () { 299 | 300 | state = STATE.NONE; 301 | 302 | this.target.copy( this.target0 ); 303 | this.object.position.copy( this.position0 ); 304 | 305 | this.update(); 306 | 307 | }; 308 | 309 | function getAutoRotationAngle() { 310 | 311 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 312 | 313 | } 314 | 315 | function getZoomScale() { 316 | 317 | return Math.pow( 0.95, scope.zoomSpeed ); 318 | 319 | } 320 | 321 | function onMouseDown( event ) { 322 | 323 | if ( scope.enabled === false ) return; 324 | event.preventDefault(); 325 | 326 | if ( event.button === 0 ) { 327 | if ( scope.noRotate === true ) return; 328 | 329 | state = STATE.ROTATE; 330 | 331 | rotateStart.set( event.clientX, event.clientY ); 332 | 333 | } else if ( event.button === 1 ) { 334 | if ( scope.noZoom === true ) return; 335 | 336 | state = STATE.DOLLY; 337 | 338 | dollyStart.set( event.clientX, event.clientY ); 339 | 340 | } else if ( event.button === 2 ) { 341 | if ( scope.noPan === true ) return; 342 | 343 | state = STATE.PAN; 344 | 345 | panStart.set( event.clientX, event.clientY ); 346 | 347 | } 348 | 349 | scope.domElement.addEventListener( 'mousemove', onMouseMove, false ); 350 | scope.domElement.addEventListener( 'mouseup', onMouseUp, false ); 351 | scope.dispatchEvent( startEvent ); 352 | 353 | } 354 | 355 | function onMouseMove( event ) { 356 | 357 | if ( scope.enabled === false ) return; 358 | 359 | event.preventDefault(); 360 | 361 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 362 | 363 | if ( state === STATE.ROTATE ) { 364 | 365 | if ( scope.noRotate === true ) return; 366 | 367 | rotateEnd.set( event.clientX, event.clientY ); 368 | rotateDelta.subVectors( rotateEnd, rotateStart ); 369 | 370 | // rotating across whole screen goes 360 degrees around 371 | scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); 372 | 373 | // rotating up and down along whole screen attempts to go 360, but limited to 180 374 | scope.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); 375 | 376 | rotateStart.copy( rotateEnd ); 377 | 378 | } else if ( state === STATE.DOLLY ) { 379 | 380 | if ( scope.noZoom === true ) return; 381 | 382 | dollyEnd.set( event.clientX, event.clientY ); 383 | dollyDelta.subVectors( dollyEnd, dollyStart ); 384 | 385 | if ( dollyDelta.y > 0 ) { 386 | 387 | scope.dollyIn(); 388 | 389 | } else { 390 | 391 | scope.dollyOut(); 392 | 393 | } 394 | 395 | dollyStart.copy( dollyEnd ); 396 | 397 | } else if ( state === STATE.PAN ) { 398 | 399 | if ( scope.noPan === true ) return; 400 | 401 | panEnd.set( event.clientX, event.clientY ); 402 | panDelta.subVectors( panEnd, panStart ); 403 | 404 | scope.pan( panDelta.x, panDelta.y ); 405 | 406 | panStart.copy( panEnd ); 407 | 408 | } 409 | 410 | scope.update(); 411 | 412 | } 413 | 414 | function onMouseUp( /* event */ ) { 415 | 416 | if ( scope.enabled === false ) return; 417 | 418 | scope.domElement.removeEventListener( 'mousemove', onMouseMove, false ); 419 | scope.domElement.removeEventListener( 'mouseup', onMouseUp, false ); 420 | scope.dispatchEvent( endEvent ); 421 | state = STATE.NONE; 422 | 423 | } 424 | 425 | function onMouseWheel( event ) { 426 | 427 | if ( scope.enabled === false || scope.noZoom === true ) return; 428 | 429 | event.preventDefault(); 430 | event.stopPropagation(); 431 | 432 | var delta = 0; 433 | 434 | if ( event.wheelDelta !== undefined ) { // WebKit / Opera / Explorer 9 435 | 436 | delta = event.wheelDelta; 437 | 438 | } else if ( event.detail !== undefined ) { // Firefox 439 | 440 | delta = - event.detail; 441 | 442 | } 443 | 444 | if ( delta > 0 ) { 445 | 446 | scope.dollyOut(); 447 | 448 | } else { 449 | 450 | scope.dollyIn(); 451 | 452 | } 453 | 454 | scope.update(); 455 | scope.dispatchEvent( startEvent ); 456 | scope.dispatchEvent( endEvent ); 457 | 458 | } 459 | 460 | function onKeyDown( event ) { 461 | 462 | if ( scope.enabled === false || scope.noKeys === true || scope.noPan === true ) return; 463 | 464 | switch ( event.keyCode ) { 465 | 466 | case scope.keys.UP: 467 | scope.pan( 0, scope.keyPanSpeed ); 468 | scope.update(); 469 | break; 470 | 471 | case scope.keys.BOTTOM: 472 | scope.pan( 0, - scope.keyPanSpeed ); 473 | scope.update(); 474 | break; 475 | 476 | case scope.keys.LEFT: 477 | scope.pan( scope.keyPanSpeed, 0 ); 478 | scope.update(); 479 | break; 480 | 481 | case scope.keys.RIGHT: 482 | scope.pan( - scope.keyPanSpeed, 0 ); 483 | scope.update(); 484 | break; 485 | 486 | } 487 | 488 | } 489 | 490 | function touchstart( event ) { 491 | 492 | if ( scope.enabled === false ) return; 493 | 494 | switch ( event.touches.length ) { 495 | 496 | case 1: // one-fingered touch: rotate 497 | 498 | if ( scope.noRotate === true ) return; 499 | 500 | state = STATE.TOUCH_ROTATE; 501 | 502 | rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 503 | break; 504 | 505 | case 2: // two-fingered touch: dolly 506 | 507 | if ( scope.noZoom === true ) return; 508 | 509 | state = STATE.TOUCH_DOLLY; 510 | 511 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 512 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 513 | var distance = Math.sqrt( dx * dx + dy * dy ); 514 | dollyStart.set( 0, distance ); 515 | break; 516 | 517 | case 3: // three-fingered touch: pan 518 | 519 | if ( scope.noPan === true ) return; 520 | 521 | state = STATE.TOUCH_PAN; 522 | 523 | panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 524 | break; 525 | 526 | default: 527 | 528 | state = STATE.NONE; 529 | 530 | } 531 | 532 | scope.dispatchEvent( startEvent ); 533 | 534 | } 535 | 536 | function touchmove( event ) { 537 | 538 | if ( scope.enabled === false ) return; 539 | 540 | event.preventDefault(); 541 | event.stopPropagation(); 542 | 543 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 544 | 545 | switch ( event.touches.length ) { 546 | 547 | case 1: // one-fingered touch: rotate 548 | 549 | if ( scope.noRotate === true ) return; 550 | if ( state !== STATE.TOUCH_ROTATE ) return; 551 | 552 | rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 553 | rotateDelta.subVectors( rotateEnd, rotateStart ); 554 | 555 | // rotating across whole screen goes 360 degrees around 556 | scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); 557 | // rotating up and down along whole screen attempts to go 360, but limited to 180 558 | scope.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); 559 | 560 | rotateStart.copy( rotateEnd ); 561 | 562 | scope.update(); 563 | break; 564 | 565 | case 2: // two-fingered touch: dolly 566 | 567 | if ( scope.noZoom === true ) return; 568 | if ( state !== STATE.TOUCH_DOLLY ) return; 569 | 570 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 571 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 572 | var distance = Math.sqrt( dx * dx + dy * dy ); 573 | 574 | dollyEnd.set( 0, distance ); 575 | dollyDelta.subVectors( dollyEnd, dollyStart ); 576 | 577 | if ( dollyDelta.y > 0 ) { 578 | 579 | scope.dollyOut(); 580 | 581 | } else { 582 | 583 | scope.dollyIn(); 584 | 585 | } 586 | 587 | dollyStart.copy( dollyEnd ); 588 | 589 | scope.update(); 590 | break; 591 | 592 | case 3: // three-fingered touch: pan 593 | 594 | if ( scope.noPan === true ) return; 595 | if ( state !== STATE.TOUCH_PAN ) return; 596 | 597 | panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 598 | panDelta.subVectors( panEnd, panStart ); 599 | 600 | scope.pan( panDelta.x, panDelta.y ); 601 | 602 | panStart.copy( panEnd ); 603 | 604 | scope.update(); 605 | break; 606 | 607 | default: 608 | 609 | state = STATE.NONE; 610 | 611 | } 612 | 613 | } 614 | 615 | function touchend( /* event */ ) { 616 | 617 | if ( scope.enabled === false ) return; 618 | 619 | scope.dispatchEvent( endEvent ); 620 | state = STATE.NONE; 621 | 622 | } 623 | 624 | this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false ); 625 | this.domElement.addEventListener( 'mousedown', onMouseDown, false ); 626 | this.domElement.addEventListener( 'mousewheel', onMouseWheel, false ); 627 | this.domElement.addEventListener( 'DOMMouseScroll', onMouseWheel, false ); // firefox 628 | 629 | this.domElement.addEventListener( 'touchstart', touchstart, false ); 630 | this.domElement.addEventListener( 'touchend', touchend, false ); 631 | this.domElement.addEventListener( 'touchmove', touchmove, false ); 632 | 633 | window.addEventListener( 'keydown', onKeyDown, false ); 634 | 635 | // force an update at start 636 | this.update(); 637 | 638 | }; 639 | 640 | THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype ); -------------------------------------------------------------------------------- /package.require.js: -------------------------------------------------------------------------------- 1 | define( [ './threex.collider.js' 2 | , './threex.colliderhelper.js' 3 | , './threex.collidersystem.js' 4 | ], function(module){ 5 | }); -------------------------------------------------------------------------------- /threex.collider.js: -------------------------------------------------------------------------------- 1 | var THREEx = THREEx || {} 2 | 3 | ////////////////////////////////////////////////////////////////////////////////// 4 | // THREEx.Collider 5 | ////////////////////////////////////////////////////////////////////////////////// 6 | 7 | /** 8 | * collider base class 9 | * 10 | * @param {THREE.Object3D} object3d - the object 11 | */ 12 | THREEx.Collider = function(object3d){ 13 | this.id = THREEx.Collider.idCount++ 14 | this.object3d = object3d 15 | this.userData = {} 16 | } 17 | 18 | 19 | THREEx.Collider.idCount = 0; 20 | 21 | 22 | /** 23 | * microevents.js - https://github.com/jeromeetienne/microevent.js 24 | * 25 | * @param {Object} destObj - the destination object 26 | */ 27 | THREEx.Collider.MicroeventMixin = function(destObj){ 28 | destObj.addEventListener = function(event, fct){ 29 | if(this._events === undefined) this._events = {}; 30 | this._events[event] = this._events[event] || []; 31 | this._events[event].push(fct); 32 | return fct; 33 | }; 34 | destObj.removeEventListener = function(event, fct){ 35 | if(this._events === undefined) this._events = {}; 36 | if( event in this._events === false ) return; 37 | this._events[event].splice(this._events[event].indexOf(fct), 1); 38 | }; 39 | destObj.dispatchEvent = function(event /* , args... */){ 40 | if(this._events === undefined) this._events = {}; 41 | if( this._events[event] === undefined ) return; 42 | var tmpArray = this._events[event].slice(); 43 | for(var i = 0; i < tmpArray.length; i++){ 44 | var result = tmpArray[i].apply(this, Array.prototype.slice.call(arguments, 1)) 45 | if( result !== undefined ) return result; 46 | } 47 | return undefined; 48 | }; 49 | }; 50 | 51 | THREEx.Collider.MicroeventMixin(THREEx.Collider.prototype) 52 | 53 | ////////////////////////////////////////////////////////////////////////////////// 54 | // Comment // 55 | ////////////////////////////////////////////////////////////////////////////////// 56 | 57 | /** 58 | * Easy create a collider from a object3d 59 | * 60 | * @param {THREE.Object3D} object3d - the object 61 | * @param {String=} hint - hint on how to create it 62 | * @return {THREE.Collider} - the create collider 63 | */ 64 | THREEx.Collider.createFromObject3d = function(object3d, hint){ 65 | hint = hint || 'default' 66 | 67 | if( hint === 'accurate' ){ 68 | var box3 = new THREE.Box3() 69 | var collider = new THREEx.ColliderBox3(object3d, box3, 'vertices') 70 | }else if( hint === 'fast' || hint === 'default' ){ 71 | // set it from object3d 72 | var box3 = new THREE.Box3() 73 | box3.setFromObject( object3d ); 74 | 75 | // cancel the effect of object3d.position 76 | var center = box3.center() 77 | center.sub(object3d.position) 78 | // cancel the effect of object3d.scale 79 | var size = box3.size() 80 | size.divide(object3d.scale) 81 | // update box3 82 | box3.setFromCenterAndSize(center, size) 83 | // actually create the collider 84 | var collider = new THREEx.ColliderBox3(object3d, box3, 'positionScaleOnly') 85 | }else console.assert(false) 86 | 87 | return collider 88 | } 89 | 90 | 91 | ////////////////////////////////////////////////////////////////////////////////// 92 | ////////////////////////////////////////////////////////////////////////////////// 93 | ////////////////////////////////////////////////////////////////////////////////// 94 | ////////////////////////////////////////////////////////////////////////////////// 95 | ////////////////////////////////////////////////////////////////////////////////// 96 | ////////////////////////////////////////////////////////////////////////////////// 97 | ////////////////////////////////////////////////////////////////////////////////// 98 | ////////////////////////////////////////////////////////////////////////////////// 99 | // THREEx.ColliderBox3 100 | ////////////////////////////////////////////////////////////////////////////////// 101 | ////////////////////////////////////////////////////////////////////////////////// 102 | ////////////////////////////////////////////////////////////////////////////////// 103 | ////////////////////////////////////////////////////////////////////////////////// 104 | ////////////////////////////////////////////////////////////////////////////////// 105 | ////////////////////////////////////////////////////////////////////////////////// 106 | ////////////////////////////////////////////////////////////////////////////////// 107 | ////////////////////////////////////////////////////////////////////////////////// 108 | 109 | THREEx.ColliderBox3 = function(object3d, shape, updateMode){ 110 | console.assert(shape instanceof THREE.Box3 ) 111 | 112 | THREEx.Collider.call( this, object3d ) 113 | 114 | this.shape = shape 115 | this.updatedBox3= shape.clone() 116 | 117 | this.updateMode = updateMode || 'vertices' 118 | } 119 | 120 | THREEx.ColliderBox3.prototype = Object.create( THREEx.Collider.prototype ); 121 | 122 | ////////////////////////////////////////////////////////////////////////////////// 123 | // .update 124 | ////////////////////////////////////////////////////////////////////////////////// 125 | 126 | /** 127 | * update this Collider 128 | * 129 | * @param {String=} updateMode - the update mode to use. default to this.updateMode 130 | */ 131 | THREEx.ColliderBox3.prototype.update = function(updateMode){ 132 | // default arguments 133 | updateMode = updateMode || this.updateMode 134 | var newBox3 = this.shape.clone() 135 | // init newBox3 based on updateMode 136 | if( updateMode === 'vertices' ){ 137 | // full recomputation of the box3 for each vertice, of geometry, of each child 138 | // - it is quite expensive 139 | newBox3.setFromObject(this.object3d) 140 | }else if( updateMode === 'transform' ){ 141 | // TODO should i do that .updateMatrixWorld ? 142 | this.object3d.updateMatrixWorld( true ); 143 | newBox3.applyMatrix4(this.object3d.matrixWorld) 144 | }else if( updateMode === 'none' ){ 145 | // may be useful if the object3d never moves 146 | // - thus you do a collider.update('vertices') on init and collide.updateMode = 'none' 147 | }else if( updateMode === 'positionScaleOnly' ){ 148 | // get matrix in world coordinate 149 | this.object3d.updateMatrixWorld( true ) 150 | var matrix = this.object3d.matrixWorld 151 | // update scale 152 | var scale = new THREE.Vector3().setFromMatrixScale( matrix ); 153 | newBox3.min.multiply(scale) 154 | newBox3.max.multiply(scale) 155 | // update position 156 | var position = new THREE.Vector3().setFromMatrixPosition( matrix ); 157 | newBox3.translate(position) 158 | }else console.assert(false) 159 | 160 | // save this.updatedBox3 161 | this.updatedBox3 = newBox3 162 | } 163 | 164 | ////////////////////////////////////////////////////////////////////////////////// 165 | // .collideWith 166 | ////////////////////////////////////////////////////////////////////////////////// 167 | 168 | /** 169 | * test if this collider collides with the otherCollider 170 | * 171 | * @param {THREEx.Collider} otherCollider - the other collider 172 | * @return {Boolean} - true if they are in contact, false otherwise 173 | */ 174 | THREEx.ColliderBox3.prototype.collideWith = function(otherCollider){ 175 | if( otherCollider instanceof THREEx.ColliderBox3 ){ 176 | return this.collideWithBox3(otherCollider) 177 | }else console.assert(false) 178 | } 179 | 180 | /** 181 | * test if this collider collides with the otherCollider 182 | * 183 | * @param {THREEx.ColliderBox3} otherCollider - the other collider 184 | * @return {Boolean} - true if they are in contact, false otherwise 185 | */ 186 | THREEx.ColliderBox3.prototype.collideWithBox3 = function(otherCollider){ 187 | console.assert( otherCollider instanceof THREEx.ColliderBox3 ) 188 | 189 | var doCollide = this.updatedBox3.isIntersectionBox(otherCollider.updatedBox3) 190 | 191 | return doCollide ? true : false 192 | } 193 | -------------------------------------------------------------------------------- /threex.colliderhelper.js: -------------------------------------------------------------------------------- 1 | var THREEx = THREEx || {} 2 | 3 | /** 4 | * An helper object to help visualize your colilder 5 | * 6 | * @param {THREE.Collider} collider - the collider to monitor 7 | */ 8 | THREEx.ColliderHelper = function( collider ){ 9 | if( collider instanceof THREEx.ColliderBox3 ){ 10 | return new THREEx.ColliderBox3Helper(collider) 11 | }else console.assert(false) 12 | } 13 | 14 | 15 | 16 | ////////////////////////////////////////////////////////////////////////////////// 17 | // THREEx.ColliderBox3Helper 18 | ////////////////////////////////////////////////////////////////////////////////// 19 | 20 | /** 21 | * An helper object to help visualize your colilder 22 | * 23 | * @param {THREE.Collider} collider - the collider to monitor 24 | */ 25 | THREEx.ColliderBox3Helper = function( collider ){ 26 | // check argument 27 | console.assert( collider instanceof THREEx.ColliderBox3 ) 28 | // setup geometry/material 29 | var geometry = new THREE.BoxGeometry(1,1,1) 30 | var material = new THREE.MeshBasicMaterial({ 31 | wireframe : true 32 | }) 33 | 34 | // create the mesh 35 | THREE.Mesh.call(this, geometry, material) 36 | 37 | /** 38 | * make the helper match the collider shape. used the .updatedBox3 39 | */ 40 | this.update = function(){ 41 | var box3 = collider.updatedBox3 42 | this.scale.copy( box3.size() ) 43 | this.position.copy( box3.center() ) 44 | } 45 | 46 | /** 47 | * free webgl memory 48 | */ 49 | this.dispose = function(){ 50 | geometry.dispose() 51 | material.dispose() 52 | } 53 | } 54 | 55 | THREEx.ColliderBox3Helper.prototype = Object.create( THREE.Mesh.prototype ); 56 | -------------------------------------------------------------------------------- /threex.collidersystem.js: -------------------------------------------------------------------------------- 1 | var THREEx = THREEx || {} 2 | 3 | ////////////////////////////////////////////////////////////////////////////////// 4 | // Comment // 5 | ////////////////////////////////////////////////////////////////////////////////// 6 | THREEx.ColliderSystem = function(){ 7 | ////////////////////////////////////////////////////////////////////////////////// 8 | // Comment // 9 | ////////////////////////////////////////////////////////////////////////////////// 10 | 11 | /** 12 | * compute collisions states and notify events appropriatly 13 | * @param {THREEx.Collider[]} colliders - array of colliders 14 | */ 15 | this.computeAndNotify = function(colliders){ 16 | // purge states from the colliders which are no more there 17 | purgeState(colliders) 18 | // compute and notify contacts between colliders 19 | notifyContacts(colliders) 20 | } 21 | 22 | ////////////////////////////////////////////////////////////////////////////////// 23 | // handle colliding states 24 | ////////////////////////////////////////////////////////////////////////////////// 25 | var states = {} 26 | this._states = states 27 | function getStateLabel(collider1, collider2){ 28 | if( collider1.id < collider2.id ) 29 | var stateLabel = collider1.id + '-' + collider2.id 30 | else 31 | var stateLabel = collider2.id + '-' + collider1.id 32 | return stateLabel 33 | 34 | } 35 | 36 | 37 | 38 | ////////////////////////////////////////////////////////////////////////////////// 39 | // Comment // 40 | ////////////////////////////////////////////////////////////////////////////////// 41 | 42 | /** 43 | * purge states 44 | * - go thru all states 45 | * - any states which isnt both in colliders, remove it 46 | * - if only one of both colliders is still present, notify contactRemoved(contactId) 47 | * 48 | * @param {THREE.Collider[]} colliders - base to purge state 49 | */ 50 | function purgeState(colliders){ 51 | // remove pending states for removed collider 52 | Object.keys(states).forEach(function(stateLabel){ 53 | // get leftColliderId 54 | var leftColliderId = parseInt(stateLabel.match(/^([0-9]+)-/)[1]) 55 | var rightColliderId = parseInt(stateLabel.match(/-([0-9]+)$/)[1]) 56 | 57 | // get colliders based on their id 58 | var leftCollider = findById(colliders, leftColliderId) 59 | var rightCollider = findById(colliders, rightColliderId) 60 | 61 | // handle differently depending on their presence 62 | if( leftCollider !== null && rightCollider !== null ){ 63 | // both still present, do nothing 64 | return 65 | }else if( leftCollider !== null && rightCollider === null ){ 66 | // right collider got removed 67 | leftCollider.dispatchEvent('contactRemoved', rightColliderId) 68 | }else if( leftCollider === null && rightCollider !== null ){ 69 | // left collider got removed 70 | rightCollider.dispatchEvent('contactRemoved', leftColliderId) 71 | }else{ 72 | // both got removed 73 | } 74 | 75 | // update states 76 | delete states[stateLabel] 77 | }) 78 | 79 | return 80 | 81 | function findById(colliders, colliderId){ 82 | for( var i = 0; i < colliders.length; i++ ){ 83 | if( colliders[i].id === colliderId ){ 84 | return colliders[i] 85 | } 86 | } 87 | return null 88 | } 89 | } 90 | 91 | ////////////////////////////////////////////////////////////////////////////////// 92 | // Comment // 93 | ////////////////////////////////////////////////////////////////////////////////// 94 | 95 | 96 | /** 97 | * Compute the collision and immediatly notify the listener 98 | * 99 | * @param {THREE.Collider[]} colliders - base to purge state 100 | */ 101 | function notifyContacts(colliders){ 102 | for(var i = 0; i < colliders.length; i++){ 103 | var collider1 = colliders[i] 104 | for(var j = i+1; j < colliders.length; j++){ 105 | var collider2 = colliders[j] 106 | // stay if they do collide 107 | var doCollide = collider1.collideWith(collider2) 108 | // get previous state 109 | var stateLabel = getStateLabel(collider1, collider2) 110 | var stateExisted= states[stateLabel] ? true : false 111 | // process depending do Collide 112 | if( doCollide ){ 113 | // notify proper events 114 | if( stateExisted === true ){ 115 | dispatchEvent(collider1, collider2, 'contactStay') 116 | }else{ 117 | dispatchEvent(collider1, collider2, 'contactEnter') 118 | } 119 | // update states 120 | states[stateLabel] = 'dummy' 121 | }else{ 122 | // notify proper events 123 | if( stateExisted === true ){ 124 | dispatchEvent(collider1, collider2, 'contactExit') 125 | } 126 | // update states 127 | delete states[stateLabel] 128 | } 129 | } 130 | } 131 | // console.log('post notify states', Object.keys(states).length) 132 | return 133 | 134 | function dispatchEvent(collider1, collider2, eventName){ 135 | // console.log('dispatchEvent', eventName, 'between', collider1.id, 'and', collider2.id) 136 | // send event to collider1 137 | collider1.dispatchEvent(eventName, collider2, collider1) 138 | // send event to collider2 139 | collider2.dispatchEvent(eventName, collider1, collider2) 140 | } 141 | } 142 | 143 | /** 144 | * reset the events states 145 | */ 146 | this.reset = function(){ 147 | states = {} 148 | } 149 | } 150 | 151 | --------------------------------------------------------------------------------