├── LICENSE ├── examples ├── js │ ├── libs │ │ └── stats.min.js │ ├── controls │ │ └── TrackballControls.js │ └── GPUPicker.js └── index.html ├── README.md └── GPUPicker.js /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Baoxuan(Brian) Xu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /examples/js/libs/stats.min.js: -------------------------------------------------------------------------------- 1 | // stats.js - http://github.com/mrdoob/stats.js 2 | var Stats=function(){function f(a,e,b){a=document.createElement(a);a.id=e;a.style.cssText=b;return a}function l(a,e,b){var c=f("div",a,"padding:0 0 3px 3px;text-align:left;background:"+b),d=f("div",a+"Text","font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px;color:"+e);d.innerHTML=a.toUpperCase();c.appendChild(d);a=f("div",a+"Graph","width:74px;height:30px;background:"+e);c.appendChild(a);for(e=0;74>e;e++)a.appendChild(f("span","","width:1px;height:30px;float:left;opacity:0.9;background:"+ 3 | b));return c}function m(a){for(var b=c.children,d=0;dr+1E3&&(d=Math.round(1E3* 5 | t/(a-r)),u=Math.min(u,d),v=Math.max(v,d),A.textContent=d+" FPS ("+u+"-"+v+")",p(B,d/100),r=a,t=0,void 0!==h)){var b=performance.memory.usedJSHeapSize,c=performance.memory.jsHeapSizeLimit;h=Math.round(9.54E-7*b);y=Math.min(y,h);z=Math.max(z,h);E.textContent=h+" MB ("+y+"-"+z+")";p(F,b/c)}return a},update:function(){k=this.end()}}};"object"===typeof module&&(module.exports=Stats); 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GPUPicker 2 | ======== 3 | #### color picking based approach for fast searches #### 4 | The goal of this project is to implement a fast way to select an object in the scene for the [THREE.js WebGL library](http://mrdoob.github.com/three.js/). 5 | 6 | ```html 7 | This build is built up on THREE.js ~r92 8 | ``` 9 | ## Features (+ [Example](http://brianxu.github.io/GPUPicker/)) 10 | 11 | * support points/lines/triangle meshes 12 | * retunr intersection object like the one returned from raycaster 13 | * Fast! See the example to compare the framerates bettween raycaster and GPUPicker 14 | 15 | ## TODOs 16 | * A cheap way to update the picking scene instead of set the scene 17 | * Despose picking geometry when the rendering geometry is disposed 18 | 19 | ## Usage 20 | 21 | Download the [script](https://github.com/brianxu/GPUPicker/blob/master/GPUPicker.js) and include it in your html after the [THREE.js WebGL library](http://mrdoob.github.com/three.js/). 22 | 23 | ```html 24 | 25 | 26 | ``` 27 | 28 | #### Initialize 29 | 30 | ```html 31 | gpuPicker = new THREE.GPUPicker({renderer:renderer, debug: false}); 32 | gpuPicker.setFilter(function (object) {return true;}); 33 | gpuPicker.setScene(scene); 34 | gpuPicker.setCamera(camera); 35 | ``` 36 | #### Pick object 37 | An example: 38 | ```html 39 | function onMouseMove( e ) { 40 | if(e.which != 0) 41 | return; 42 | mouse.x = e.clientX; 43 | mouse.y = e.clientY; 44 | var raymouse = new THREE.Vector2(); 45 | raymouse.x = ( e.clientX / window.innerWidth ) * 2 - 1; 46 | raymouse.y = - ( e.clientY / window.innerHeight ) * 2 + 1; 47 | raycaster.setFromCamera( raymouse, camera ); 48 | var intersect; 49 | intersect = gpuPicker.pick(mouse, raycaster); 50 | } 51 | ``` 52 | 53 | #### Update 54 | When the scene camera has some change set the .needUpdate to be true to rerender the picking scene. 55 | ```html 56 | gpuPicker.needUpdate = true; 57 | ``` 58 | 59 | When the window size changes, resize the texture. 60 | ```html 61 | gpuPicker.resizeTexture( newWidth, newHeight ); 62 | ``` 63 | 64 | When the scene changes, reset the scene. 65 | ```html 66 | gpuPicker.setScene(scene); 67 | ``` 68 | 69 | ##[Example](http://brianxu.github.io/GPUPicker/) 70 | -------------------------------------------------------------------------------- /examples/js/controls/TrackballControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Eberhard Graether / http://egraether.com/ 3 | * @author Mark Lundin / http://mark-lundin.com 4 | * @author Simone Manini / http://daron1337.github.io 5 | * @author Luca Antiga / http://lantiga.github.io 6 | */ 7 | 8 | THREE.TrackballControls = function ( object, domElement ) { 9 | 10 | var _this = this; 11 | var STATE = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 }; 12 | 13 | this.object = object; 14 | this.domElement = ( domElement !== undefined ) ? domElement : document; 15 | 16 | // API 17 | 18 | this.enabled = true; 19 | 20 | this.screen = { left: 0, top: 0, width: 0, height: 0 }; 21 | 22 | this.rotateSpeed = 1.0; 23 | this.zoomSpeed = 1.2; 24 | this.panSpeed = 0.3; 25 | 26 | this.noRotate = false; 27 | this.noZoom = false; 28 | this.noPan = false; 29 | 30 | this.staticMoving = false; 31 | this.dynamicDampingFactor = 0.2; 32 | 33 | this.minDistance = 0; 34 | this.maxDistance = Infinity; 35 | 36 | this.keys = [ 65 /*A*/, 83 /*S*/, 68 /*D*/ ]; 37 | 38 | // internals 39 | 40 | this.target = new THREE.Vector3(); 41 | 42 | var EPS = 0.000001; 43 | 44 | var lastPosition = new THREE.Vector3(); 45 | 46 | var _state = STATE.NONE, 47 | _prevState = STATE.NONE, 48 | 49 | _eye = new THREE.Vector3(), 50 | 51 | _movePrev = new THREE.Vector2(), 52 | _moveCurr = new THREE.Vector2(), 53 | 54 | _lastAxis = new THREE.Vector3(), 55 | _lastAngle = 0, 56 | 57 | _zoomStart = new THREE.Vector2(), 58 | _zoomEnd = new THREE.Vector2(), 59 | 60 | _touchZoomDistanceStart = 0, 61 | _touchZoomDistanceEnd = 0, 62 | 63 | _panStart = new THREE.Vector2(), 64 | _panEnd = new THREE.Vector2(); 65 | 66 | // for reset 67 | 68 | this.target0 = this.target.clone(); 69 | this.position0 = this.object.position.clone(); 70 | this.up0 = this.object.up.clone(); 71 | 72 | // events 73 | 74 | var changeEvent = { type: 'change' }; 75 | var startEvent = { type: 'start' }; 76 | var endEvent = { type: 'end' }; 77 | 78 | 79 | // methods 80 | 81 | this.handleResize = function () { 82 | 83 | if ( this.domElement === document ) { 84 | 85 | this.screen.left = 0; 86 | this.screen.top = 0; 87 | this.screen.width = window.innerWidth; 88 | this.screen.height = window.innerHeight; 89 | 90 | } else { 91 | 92 | var box = this.domElement.getBoundingClientRect(); 93 | // adjustments come from similar code in the jquery offset() function 94 | var d = this.domElement.ownerDocument.documentElement; 95 | this.screen.left = box.left + window.pageXOffset - d.clientLeft; 96 | this.screen.top = box.top + window.pageYOffset - d.clientTop; 97 | this.screen.width = box.width; 98 | this.screen.height = box.height; 99 | 100 | } 101 | 102 | }; 103 | 104 | this.handleEvent = function ( event ) { 105 | 106 | if ( typeof this[ event.type ] == 'function' ) { 107 | 108 | this[ event.type ]( event ); 109 | 110 | } 111 | 112 | }; 113 | 114 | var getMouseOnScreen = ( function () { 115 | 116 | var vector = new THREE.Vector2(); 117 | 118 | return function ( pageX, pageY ) { 119 | 120 | vector.set( 121 | ( pageX - _this.screen.left ) / _this.screen.width, 122 | ( pageY - _this.screen.top ) / _this.screen.height 123 | ); 124 | 125 | return vector; 126 | 127 | }; 128 | 129 | }() ); 130 | 131 | var getMouseOnCircle = ( function () { 132 | 133 | var vector = new THREE.Vector2(); 134 | 135 | return function ( pageX, pageY ) { 136 | 137 | vector.set( 138 | ( ( pageX - _this.screen.width * 0.5 - _this.screen.left ) / ( _this.screen.width * 0.5 ) ), 139 | ( ( _this.screen.height + 2 * ( _this.screen.top - pageY ) ) / _this.screen.width ) // screen.width intentional 140 | ); 141 | 142 | return vector; 143 | }; 144 | 145 | }() ); 146 | 147 | this.rotateCamera = (function() { 148 | 149 | var axis = new THREE.Vector3(), 150 | quaternion = new THREE.Quaternion(), 151 | eyeDirection = new THREE.Vector3(), 152 | objectUpDirection = new THREE.Vector3(), 153 | objectSidewaysDirection = new THREE.Vector3(), 154 | moveDirection = new THREE.Vector3(), 155 | angle; 156 | 157 | return function () { 158 | 159 | moveDirection.set( _moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0 ); 160 | angle = moveDirection.length(); 161 | 162 | if ( angle ) { 163 | 164 | _eye.copy( _this.object.position ).sub( _this.target ); 165 | 166 | eyeDirection.copy( _eye ).normalize(); 167 | objectUpDirection.copy( _this.object.up ).normalize(); 168 | objectSidewaysDirection.crossVectors( objectUpDirection, eyeDirection ).normalize(); 169 | 170 | objectUpDirection.setLength( _moveCurr.y - _movePrev.y ); 171 | objectSidewaysDirection.setLength( _moveCurr.x - _movePrev.x ); 172 | 173 | moveDirection.copy( objectUpDirection.add( objectSidewaysDirection ) ); 174 | 175 | axis.crossVectors( moveDirection, _eye ).normalize(); 176 | 177 | angle *= _this.rotateSpeed; 178 | quaternion.setFromAxisAngle( axis, angle ); 179 | 180 | _eye.applyQuaternion( quaternion ); 181 | _this.object.up.applyQuaternion( quaternion ); 182 | 183 | _lastAxis.copy( axis ); 184 | _lastAngle = angle; 185 | 186 | } 187 | 188 | else if ( !_this.staticMoving && _lastAngle ) { 189 | 190 | _lastAngle *= Math.sqrt( 1.0 - _this.dynamicDampingFactor ); 191 | _eye.copy( _this.object.position ).sub( _this.target ); 192 | quaternion.setFromAxisAngle( _lastAxis, _lastAngle ); 193 | _eye.applyQuaternion( quaternion ); 194 | _this.object.up.applyQuaternion( quaternion ); 195 | 196 | } 197 | 198 | _movePrev.copy( _moveCurr ); 199 | 200 | }; 201 | 202 | }()); 203 | 204 | 205 | this.zoomCamera = function () { 206 | 207 | var factor; 208 | 209 | if ( _state === STATE.TOUCH_ZOOM_PAN ) { 210 | 211 | factor = _touchZoomDistanceStart / _touchZoomDistanceEnd; 212 | _touchZoomDistanceStart = _touchZoomDistanceEnd; 213 | _eye.multiplyScalar( factor ); 214 | 215 | } else { 216 | 217 | factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * _this.zoomSpeed; 218 | 219 | if ( factor !== 1.0 && factor > 0.0 ) { 220 | 221 | _eye.multiplyScalar( factor ); 222 | 223 | if ( _this.staticMoving ) { 224 | 225 | _zoomStart.copy( _zoomEnd ); 226 | 227 | } else { 228 | 229 | _zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor; 230 | 231 | } 232 | 233 | } 234 | 235 | } 236 | 237 | }; 238 | 239 | this.panCamera = (function() { 240 | 241 | var mouseChange = new THREE.Vector2(), 242 | objectUp = new THREE.Vector3(), 243 | pan = new THREE.Vector3(); 244 | 245 | return function () { 246 | 247 | mouseChange.copy( _panEnd ).sub( _panStart ); 248 | 249 | if ( mouseChange.lengthSq() ) { 250 | 251 | mouseChange.multiplyScalar( _eye.length() * _this.panSpeed ); 252 | 253 | pan.copy( _eye ).cross( _this.object.up ).setLength( mouseChange.x ); 254 | pan.add( objectUp.copy( _this.object.up ).setLength( mouseChange.y ) ); 255 | 256 | _this.object.position.add( pan ); 257 | _this.target.add( pan ); 258 | 259 | if ( _this.staticMoving ) { 260 | 261 | _panStart.copy( _panEnd ); 262 | 263 | } else { 264 | 265 | _panStart.add( mouseChange.subVectors( _panEnd, _panStart ).multiplyScalar( _this.dynamicDampingFactor ) ); 266 | 267 | } 268 | 269 | } 270 | }; 271 | 272 | }()); 273 | 274 | this.checkDistances = function () { 275 | 276 | if ( !_this.noZoom || !_this.noPan ) { 277 | 278 | if ( _eye.lengthSq() > _this.maxDistance * _this.maxDistance ) { 279 | 280 | _this.object.position.addVectors( _this.target, _eye.setLength( _this.maxDistance ) ); 281 | _zoomStart.copy( _zoomEnd ); 282 | 283 | } 284 | 285 | if ( _eye.lengthSq() < _this.minDistance * _this.minDistance ) { 286 | 287 | _this.object.position.addVectors( _this.target, _eye.setLength( _this.minDistance ) ); 288 | _zoomStart.copy( _zoomEnd ); 289 | 290 | } 291 | 292 | } 293 | 294 | }; 295 | 296 | this.update = function () { 297 | 298 | _eye.subVectors( _this.object.position, _this.target ); 299 | 300 | if ( !_this.noRotate ) { 301 | 302 | _this.rotateCamera(); 303 | 304 | } 305 | 306 | if ( !_this.noZoom ) { 307 | 308 | _this.zoomCamera(); 309 | 310 | } 311 | 312 | if ( !_this.noPan ) { 313 | 314 | _this.panCamera(); 315 | 316 | } 317 | 318 | _this.object.position.addVectors( _this.target, _eye ); 319 | 320 | _this.checkDistances(); 321 | 322 | _this.object.lookAt( _this.target ); 323 | 324 | if ( lastPosition.distanceToSquared( _this.object.position ) > EPS ) { 325 | 326 | _this.dispatchEvent( changeEvent ); 327 | 328 | lastPosition.copy( _this.object.position ); 329 | 330 | } 331 | 332 | }; 333 | 334 | this.reset = function () { 335 | 336 | _state = STATE.NONE; 337 | _prevState = STATE.NONE; 338 | 339 | _this.target.copy( _this.target0 ); 340 | _this.object.position.copy( _this.position0 ); 341 | _this.object.up.copy( _this.up0 ); 342 | 343 | _eye.subVectors( _this.object.position, _this.target ); 344 | 345 | _this.object.lookAt( _this.target ); 346 | 347 | _this.dispatchEvent( changeEvent ); 348 | 349 | lastPosition.copy( _this.object.position ); 350 | 351 | }; 352 | 353 | // listeners 354 | 355 | function keydown( event ) { 356 | 357 | if ( _this.enabled === false ) return; 358 | 359 | window.removeEventListener( 'keydown', keydown ); 360 | 361 | _prevState = _state; 362 | 363 | if ( _state !== STATE.NONE ) { 364 | 365 | return; 366 | 367 | } else if ( event.keyCode === _this.keys[ STATE.ROTATE ] && !_this.noRotate ) { 368 | 369 | _state = STATE.ROTATE; 370 | 371 | } else if ( event.keyCode === _this.keys[ STATE.ZOOM ] && !_this.noZoom ) { 372 | 373 | _state = STATE.ZOOM; 374 | 375 | } else if ( event.keyCode === _this.keys[ STATE.PAN ] && !_this.noPan ) { 376 | 377 | _state = STATE.PAN; 378 | 379 | } 380 | 381 | } 382 | 383 | function keyup( event ) { 384 | 385 | if ( _this.enabled === false ) return; 386 | 387 | _state = _prevState; 388 | 389 | window.addEventListener( 'keydown', keydown, false ); 390 | 391 | } 392 | 393 | function mousedown( event ) { 394 | 395 | if ( _this.enabled === false ) return; 396 | 397 | event.preventDefault(); 398 | event.stopPropagation(); 399 | 400 | if ( _state === STATE.NONE ) { 401 | 402 | _state = event.button; 403 | 404 | } 405 | 406 | if ( _state === STATE.ROTATE && !_this.noRotate ) { 407 | 408 | _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) ); 409 | _movePrev.copy(_moveCurr); 410 | 411 | } else if ( _state === STATE.ZOOM && !_this.noZoom ) { 412 | 413 | _zoomStart.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 414 | _zoomEnd.copy(_zoomStart); 415 | 416 | } else if ( _state === STATE.PAN && !_this.noPan ) { 417 | 418 | _panStart.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 419 | _panEnd.copy(_panStart); 420 | 421 | } 422 | 423 | document.addEventListener( 'mousemove', mousemove, false ); 424 | document.addEventListener( 'mouseup', mouseup, false ); 425 | 426 | _this.dispatchEvent( startEvent ); 427 | 428 | } 429 | 430 | function mousemove( event ) { 431 | 432 | if ( _this.enabled === false ) return; 433 | 434 | event.preventDefault(); 435 | event.stopPropagation(); 436 | 437 | if ( _state === STATE.ROTATE && !_this.noRotate ) { 438 | 439 | _movePrev.copy(_moveCurr); 440 | _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) ); 441 | 442 | } else if ( _state === STATE.ZOOM && !_this.noZoom ) { 443 | 444 | _zoomEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 445 | 446 | } else if ( _state === STATE.PAN && !_this.noPan ) { 447 | 448 | _panEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 449 | 450 | } 451 | 452 | } 453 | 454 | function mouseup( event ) { 455 | 456 | if ( _this.enabled === false ) return; 457 | 458 | event.preventDefault(); 459 | event.stopPropagation(); 460 | 461 | _state = STATE.NONE; 462 | 463 | document.removeEventListener( 'mousemove', mousemove ); 464 | document.removeEventListener( 'mouseup', mouseup ); 465 | _this.dispatchEvent( endEvent ); 466 | 467 | } 468 | 469 | function mousewheel( event ) { 470 | 471 | if ( _this.enabled === false ) return; 472 | 473 | event.preventDefault(); 474 | event.stopPropagation(); 475 | 476 | var delta = 0; 477 | 478 | if ( event.wheelDelta ) { // WebKit / Opera / Explorer 9 479 | 480 | delta = event.wheelDelta / 40; 481 | 482 | } else if ( event.detail ) { // Firefox 483 | 484 | delta = - event.detail / 3; 485 | 486 | } 487 | 488 | _zoomStart.y += delta * 0.01; 489 | _this.dispatchEvent( startEvent ); 490 | _this.dispatchEvent( endEvent ); 491 | 492 | } 493 | 494 | function touchstart( event ) { 495 | 496 | if ( _this.enabled === false ) return; 497 | 498 | switch ( event.touches.length ) { 499 | 500 | case 1: 501 | _state = STATE.TOUCH_ROTATE; 502 | _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); 503 | _movePrev.copy(_moveCurr); 504 | break; 505 | 506 | case 2: 507 | _state = STATE.TOUCH_ZOOM_PAN; 508 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 509 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 510 | _touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy ); 511 | 512 | var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2; 513 | var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2; 514 | _panStart.copy( getMouseOnScreen( x, y ) ); 515 | _panEnd.copy( _panStart ); 516 | break; 517 | 518 | default: 519 | _state = STATE.NONE; 520 | 521 | } 522 | _this.dispatchEvent( startEvent ); 523 | 524 | 525 | } 526 | 527 | function touchmove( event ) { 528 | 529 | if ( _this.enabled === false ) return; 530 | 531 | event.preventDefault(); 532 | event.stopPropagation(); 533 | 534 | switch ( event.touches.length ) { 535 | 536 | case 1: 537 | _movePrev.copy(_moveCurr); 538 | _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); 539 | break; 540 | 541 | case 2: 542 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 543 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 544 | _touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy ); 545 | 546 | var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2; 547 | var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2; 548 | _panEnd.copy( getMouseOnScreen( x, y ) ); 549 | break; 550 | 551 | default: 552 | _state = STATE.NONE; 553 | 554 | } 555 | 556 | } 557 | 558 | function touchend( event ) { 559 | 560 | if ( _this.enabled === false ) return; 561 | 562 | switch ( event.touches.length ) { 563 | 564 | case 1: 565 | _movePrev.copy(_moveCurr); 566 | _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); 567 | break; 568 | 569 | case 2: 570 | _touchZoomDistanceStart = _touchZoomDistanceEnd = 0; 571 | 572 | var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2; 573 | var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2; 574 | _panEnd.copy( getMouseOnScreen( x, y ) ); 575 | _panStart.copy( _panEnd ); 576 | break; 577 | 578 | } 579 | 580 | _state = STATE.NONE; 581 | _this.dispatchEvent( endEvent ); 582 | 583 | } 584 | 585 | this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false ); 586 | 587 | this.domElement.addEventListener( 'mousedown', mousedown, false ); 588 | 589 | this.domElement.addEventListener( 'mousewheel', mousewheel, false ); 590 | this.domElement.addEventListener( 'DOMMouseScroll', mousewheel, false ); // firefox 591 | 592 | this.domElement.addEventListener( 'touchstart', touchstart, false ); 593 | this.domElement.addEventListener( 'touchend', touchend, false ); 594 | this.domElement.addEventListener( 'touchmove', touchmove, false ); 595 | 596 | window.addEventListener( 'keydown', keydown, false ); 597 | window.addEventListener( 'keyup', keyup, false ); 598 | 599 | this.handleResize(); 600 | 601 | // force an update at start 602 | this.update(); 603 | 604 | }; 605 | 606 | THREE.TrackballControls.prototype = Object.create( THREE.EventDispatcher.prototype ); 607 | THREE.TrackballControls.prototype.constructor = THREE.TrackballControls; 608 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | three.js webgl - gpu picking 6 | 7 | 8 | 30 | 31 | 32 | 33 | 34 |
35 | GPUPicker webgl - gpu picking by 36 | baoxuan 37 |
use key 'g' to switch between GPUPicker and regular raycasting; use key 'd' to hide/show debug rendering 38 |
39 | 40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 535 | 536 | 537 | 538 | -------------------------------------------------------------------------------- /GPUPicker.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * @author baoxuanxu https://github.com/brianxu 4 | */ 5 | var THREE = THREE || {}; 6 | (function (THREE) { 7 | var FaceIDShader = { 8 | vertexShader: [ 9 | "attribute float id;", 10 | "", 11 | "uniform float size;", 12 | "uniform float scale;", 13 | "uniform float baseId;", 14 | "", 15 | "varying vec4 worldId;", 16 | "", 17 | "void main() {", 18 | " vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );", 19 | " gl_PointSize = size * ( scale / length( mvPosition.xyz ) );", 20 | " float i = baseId + id;", 21 | " vec3 a = fract(vec3(1.0/255.0, 1.0/(255.0*255.0), 1.0/(255.0*255.0*255.0)) * i);", 22 | " a -= a.xxy * vec3(0.0, 1.0/255.0, 1.0/255.0);", 23 | " worldId = vec4(a,1);", 24 | " gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", 25 | "}" 26 | ].join("\n"), 27 | 28 | fragmentShader: [ 29 | "#ifdef GL_ES\n", 30 | "precision highp float;\n", 31 | "#endif\n", 32 | "", 33 | "varying vec4 worldId;", 34 | "", 35 | "void main() {", 36 | " gl_FragColor = worldId;", 37 | "}" 38 | ].join("\n") 39 | }; 40 | 41 | var FaceIDMaterial = function () { 42 | THREE.ShaderMaterial.call(this, { 43 | uniforms: { 44 | baseId: { 45 | type: "f", 46 | value: 0 47 | }, 48 | size: { 49 | type: "f", 50 | value: 0.01, 51 | }, 52 | scale: { 53 | type: "f", 54 | value: 400, 55 | } 56 | }, 57 | vertexShader: FaceIDShader.vertexShader, 58 | fragmentShader: FaceIDShader.fragmentShader 59 | 60 | }); 61 | }; 62 | FaceIDMaterial.prototype = Object.create(THREE.ShaderMaterial.prototype); 63 | FaceIDMaterial.prototype.constructor = FaceIDMaterial; 64 | FaceIDMaterial.prototype.setBaseID = function (baseId) { 65 | this.uniforms.baseId.value = baseId; 66 | }; 67 | FaceIDMaterial.prototype.setPointSize = function (size) { 68 | this.uniforms.size.value = size; 69 | }; 70 | FaceIDMaterial.prototype.setPointScale = function (scale) { 71 | this.uniforms.scale.value = scale; 72 | }; 73 | 74 | //add a originalObject to Object3D 75 | (function (clone) { 76 | THREE.Object3D.prototype.clone = function (recursive) { 77 | var object = clone.call(this, recursive); 78 | // keep a ref to originalObject 79 | object.originalObject = this; 80 | object.priority = this.priority; 81 | return object; 82 | }; 83 | }(THREE.Object3D.prototype.clone)); 84 | //add a originalObject to Points 85 | (function (clone) { 86 | THREE.Points.prototype.clone = function (recursive) { 87 | var object = clone.call(this, recursive); 88 | // keep a ref to originalObject 89 | object.originalObject = this; 90 | object.priority = this.priority; 91 | return object; 92 | }; 93 | }(THREE.Points.prototype.clone)); 94 | //add a originalObject to Mesh 95 | (function (clone) { 96 | THREE.Mesh.prototype.clone = function () { 97 | var object = clone.call(this); 98 | // keep a ref to originalObject 99 | object.originalObject = this; 100 | object.priority = this.priority; 101 | return object; 102 | }; 103 | }(THREE.Mesh.prototype.clone)); 104 | //add a originalObject to Line 105 | (function (clone) { 106 | THREE.Line.prototype.clone = function () { 107 | var object = clone.call(this); 108 | // keep a ref to originalObject 109 | object.originalObject = this; 110 | object.priority = this.priority; 111 | return object; 112 | }; 113 | }(THREE.Line.prototype.clone)); 114 | 115 | THREE.Mesh.prototype.raycastWithID = (function () { 116 | var vA = new THREE.Vector3(); 117 | var vB = new THREE.Vector3(); 118 | var vC = new THREE.Vector3(); 119 | var inverseMatrix = new THREE.Matrix4(); 120 | var ray = new THREE.Ray(); 121 | var triangle = new THREE.Triangle(); 122 | var intersectionPointWorld = new THREE.Vector3(); 123 | var intersectionPoint = new THREE.Vector3(); 124 | function checkIntersection(object, raycaster, ray, pA, pB, pC, point) { 125 | var intersect; 126 | intersect = ray.intersectTriangle(pC, pB, pA, false, point); 127 | var plane = new THREE.Plane(); 128 | plane.setFromCoplanarPoints(pA, pB, pC); 129 | intersect = ray.intersectPlane(plane, new THREE.Vector3()); 130 | if (intersect === null) return null; 131 | intersectionPointWorld.copy(point); 132 | intersectionPointWorld.applyMatrix4(object.matrixWorld); 133 | var distance = raycaster.ray.origin.distanceTo(intersectionPointWorld); 134 | if (distance < raycaster.near || distance > raycaster.far) return null; 135 | return { 136 | distance: distance, 137 | point: intersectionPointWorld.clone(), 138 | object: object 139 | }; 140 | 141 | } 142 | 143 | return function (elID, raycaster) { 144 | var geometry = this.geometry; 145 | var attributes = geometry.attributes; 146 | inverseMatrix.getInverse(this.matrixWorld); 147 | ray.copy(raycaster.ray).applyMatrix4(inverseMatrix); 148 | var a, b, c; 149 | if (geometry.index !== null) { 150 | console.log("WARNING: raycastWithID does not support indexed vertices"); 151 | } else { 152 | var position = attributes.position; 153 | var j = elID * 3; 154 | a = j; 155 | b = j + 1; 156 | c = j + 2; 157 | vA.fromBufferAttribute(position, a); 158 | vB.fromBufferAttribute(position, b); 159 | vC.fromBufferAttribute(position, c); 160 | } 161 | var intersection = checkIntersection(this, raycaster, ray, vA, vB, vC, intersectionPoint); 162 | if (intersection === null) { 163 | console.log("WARNING: intersectionPoint missing"); 164 | return; 165 | } 166 | 167 | var face = new THREE.Face3(a, b, c); 168 | THREE.Triangle.getNormal(vA, vB, vC, face.normal); 169 | 170 | intersection.face = face; 171 | intersection.faceIndex = a; 172 | return intersection; 173 | }; 174 | 175 | }()); 176 | 177 | THREE.Line.prototype.raycastWithID = (function () { 178 | var inverseMatrix = new THREE.Matrix4(); 179 | var ray = new THREE.Ray(); 180 | 181 | var vStart = new THREE.Vector3(); 182 | var vEnd = new THREE.Vector3(); 183 | var interSegment = new THREE.Vector3(); 184 | var interRay = new THREE.Vector3(); 185 | return function (elID, raycaster) { 186 | inverseMatrix.getInverse(this.matrixWorld); 187 | ray.copy(raycaster.ray).applyMatrix4(inverseMatrix); 188 | var geometry = this.geometry; 189 | if (geometry instanceof THREE.BufferGeometry) { 190 | 191 | var attributes = geometry.attributes; 192 | 193 | if (geometry.index !== null) { 194 | console.log("WARNING: raycastWithID does not support indexed vertices"); 195 | } else { 196 | 197 | var positions = attributes.position.array; 198 | var i = elID * 6; 199 | vStart.fromArray(positions, i); 200 | vEnd.fromArray(positions, i + 3); 201 | 202 | var distSq = ray.distanceSqToSegment(vStart, vEnd, interRay, interSegment); 203 | var distance = ray.origin.distanceTo(interRay); 204 | 205 | if (distance < raycaster.near || distance > raycaster.far) return; 206 | 207 | var intersect = { 208 | 209 | distance: distance, 210 | // What do we want? intersection point on the ray or on the segment?? 211 | // point: raycaster.ray.at( distance ), 212 | point: interSegment.clone().applyMatrix4(this.matrixWorld), 213 | index: i, 214 | face: null, 215 | faceIndex: null, 216 | object: this 217 | 218 | }; 219 | return intersect; 220 | 221 | } 222 | 223 | } 224 | }; 225 | 226 | })(); 227 | 228 | THREE.Points.prototype.raycastWithID = (function () { 229 | 230 | var inverseMatrix = new THREE.Matrix4(); 231 | var ray = new THREE.Ray(); 232 | 233 | return function (elID, raycaster) { 234 | var object = this; 235 | var geometry = object.geometry; 236 | 237 | inverseMatrix.getInverse(this.matrixWorld); 238 | ray.copy(raycaster.ray).applyMatrix4(inverseMatrix); 239 | var position = new THREE.Vector3(); 240 | 241 | var testPoint = function (point, index) { 242 | var rayPointDistance = ray.distanceToPoint(point); 243 | var intersectPoint = ray.closestPointToPoint(point); 244 | intersectPoint.applyMatrix4(object.matrixWorld); 245 | 246 | var distance = raycaster.ray.origin.distanceTo(intersectPoint); 247 | 248 | if (distance < raycaster.near || distance > raycaster.far) return; 249 | 250 | var intersect = { 251 | 252 | distance: distance, 253 | distanceToRay: rayPointDistance, 254 | point: intersectPoint.clone(), 255 | index: index, 256 | face: null, 257 | object: object 258 | 259 | }; 260 | return intersect; 261 | }; 262 | var attributes = geometry.attributes; 263 | var positions = attributes.position.array; 264 | position.fromArray(positions, elID * 3); 265 | 266 | return testPoint(position, elID); 267 | 268 | }; 269 | 270 | }()); 271 | 272 | THREE.GPUPicker = function (option) { 273 | if (option === undefined) { 274 | option = {}; 275 | } 276 | this.pickingScene = new THREE.Scene(); 277 | this.pickingTexture = new THREE.WebGLRenderTarget(); 278 | this.pickingTexture.texture.minFilter = THREE.LinearFilter; 279 | this.pickingTexture.texture.generateMipmaps = false; 280 | this.lineShell = option.lineShell !== undefined ? option.lineShell : 4; 281 | this.pointShell = option.pointShell !== undefined ? option.pointShell : 0.1; 282 | this.debug = option.debug !== undefined ? option.debug : false; 283 | this.needUpdate = true; 284 | if (option.renderer) { 285 | this.setRenderer(option.renderer); 286 | } 287 | 288 | // array of original objects 289 | this.container = []; 290 | this.objectsMap = {}; 291 | //default filter 292 | this.setFilter(); 293 | }; 294 | THREE.GPUPicker.prototype.setRenderer = function (renderer) { 295 | this.renderer = renderer; 296 | var size = renderer.getSize(); 297 | this.resizeTexture(size.width, size.height); 298 | this.needUpdate = true; 299 | }; 300 | THREE.GPUPicker.prototype.resizeTexture = function (width, height) { 301 | this.pickingTexture.setSize(width, height); 302 | this.pixelBuffer = new Uint8Array(4 * width * height); 303 | this.needUpdate = true; 304 | }; 305 | THREE.GPUPicker.prototype.setCamera = function (camera) { 306 | this.camera = camera; 307 | this.needUpdate = true; 308 | }; 309 | THREE.GPUPicker.prototype.update = function () { 310 | if (this.needUpdate) { 311 | this.renderer.render(this.pickingScene, this.camera, this.pickingTexture); 312 | //read the rendering texture 313 | this.renderer.readRenderTargetPixels(this.pickingTexture, 0, 0, this.pickingTexture.width, this.pickingTexture.height, this.pixelBuffer); 314 | this.needUpdate = false; 315 | if (this.debug) console.log("GPUPicker rendering updated"); 316 | } 317 | }; 318 | THREE.GPUPicker.prototype.setFilter = function (func) { 319 | if (func instanceof Function) { 320 | this.filterFunc = func; 321 | } else { 322 | //default filter 323 | this.filterFunc = function (object) { 324 | return true; 325 | }; 326 | } 327 | 328 | }; 329 | THREE.GPUPicker.prototype.setScene = function (scene) { 330 | this.pickingScene = scene.clone(); 331 | this._processObject(this.pickingScene, 0); 332 | this.needUpdate = true; 333 | }; 334 | 335 | 336 | THREE.GPUPicker.prototype.pick = function (mouse, raycaster) { 337 | this.update(); 338 | var index = mouse.x + (this.pickingTexture.height - mouse.y) * this.pickingTexture.width; 339 | //interpret the pixel as an ID 340 | var id = (this.pixelBuffer[index * 4 + 2] * 255 * 255) + (this.pixelBuffer[index * 4 + 1] * 255) + (this.pixelBuffer[index * 4 + 0]); 341 | // get object with this id in range 342 | // var object = this._getObject(id); 343 | if (this.debug) console.log("pick id:", id); 344 | var result = this._getObject(this.pickingScene, 0, id); 345 | var object = result[1]; 346 | var elementId = id - result[0]; 347 | if (object) { 348 | if (object.raycastWithID) { 349 | var intersect = object.raycastWithID(elementId, raycaster); 350 | intersect.object = object.originalObject; 351 | return intersect; 352 | } 353 | 354 | } 355 | return; 356 | }; 357 | 358 | /* 359 | * get object by id 360 | */ 361 | THREE.GPUPicker.prototype._getObject = function (object, baseId, id) { 362 | // if (this.debug) console.log("_getObject ",baseId); 363 | if (object.elementsCount !== undefined && id >= baseId && id < baseId + object.elementsCount) { 364 | return [baseId, object]; 365 | } 366 | if (object.elementsCount !== undefined) { 367 | baseId += object.elementsCount; 368 | } 369 | var result = [baseId, undefined]; 370 | for (var i = 0; i < object.children.length; i++) { 371 | result = this._getObject(object.children[i], result[0], id); 372 | if (result[1] !== undefined) 373 | break; 374 | } 375 | return result; 376 | }; 377 | 378 | /* 379 | * process the object to add elementId information 380 | */ 381 | THREE.GPUPicker.prototype._processObject = function (object, baseId) { 382 | baseId += this._addElementID(object, baseId); 383 | for (var i = 0; i < object.children.length; i++) { 384 | baseId = this._processObject(object.children[i], baseId); 385 | 386 | } 387 | return baseId; 388 | }; 389 | 390 | THREE.GPUPicker.prototype._addElementID = function (object, baseId) { 391 | if (!this.filterFunc(object) && object.geometry !== undefined) { 392 | object.visible = false; 393 | return 0; 394 | } 395 | 396 | if (object.geometry) { 397 | var __pickingGeometry; 398 | //check if geometry has cached geometry for picking 399 | if (object.geometry.__pickingGeometry) { 400 | __pickingGeometry = object.geometry.__pickingGeometry; 401 | } else { 402 | __pickingGeometry = object.geometry; 403 | // convert geometry to buffer geometry 404 | if (object.geometry instanceof THREE.Geometry) { 405 | if (this.debug) console.log("convert geometry to buffer geometry"); 406 | __pickingGeometry = new THREE.BufferGeometry().setFromObject(object); 407 | } 408 | var units = 1; 409 | if (object instanceof THREE.Points) { 410 | units = 1; 411 | } else if (object instanceof THREE.Line) { 412 | units = 2; 413 | } else if (object instanceof THREE.Mesh) { 414 | units = 3; 415 | } 416 | var el, el3, elementsCount, i, indices, positionBuffer, vertex3, verts, vertexIndex3; 417 | if (__pickingGeometry.index !== null) { 418 | __pickingGeometry = __pickingGeometry.clone(); 419 | if (this.debug) console.log("convert indexed geometry to non-indexed geometry"); 420 | 421 | indices = __pickingGeometry.index.array; 422 | verts = __pickingGeometry.attributes.position.array; 423 | delete __pickingGeometry.attributes.position; 424 | __pickingGeometry.index = null; 425 | delete __pickingGeometry.attributes.normal; 426 | elementsCount = indices.length / units; 427 | positionBuffer = new Float32Array(elementsCount * 3 * units); 428 | 429 | __pickingGeometry.addAttribute('position', new THREE.BufferAttribute(positionBuffer, 3)); 430 | for (el = 0; el < elementsCount; ++el) { 431 | el3 = units * el; 432 | for (i = 0; i < units; ++i) { 433 | vertexIndex3 = 3 * indices[el3 + i]; 434 | vertex3 = 3 * (el3 + i); 435 | positionBuffer[vertex3] = verts[vertexIndex3]; 436 | positionBuffer[vertex3 + 1] = verts[vertexIndex3 + 1]; 437 | positionBuffer[vertex3 + 2] = verts[vertexIndex3 + 2]; 438 | } 439 | } 440 | 441 | __pickingGeometry.computeVertexNormals(); 442 | } 443 | if (object instanceof THREE.Line && !(object instanceof THREE.LineSegments)) { 444 | if (this.debug) console.log("convert Line to LineSegments"); 445 | verts = __pickingGeometry.attributes.position.array; 446 | delete __pickingGeometry.attributes.position; 447 | elementsCount = verts.length / 3 - 1; 448 | positionBuffer = new Float32Array(elementsCount * units * 3); 449 | 450 | __pickingGeometry.addAttribute('position', new THREE.BufferAttribute(positionBuffer, 3)); 451 | for (el = 0; el < elementsCount; ++el) { 452 | el3 = 3 * el; 453 | vertexIndex3 = el3; 454 | vertex3 = el3 * 2; 455 | positionBuffer[vertex3] = verts[vertexIndex3]; 456 | positionBuffer[vertex3 + 1] = verts[vertexIndex3 + 1]; 457 | positionBuffer[vertex3 + 2] = verts[vertexIndex3 + 2]; 458 | positionBuffer[vertex3 + 3] = verts[vertexIndex3 + 3]; 459 | positionBuffer[vertex3 + 4] = verts[vertexIndex3 + 4]; 460 | positionBuffer[vertex3 + 5] = verts[vertexIndex3 + 5]; 461 | 462 | } 463 | 464 | __pickingGeometry.computeVertexNormals(); 465 | object.__proto__ = THREE.LineSegments.prototype; //make the renderer render as line segments 466 | } 467 | var attributes = __pickingGeometry.attributes; 468 | var positions = attributes.position.array; 469 | var vertexCount = positions.length / 3; 470 | var ids = new THREE.Float32BufferAttribute(vertexCount, 1); 471 | //set vertex id color 472 | 473 | for (var i = 0, il = vertexCount / units; i < il; i++) { 474 | for (var j = 0; j < units; ++j) { 475 | ids.array[i * units + j] = i; 476 | } 477 | } 478 | __pickingGeometry.addAttribute('id', ids); 479 | __pickingGeometry.elementsCount = vertexCount / units; 480 | //cache __pickingGeometry inside geometry 481 | object.geometry.__pickingGeometry = __pickingGeometry; 482 | } 483 | 484 | //use __pickingGeometry in the picking mesh 485 | object.geometry = __pickingGeometry; 486 | object.elementsCount = __pickingGeometry.elementsCount;//elements count 487 | 488 | var pointSize = object.material.size || 0.01; 489 | var linewidth = object.material.linewidth || 1; 490 | object.material = new FaceIDMaterial(); 491 | object.material.linewidth = linewidth + this.lineShell;//make the line a little wider to hit 492 | object.material.setBaseID(baseId); 493 | object.material.setPointSize(pointSize + this.pointShell);//make the point a little wider to hit 494 | object.material.setPointScale(this.renderer.getSize().height * this.renderer.getPixelRatio() / 2); 495 | return object.elementsCount; 496 | } 497 | return 0; 498 | }; 499 | }(THREE)); 500 | -------------------------------------------------------------------------------- /examples/js/GPUPicker.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * @author baoxuanxu https://github.com/brianxu 4 | */ 5 | var THREE = THREE || {}; 6 | (function (THREE) { 7 | var FaceIDShader = { 8 | vertexShader: [ 9 | "attribute float id;", 10 | "", 11 | "uniform float size;", 12 | "uniform float scale;", 13 | "uniform float baseId;", 14 | "", 15 | "varying vec4 worldId;", 16 | "", 17 | "void main() {", 18 | " vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );", 19 | " gl_PointSize = size * ( scale / length( mvPosition.xyz ) );", 20 | " float i = baseId + id;", 21 | " vec3 a = fract(vec3(1.0/255.0, 1.0/(255.0*255.0), 1.0/(255.0*255.0*255.0)) * i);", 22 | " a -= a.xxy * vec3(0.0, 1.0/255.0, 1.0/255.0);", 23 | " worldId = vec4(a,1);", 24 | " gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", 25 | "}" 26 | ].join("\n"), 27 | 28 | fragmentShader: [ 29 | "#ifdef GL_ES\n", 30 | "precision highp float;\n", 31 | "#endif\n", 32 | "", 33 | "varying vec4 worldId;", 34 | "", 35 | "void main() {", 36 | " gl_FragColor = worldId;", 37 | "}" 38 | ].join("\n") 39 | }; 40 | 41 | var FaceIDMaterial = function () { 42 | THREE.ShaderMaterial.call(this, { 43 | uniforms: { 44 | baseId: { 45 | type: "f", 46 | value: 0 47 | }, 48 | size: { 49 | type: "f", 50 | value: 0.01, 51 | }, 52 | scale: { 53 | type: "f", 54 | value: 400, 55 | } 56 | }, 57 | vertexShader: FaceIDShader.vertexShader, 58 | fragmentShader: FaceIDShader.fragmentShader 59 | 60 | }); 61 | }; 62 | FaceIDMaterial.prototype = Object.create(THREE.ShaderMaterial.prototype); 63 | FaceIDMaterial.prototype.constructor = FaceIDMaterial; 64 | FaceIDMaterial.prototype.setBaseID = function (baseId) { 65 | this.uniforms.baseId.value = baseId; 66 | }; 67 | FaceIDMaterial.prototype.setPointSize = function (size) { 68 | this.uniforms.size.value = size; 69 | }; 70 | FaceIDMaterial.prototype.setPointScale = function (scale) { 71 | this.uniforms.scale.value = scale; 72 | }; 73 | 74 | //add a originalObject to Object3D 75 | (function (clone) { 76 | THREE.Object3D.prototype.clone = function (recursive) { 77 | var object = clone.call(this, recursive); 78 | // keep a ref to originalObject 79 | object.originalObject = this; 80 | object.priority = this.priority; 81 | return object; 82 | }; 83 | }(THREE.Object3D.prototype.clone)); 84 | //add a originalObject to Points 85 | (function (clone) { 86 | THREE.Points.prototype.clone = function (recursive) { 87 | var object = clone.call(this, recursive); 88 | // keep a ref to originalObject 89 | object.originalObject = this; 90 | object.priority = this.priority; 91 | return object; 92 | }; 93 | }(THREE.Points.prototype.clone)); 94 | //add a originalObject to Mesh 95 | (function (clone) { 96 | THREE.Mesh.prototype.clone = function () { 97 | var object = clone.call(this); 98 | // keep a ref to originalObject 99 | object.originalObject = this; 100 | object.priority = this.priority; 101 | return object; 102 | }; 103 | }(THREE.Mesh.prototype.clone)); 104 | //add a originalObject to Line 105 | (function (clone) { 106 | THREE.Line.prototype.clone = function () { 107 | var object = clone.call(this); 108 | // keep a ref to originalObject 109 | object.originalObject = this; 110 | object.priority = this.priority; 111 | return object; 112 | }; 113 | }(THREE.Line.prototype.clone)); 114 | 115 | THREE.Mesh.prototype.raycastWithID = (function () { 116 | var vA = new THREE.Vector3(); 117 | var vB = new THREE.Vector3(); 118 | var vC = new THREE.Vector3(); 119 | var inverseMatrix = new THREE.Matrix4(); 120 | var ray = new THREE.Ray(); 121 | var triangle = new THREE.Triangle(); 122 | var intersectionPointWorld = new THREE.Vector3(); 123 | var intersectionPoint = new THREE.Vector3(); 124 | function checkIntersection(object, raycaster, ray, pA, pB, pC, point) { 125 | var intersect; 126 | intersect = ray.intersectTriangle(pC, pB, pA, false, point); 127 | var plane = new THREE.Plane(); 128 | plane.setFromCoplanarPoints(pA, pB, pC); 129 | intersect = ray.intersectPlane(plane, new THREE.Vector3()); 130 | if (intersect === null) return null; 131 | intersectionPointWorld.copy(point); 132 | intersectionPointWorld.applyMatrix4(object.matrixWorld); 133 | var distance = raycaster.ray.origin.distanceTo(intersectionPointWorld); 134 | if (distance < raycaster.near || distance > raycaster.far) return null; 135 | return { 136 | distance: distance, 137 | point: intersectionPointWorld.clone(), 138 | object: object 139 | }; 140 | 141 | } 142 | 143 | return function (elID, raycaster) { 144 | var geometry = this.geometry; 145 | var attributes = geometry.attributes; 146 | inverseMatrix.getInverse(this.matrixWorld); 147 | ray.copy(raycaster.ray).applyMatrix4(inverseMatrix); 148 | var a, b, c; 149 | if (geometry.index !== null) { 150 | console.log("WARNING: raycastWithID does not support indexed vertices"); 151 | } else { 152 | var position = attributes.position; 153 | var j = elID * 3; 154 | a = j; 155 | b = j + 1; 156 | c = j + 2; 157 | vA.fromBufferAttribute(position, a); 158 | vB.fromBufferAttribute(position, b); 159 | vC.fromBufferAttribute(position, c); 160 | } 161 | var intersection = checkIntersection(this, raycaster, ray, vA, vB, vC, intersectionPoint); 162 | if (intersection === null) { 163 | console.log("WARNING: intersectionPoint missing"); 164 | return; 165 | } 166 | 167 | var face = new THREE.Face3(a, b, c); 168 | THREE.Triangle.getNormal(vA, vB, vC, face.normal); 169 | 170 | intersection.face = face; 171 | intersection.faceIndex = a; 172 | return intersection; 173 | }; 174 | 175 | }()); 176 | 177 | THREE.Line.prototype.raycastWithID = (function () { 178 | var inverseMatrix = new THREE.Matrix4(); 179 | var ray = new THREE.Ray(); 180 | 181 | var vStart = new THREE.Vector3(); 182 | var vEnd = new THREE.Vector3(); 183 | var interSegment = new THREE.Vector3(); 184 | var interRay = new THREE.Vector3(); 185 | return function (elID, raycaster) { 186 | inverseMatrix.getInverse(this.matrixWorld); 187 | ray.copy(raycaster.ray).applyMatrix4(inverseMatrix); 188 | var geometry = this.geometry; 189 | if (geometry instanceof THREE.BufferGeometry) { 190 | 191 | var attributes = geometry.attributes; 192 | 193 | if (geometry.index !== null) { 194 | console.log("WARNING: raycastWithID does not support indexed vertices"); 195 | } else { 196 | 197 | var positions = attributes.position.array; 198 | var i = elID * 6; 199 | vStart.fromArray(positions, i); 200 | vEnd.fromArray(positions, i + 3); 201 | 202 | var distSq = ray.distanceSqToSegment(vStart, vEnd, interRay, interSegment); 203 | var distance = ray.origin.distanceTo(interRay); 204 | 205 | if (distance < raycaster.near || distance > raycaster.far) return; 206 | 207 | var intersect = { 208 | 209 | distance: distance, 210 | // What do we want? intersection point on the ray or on the segment?? 211 | // point: raycaster.ray.at( distance ), 212 | point: interSegment.clone().applyMatrix4(this.matrixWorld), 213 | index: i, 214 | face: null, 215 | faceIndex: null, 216 | object: this 217 | 218 | }; 219 | return intersect; 220 | 221 | } 222 | 223 | } 224 | }; 225 | 226 | })(); 227 | 228 | THREE.Points.prototype.raycastWithID = (function () { 229 | 230 | var inverseMatrix = new THREE.Matrix4(); 231 | var ray = new THREE.Ray(); 232 | 233 | return function (elID, raycaster) { 234 | var object = this; 235 | var geometry = object.geometry; 236 | 237 | inverseMatrix.getInverse(this.matrixWorld); 238 | ray.copy(raycaster.ray).applyMatrix4(inverseMatrix); 239 | var position = new THREE.Vector3(); 240 | 241 | var testPoint = function (point, index) { 242 | var rayPointDistance = ray.distanceToPoint(point); 243 | var intersectPoint = ray.closestPointToPoint(point); 244 | intersectPoint.applyMatrix4(object.matrixWorld); 245 | 246 | var distance = raycaster.ray.origin.distanceTo(intersectPoint); 247 | 248 | if (distance < raycaster.near || distance > raycaster.far) return; 249 | 250 | var intersect = { 251 | 252 | distance: distance, 253 | distanceToRay: rayPointDistance, 254 | point: intersectPoint.clone(), 255 | index: index, 256 | face: null, 257 | object: object 258 | 259 | }; 260 | return intersect; 261 | }; 262 | var attributes = geometry.attributes; 263 | var positions = attributes.position.array; 264 | position.fromArray(positions, elID * 3); 265 | 266 | return testPoint(position, elID); 267 | 268 | }; 269 | 270 | }()); 271 | 272 | THREE.GPUPicker = function (option) { 273 | if (option === undefined) { 274 | option = {}; 275 | } 276 | this.pickingScene = new THREE.Scene(); 277 | this.pickingTexture = new THREE.WebGLRenderTarget(); 278 | this.pickingTexture.texture.minFilter = THREE.LinearFilter; 279 | this.pickingTexture.texture.generateMipmaps = false; 280 | this.lineShell = option.lineShell !== undefined ? option.lineShell : 4; 281 | this.pointShell = option.pointShell !== undefined ? option.pointShell : 0.1; 282 | this.debug = option.debug !== undefined ? option.debug : false; 283 | this.needUpdate = true; 284 | if (option.renderer) { 285 | this.setRenderer(option.renderer); 286 | } 287 | 288 | // array of original objects 289 | this.container = []; 290 | this.objectsMap = {}; 291 | //default filter 292 | this.setFilter(); 293 | }; 294 | THREE.GPUPicker.prototype.setRenderer = function (renderer) { 295 | this.renderer = renderer; 296 | var size = renderer.getSize(); 297 | this.resizeTexture(size.width, size.height); 298 | this.needUpdate = true; 299 | }; 300 | THREE.GPUPicker.prototype.resizeTexture = function (width, height) { 301 | this.pickingTexture.setSize(width, height); 302 | this.pixelBuffer = new Uint8Array(4 * width * height); 303 | this.needUpdate = true; 304 | }; 305 | THREE.GPUPicker.prototype.setCamera = function (camera) { 306 | this.camera = camera; 307 | this.needUpdate = true; 308 | }; 309 | THREE.GPUPicker.prototype.update = function () { 310 | if (this.needUpdate) { 311 | this.renderer.render(this.pickingScene, this.camera, this.pickingTexture); 312 | //read the rendering texture 313 | this.renderer.readRenderTargetPixels(this.pickingTexture, 0, 0, this.pickingTexture.width, this.pickingTexture.height, this.pixelBuffer); 314 | this.needUpdate = false; 315 | if (this.debug) console.log("GPUPicker rendering updated"); 316 | } 317 | }; 318 | THREE.GPUPicker.prototype.setFilter = function (func) { 319 | if (func instanceof Function) { 320 | this.filterFunc = func; 321 | } else { 322 | //default filter 323 | this.filterFunc = function (object) { 324 | return true; 325 | }; 326 | } 327 | 328 | }; 329 | THREE.GPUPicker.prototype.setScene = function (scene) { 330 | this.pickingScene = scene.clone(); 331 | this._processObject(this.pickingScene, 0); 332 | this.needUpdate = true; 333 | }; 334 | 335 | 336 | THREE.GPUPicker.prototype.pick = function (mouse, raycaster) { 337 | this.update(); 338 | var index = mouse.x + (this.pickingTexture.height - mouse.y) * this.pickingTexture.width; 339 | //interpret the pixel as an ID 340 | var id = (this.pixelBuffer[index * 4 + 2] * 255 * 255) + (this.pixelBuffer[index * 4 + 1] * 255) + (this.pixelBuffer[index * 4 + 0]); 341 | // get object with this id in range 342 | // var object = this._getObject(id); 343 | if (this.debug) console.log("pick id:", id); 344 | var result = this._getObject(this.pickingScene, 0, id); 345 | var object = result[1]; 346 | var elementId = id - result[0]; 347 | if (object) { 348 | if (object.raycastWithID) { 349 | var intersect = object.raycastWithID(elementId, raycaster); 350 | intersect.object = object.originalObject; 351 | return intersect; 352 | } 353 | 354 | } 355 | return; 356 | }; 357 | 358 | /* 359 | * get object by id 360 | */ 361 | THREE.GPUPicker.prototype._getObject = function (object, baseId, id) { 362 | // if (this.debug) console.log("_getObject ",baseId); 363 | if (object.elementsCount !== undefined && id >= baseId && id < baseId + object.elementsCount) { 364 | return [baseId, object]; 365 | } 366 | if (object.elementsCount !== undefined) { 367 | baseId += object.elementsCount; 368 | } 369 | var result = [baseId, undefined]; 370 | for (var i = 0; i < object.children.length; i++) { 371 | result = this._getObject(object.children[i], result[0], id); 372 | if (result[1] !== undefined) 373 | break; 374 | } 375 | return result; 376 | }; 377 | 378 | /* 379 | * process the object to add elementId information 380 | */ 381 | THREE.GPUPicker.prototype._processObject = function (object, baseId) { 382 | baseId += this._addElementID(object, baseId); 383 | for (var i = 0; i < object.children.length; i++) { 384 | baseId = this._processObject(object.children[i], baseId); 385 | 386 | } 387 | return baseId; 388 | }; 389 | 390 | THREE.GPUPicker.prototype._addElementID = function (object, baseId) { 391 | if (!this.filterFunc(object) && object.geometry !== undefined) { 392 | object.visible = false; 393 | return 0; 394 | } 395 | 396 | if (object.geometry) { 397 | var __pickingGeometry; 398 | //check if geometry has cached geometry for picking 399 | if (object.geometry.__pickingGeometry) { 400 | __pickingGeometry = object.geometry.__pickingGeometry; 401 | } else { 402 | __pickingGeometry = object.geometry; 403 | // convert geometry to buffer geometry 404 | if (object.geometry instanceof THREE.Geometry) { 405 | if (this.debug) console.log("convert geometry to buffer geometry"); 406 | __pickingGeometry = new THREE.BufferGeometry().setFromObject(object); 407 | } 408 | var units = 1; 409 | if (object instanceof THREE.Points) { 410 | units = 1; 411 | } else if (object instanceof THREE.Line) { 412 | units = 2; 413 | } else if (object instanceof THREE.Mesh) { 414 | units = 3; 415 | } 416 | var el, el3, elementsCount, i, indices, positionBuffer, vertex3, verts, vertexIndex3; 417 | if (__pickingGeometry.index !== null) { 418 | __pickingGeometry = __pickingGeometry.clone(); 419 | if (this.debug) console.log("convert indexed geometry to non-indexed geometry"); 420 | 421 | indices = __pickingGeometry.index.array; 422 | verts = __pickingGeometry.attributes.position.array; 423 | delete __pickingGeometry.attributes.position; 424 | __pickingGeometry.index = null; 425 | delete __pickingGeometry.attributes.normal; 426 | elementsCount = indices.length / units; 427 | positionBuffer = new Float32Array(elementsCount * 3 * units); 428 | 429 | __pickingGeometry.addAttribute('position', new THREE.BufferAttribute(positionBuffer, 3)); 430 | for (el = 0; el < elementsCount; ++el) { 431 | el3 = units * el; 432 | for (i = 0; i < units; ++i) { 433 | vertexIndex3 = 3 * indices[el3 + i]; 434 | vertex3 = 3 * (el3 + i); 435 | positionBuffer[vertex3] = verts[vertexIndex3]; 436 | positionBuffer[vertex3 + 1] = verts[vertexIndex3 + 1]; 437 | positionBuffer[vertex3 + 2] = verts[vertexIndex3 + 2]; 438 | } 439 | } 440 | 441 | __pickingGeometry.computeVertexNormals(); 442 | } 443 | if (object instanceof THREE.Line && !(object instanceof THREE.LineSegments)) { 444 | if (this.debug) console.log("convert Line to LineSegments"); 445 | verts = __pickingGeometry.attributes.position.array; 446 | delete __pickingGeometry.attributes.position; 447 | elementsCount = verts.length / 3 - 1; 448 | positionBuffer = new Float32Array(elementsCount * units * 3); 449 | 450 | __pickingGeometry.addAttribute('position', new THREE.BufferAttribute(positionBuffer, 3)); 451 | for (el = 0; el < elementsCount; ++el) { 452 | el3 = 3 * el; 453 | vertexIndex3 = el3; 454 | vertex3 = el3 * 2; 455 | positionBuffer[vertex3] = verts[vertexIndex3]; 456 | positionBuffer[vertex3 + 1] = verts[vertexIndex3 + 1]; 457 | positionBuffer[vertex3 + 2] = verts[vertexIndex3 + 2]; 458 | positionBuffer[vertex3 + 3] = verts[vertexIndex3 + 3]; 459 | positionBuffer[vertex3 + 4] = verts[vertexIndex3 + 4]; 460 | positionBuffer[vertex3 + 5] = verts[vertexIndex3 + 5]; 461 | 462 | } 463 | 464 | __pickingGeometry.computeVertexNormals(); 465 | object.__proto__ = THREE.LineSegments.prototype; //make the renderer render as line segments 466 | } 467 | var attributes = __pickingGeometry.attributes; 468 | var positions = attributes.position.array; 469 | var vertexCount = positions.length / 3; 470 | var ids = new THREE.Float32BufferAttribute(vertexCount, 1); 471 | //set vertex id color 472 | 473 | for (var i = 0, il = vertexCount / units; i < il; i++) { 474 | for (var j = 0; j < units; ++j) { 475 | ids.array[i * units + j] = i; 476 | } 477 | } 478 | __pickingGeometry.addAttribute('id', ids); 479 | __pickingGeometry.elementsCount = vertexCount / units; 480 | //cache __pickingGeometry inside geometry 481 | object.geometry.__pickingGeometry = __pickingGeometry; 482 | } 483 | 484 | //use __pickingGeometry in the picking mesh 485 | object.geometry = __pickingGeometry; 486 | object.elementsCount = __pickingGeometry.elementsCount;//elements count 487 | 488 | var pointSize = object.material.size || 0.01; 489 | var linewidth = object.material.linewidth || 1; 490 | object.material = new FaceIDMaterial(); 491 | object.material.linewidth = linewidth + this.lineShell;//make the line a little wider to hit 492 | object.material.setBaseID(baseId); 493 | object.material.setPointSize(pointSize + this.pointShell);//make the point a little wider to hit 494 | object.material.setPointScale(this.renderer.getSize().height * this.renderer.getPixelRatio() / 2); 495 | return object.elementsCount; 496 | } 497 | return 0; 498 | }; 499 | }(THREE)); 500 | --------------------------------------------------------------------------------