├── .gitignore ├── README.md ├── assets ├── buddha.obj └── teapot.obj ├── libs ├── OBJLoader.js ├── OrbitControls.js ├── bvhtree.js └── three.js ├── main.html ├── pres └── pres PRT.pdf └── src ├── main.js └── sh.js /.gitignore: -------------------------------------------------------------------------------- 1 | prt_precomputed.json 2 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PRT 2 | Precomputed Radiance Transfert (PRT) 3 | 4 | Demo for the computer graphics paper discussion group at McGill University. 5 | 6 | Based on: 7 | Precomputed Radiance Transfer for Real-Time Rendering in Dynamic, Low-Frequency Lighting Environments 8 | http://www.ppsloan.org/publications/shillum_final23.pdf 9 | 10 | ## Interface 11 |  12 | 13 | ## Usage 14 | Run using: 15 | 16 | ### Windows 17 | `chrome --allow-file-access-from-files` 18 | 19 | ### MacOS 20 | `/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --allow-file-access-from-files` 21 | -------------------------------------------------------------------------------- /libs/OBJLoader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author mrdoob / http://mrdoob.com/ 3 | */ 4 | 5 | THREE.OBJLoader = function ( manager ) { 6 | 7 | this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager; 8 | 9 | this.materials = null; 10 | 11 | this.regexp = { 12 | // v float float float 13 | vertex_pattern : /^v\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/, 14 | // vn float float float 15 | normal_pattern : /^vn\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/, 16 | // vt float float 17 | uv_pattern : /^vt\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/, 18 | // f vertex vertex vertex 19 | face_vertex : /^f\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)(?:\s+(-?\d+))?/, 20 | // f vertex/uv vertex/uv vertex/uv 21 | face_vertex_uv : /^f\s+(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)(?:\s+(-?\d+)\/(-?\d+))?/, 22 | // f vertex/uv/normal vertex/uv/normal vertex/uv/normal 23 | face_vertex_uv_normal : /^f\s+(-?\d+)\/(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\/(-?\d+)(?:\s+(-?\d+)\/(-?\d+)\/(-?\d+))?/, 24 | // f vertex//normal vertex//normal vertex//normal 25 | face_vertex_normal : /^f\s+(-?\d+)\/\/(-?\d+)\s+(-?\d+)\/\/(-?\d+)\s+(-?\d+)\/\/(-?\d+)(?:\s+(-?\d+)\/\/(-?\d+))?/, 26 | // o object_name | g group_name 27 | object_pattern : /^[og]\s*(.+)?/, 28 | // s boolean 29 | smoothing_pattern : /^s\s+(\d+|on|off)/, 30 | // mtllib file_reference 31 | material_library_pattern : /^mtllib /, 32 | // usemtl material_name 33 | material_use_pattern : /^usemtl / 34 | }; 35 | 36 | }; 37 | 38 | THREE.OBJLoader.prototype = { 39 | 40 | constructor: THREE.OBJLoader, 41 | 42 | load: function ( url, onLoad, onProgress, onError ) { 43 | 44 | var scope = this; 45 | 46 | var loader = new THREE.FileLoader( scope.manager ); 47 | loader.setPath( this.path ); 48 | loader.load( url, function ( text ) { 49 | 50 | onLoad( scope.parse( text ) ); 51 | 52 | }, onProgress, onError ); 53 | 54 | }, 55 | 56 | setPath: function ( value ) { 57 | 58 | this.path = value; 59 | 60 | }, 61 | 62 | setMaterials: function ( materials ) { 63 | 64 | this.materials = materials; 65 | 66 | }, 67 | 68 | _createParserState : function () { 69 | 70 | var state = { 71 | objects : [], 72 | object : {}, 73 | 74 | vertices : [], 75 | normals : [], 76 | uvs : [], 77 | 78 | materialLibraries : [], 79 | 80 | startObject: function ( name, fromDeclaration ) { 81 | 82 | // If the current object (initial from reset) is not from a g/o declaration in the parsed 83 | // file. We need to use it for the first parsed g/o to keep things in sync. 84 | if ( this.object && this.object.fromDeclaration === false ) { 85 | 86 | this.object.name = name; 87 | this.object.fromDeclaration = ( fromDeclaration !== false ); 88 | return; 89 | 90 | } 91 | 92 | var previousMaterial = ( this.object && typeof this.object.currentMaterial === 'function' ? this.object.currentMaterial() : undefined ); 93 | 94 | if ( this.object && typeof this.object._finalize === 'function' ) { 95 | 96 | this.object._finalize( true ); 97 | 98 | } 99 | 100 | this.object = { 101 | name : name || '', 102 | fromDeclaration : ( fromDeclaration !== false ), 103 | 104 | geometry : { 105 | vertices : [], 106 | normals : [], 107 | uvs : [] 108 | }, 109 | materials : [], 110 | smooth : true, 111 | 112 | startMaterial : function( name, libraries ) { 113 | 114 | var previous = this._finalize( false ); 115 | 116 | // New usemtl declaration overwrites an inherited material, except if faces were declared 117 | // after the material, then it must be preserved for proper MultiMaterial continuation. 118 | if ( previous && ( previous.inherited || previous.groupCount <= 0 ) ) { 119 | 120 | this.materials.splice( previous.index, 1 ); 121 | 122 | } 123 | 124 | var material = { 125 | index : this.materials.length, 126 | name : name || '', 127 | mtllib : ( Array.isArray( libraries ) && libraries.length > 0 ? libraries[ libraries.length - 1 ] : '' ), 128 | smooth : ( previous !== undefined ? previous.smooth : this.smooth ), 129 | groupStart : ( previous !== undefined ? previous.groupEnd : 0 ), 130 | groupEnd : -1, 131 | groupCount : -1, 132 | inherited : false, 133 | 134 | clone : function( index ) { 135 | var cloned = { 136 | index : ( typeof index === 'number' ? index : this.index ), 137 | name : this.name, 138 | mtllib : this.mtllib, 139 | smooth : this.smooth, 140 | groupStart : 0, 141 | groupEnd : -1, 142 | groupCount : -1, 143 | inherited : false 144 | }; 145 | cloned.clone = this.clone.bind(cloned); 146 | return cloned; 147 | } 148 | }; 149 | 150 | this.materials.push( material ); 151 | 152 | return material; 153 | 154 | }, 155 | 156 | currentMaterial : function() { 157 | 158 | if ( this.materials.length > 0 ) { 159 | return this.materials[ this.materials.length - 1 ]; 160 | } 161 | 162 | return undefined; 163 | 164 | }, 165 | 166 | _finalize : function( end ) { 167 | 168 | var lastMultiMaterial = this.currentMaterial(); 169 | if ( lastMultiMaterial && lastMultiMaterial.groupEnd === -1 ) { 170 | 171 | lastMultiMaterial.groupEnd = this.geometry.vertices.length / 3; 172 | lastMultiMaterial.groupCount = lastMultiMaterial.groupEnd - lastMultiMaterial.groupStart; 173 | lastMultiMaterial.inherited = false; 174 | 175 | } 176 | 177 | // Ignore objects tail materials if no face declarations followed them before a new o/g started. 178 | if ( end && this.materials.length > 1 ) { 179 | 180 | for ( var mi = this.materials.length - 1; mi >= 0; mi-- ) { 181 | if ( this.materials[mi].groupCount <= 0 ) { 182 | this.materials.splice( mi, 1 ); 183 | } 184 | } 185 | 186 | } 187 | 188 | // Guarantee at least one empty material, this makes the creation later more straight forward. 189 | if ( end && this.materials.length === 0 ) { 190 | 191 | this.materials.push({ 192 | name : '', 193 | smooth : this.smooth 194 | }); 195 | 196 | } 197 | 198 | return lastMultiMaterial; 199 | 200 | } 201 | }; 202 | 203 | // Inherit previous objects material. 204 | // Spec tells us that a declared material must be set to all objects until a new material is declared. 205 | // If a usemtl declaration is encountered while this new object is being parsed, it will 206 | // overwrite the inherited material. Exception being that there was already face declarations 207 | // to the inherited material, then it will be preserved for proper MultiMaterial continuation. 208 | 209 | if ( previousMaterial && previousMaterial.name && typeof previousMaterial.clone === "function" ) { 210 | 211 | var declared = previousMaterial.clone( 0 ); 212 | declared.inherited = true; 213 | this.object.materials.push( declared ); 214 | 215 | } 216 | 217 | this.objects.push( this.object ); 218 | 219 | }, 220 | 221 | finalize : function() { 222 | 223 | if ( this.object && typeof this.object._finalize === 'function' ) { 224 | 225 | this.object._finalize( true ); 226 | 227 | } 228 | 229 | }, 230 | 231 | parseVertexIndex: function ( value, len ) { 232 | 233 | var index = parseInt( value, 10 ); 234 | return ( index >= 0 ? index - 1 : index + len / 3 ) * 3; 235 | 236 | }, 237 | 238 | parseNormalIndex: function ( value, len ) { 239 | 240 | var index = parseInt( value, 10 ); 241 | return ( index >= 0 ? index - 1 : index + len / 3 ) * 3; 242 | 243 | }, 244 | 245 | parseUVIndex: function ( value, len ) { 246 | 247 | var index = parseInt( value, 10 ); 248 | return ( index >= 0 ? index - 1 : index + len / 2 ) * 2; 249 | 250 | }, 251 | 252 | addVertex: function ( a, b, c ) { 253 | 254 | var src = this.vertices; 255 | var dst = this.object.geometry.vertices; 256 | 257 | dst.push( src[ a + 0 ] ); 258 | dst.push( src[ a + 1 ] ); 259 | dst.push( src[ a + 2 ] ); 260 | dst.push( src[ b + 0 ] ); 261 | dst.push( src[ b + 1 ] ); 262 | dst.push( src[ b + 2 ] ); 263 | dst.push( src[ c + 0 ] ); 264 | dst.push( src[ c + 1 ] ); 265 | dst.push( src[ c + 2 ] ); 266 | 267 | }, 268 | 269 | addVertexLine: function ( a ) { 270 | 271 | var src = this.vertices; 272 | var dst = this.object.geometry.vertices; 273 | 274 | dst.push( src[ a + 0 ] ); 275 | dst.push( src[ a + 1 ] ); 276 | dst.push( src[ a + 2 ] ); 277 | 278 | }, 279 | 280 | addNormal : function ( a, b, c ) { 281 | 282 | var src = this.normals; 283 | var dst = this.object.geometry.normals; 284 | 285 | dst.push( src[ a + 0 ] ); 286 | dst.push( src[ a + 1 ] ); 287 | dst.push( src[ a + 2 ] ); 288 | dst.push( src[ b + 0 ] ); 289 | dst.push( src[ b + 1 ] ); 290 | dst.push( src[ b + 2 ] ); 291 | dst.push( src[ c + 0 ] ); 292 | dst.push( src[ c + 1 ] ); 293 | dst.push( src[ c + 2 ] ); 294 | 295 | }, 296 | 297 | addUV: function ( a, b, c ) { 298 | 299 | var src = this.uvs; 300 | var dst = this.object.geometry.uvs; 301 | 302 | dst.push( src[ a + 0 ] ); 303 | dst.push( src[ a + 1 ] ); 304 | dst.push( src[ b + 0 ] ); 305 | dst.push( src[ b + 1 ] ); 306 | dst.push( src[ c + 0 ] ); 307 | dst.push( src[ c + 1 ] ); 308 | 309 | }, 310 | 311 | addUVLine: function ( a ) { 312 | 313 | var src = this.uvs; 314 | var dst = this.object.geometry.uvs; 315 | 316 | dst.push( src[ a + 0 ] ); 317 | dst.push( src[ a + 1 ] ); 318 | 319 | }, 320 | 321 | addFace: function ( a, b, c, d, ua, ub, uc, ud, na, nb, nc, nd ) { 322 | 323 | var vLen = this.vertices.length; 324 | 325 | var ia = this.parseVertexIndex( a, vLen ); 326 | var ib = this.parseVertexIndex( b, vLen ); 327 | var ic = this.parseVertexIndex( c, vLen ); 328 | var id; 329 | 330 | if ( d === undefined ) { 331 | 332 | this.addVertex( ia, ib, ic ); 333 | 334 | } else { 335 | 336 | id = this.parseVertexIndex( d, vLen ); 337 | 338 | this.addVertex( ia, ib, id ); 339 | this.addVertex( ib, ic, id ); 340 | 341 | } 342 | 343 | if ( ua !== undefined ) { 344 | 345 | var uvLen = this.uvs.length; 346 | 347 | ia = this.parseUVIndex( ua, uvLen ); 348 | ib = this.parseUVIndex( ub, uvLen ); 349 | ic = this.parseUVIndex( uc, uvLen ); 350 | 351 | if ( d === undefined ) { 352 | 353 | this.addUV( ia, ib, ic ); 354 | 355 | } else { 356 | 357 | id = this.parseUVIndex( ud, uvLen ); 358 | 359 | this.addUV( ia, ib, id ); 360 | this.addUV( ib, ic, id ); 361 | 362 | } 363 | 364 | } 365 | 366 | if ( na !== undefined ) { 367 | 368 | // Normals are many times the same. If so, skip function call and parseInt. 369 | var nLen = this.normals.length; 370 | ia = this.parseNormalIndex( na, nLen ); 371 | 372 | ib = na === nb ? ia : this.parseNormalIndex( nb, nLen ); 373 | ic = na === nc ? ia : this.parseNormalIndex( nc, nLen ); 374 | 375 | if ( d === undefined ) { 376 | 377 | this.addNormal( ia, ib, ic ); 378 | 379 | } else { 380 | 381 | id = this.parseNormalIndex( nd, nLen ); 382 | 383 | this.addNormal( ia, ib, id ); 384 | this.addNormal( ib, ic, id ); 385 | 386 | } 387 | 388 | } 389 | 390 | }, 391 | 392 | addLineGeometry: function ( vertices, uvs ) { 393 | 394 | this.object.geometry.type = 'Line'; 395 | 396 | var vLen = this.vertices.length; 397 | var uvLen = this.uvs.length; 398 | 399 | for ( var vi = 0, l = vertices.length; vi < l; vi ++ ) { 400 | 401 | this.addVertexLine( this.parseVertexIndex( vertices[ vi ], vLen ) ); 402 | 403 | } 404 | 405 | for ( var uvi = 0, l = uvs.length; uvi < l; uvi ++ ) { 406 | 407 | this.addUVLine( this.parseUVIndex( uvs[ uvi ], uvLen ) ); 408 | 409 | } 410 | 411 | } 412 | 413 | }; 414 | 415 | state.startObject( '', false ); 416 | 417 | return state; 418 | 419 | }, 420 | 421 | parse: function ( text ) { 422 | 423 | console.time( 'OBJLoader' ); 424 | 425 | var state = this._createParserState(); 426 | 427 | if ( text.indexOf( '\r\n' ) !== - 1 ) { 428 | 429 | // This is faster than String.split with regex that splits on both 430 | text = text.replace( /\r\n/g, '\n' ); 431 | 432 | } 433 | 434 | if ( text.indexOf( '\\\n' ) !== - 1) { 435 | 436 | // join lines separated by a line continuation character (\) 437 | text = text.replace( /\\\n/g, '' ); 438 | 439 | } 440 | 441 | var lines = text.split( '\n' ); 442 | var line = '', lineFirstChar = '', lineSecondChar = ''; 443 | var lineLength = 0; 444 | var result = []; 445 | 446 | // Faster to just trim left side of the line. Use if available. 447 | var trimLeft = ( typeof ''.trimLeft === 'function' ); 448 | 449 | for ( var i = 0, l = lines.length; i < l; i ++ ) { 450 | 451 | line = lines[ i ]; 452 | 453 | line = trimLeft ? line.trimLeft() : line.trim(); 454 | 455 | lineLength = line.length; 456 | 457 | if ( lineLength === 0 ) continue; 458 | 459 | lineFirstChar = line.charAt( 0 ); 460 | 461 | // @todo invoke passed in handler if any 462 | if ( lineFirstChar === '#' ) continue; 463 | 464 | if ( lineFirstChar === 'v' ) { 465 | 466 | lineSecondChar = line.charAt( 1 ); 467 | 468 | if ( lineSecondChar === ' ' && ( result = this.regexp.vertex_pattern.exec( line ) ) !== null ) { 469 | 470 | // 0 1 2 3 471 | // ["v 1.0 2.0 3.0", "1.0", "2.0", "3.0"] 472 | 473 | state.vertices.push( 474 | parseFloat( result[ 1 ] ), 475 | parseFloat( result[ 2 ] ), 476 | parseFloat( result[ 3 ] ) 477 | ); 478 | 479 | } else if ( lineSecondChar === 'n' && ( result = this.regexp.normal_pattern.exec( line ) ) !== null ) { 480 | 481 | // 0 1 2 3 482 | // ["vn 1.0 2.0 3.0", "1.0", "2.0", "3.0"] 483 | 484 | state.normals.push( 485 | parseFloat( result[ 1 ] ), 486 | parseFloat( result[ 2 ] ), 487 | parseFloat( result[ 3 ] ) 488 | ); 489 | 490 | } else if ( lineSecondChar === 't' && ( result = this.regexp.uv_pattern.exec( line ) ) !== null ) { 491 | 492 | // 0 1 2 493 | // ["vt 0.1 0.2", "0.1", "0.2"] 494 | 495 | state.uvs.push( 496 | parseFloat( result[ 1 ] ), 497 | parseFloat( result[ 2 ] ) 498 | ); 499 | 500 | } else { 501 | 502 | throw new Error( "Unexpected vertex/normal/uv line: '" + line + "'" ); 503 | 504 | } 505 | 506 | } else if ( lineFirstChar === "f" ) { 507 | 508 | if ( ( result = this.regexp.face_vertex_uv_normal.exec( line ) ) !== null ) { 509 | 510 | // f vertex/uv/normal vertex/uv/normal vertex/uv/normal 511 | // 0 1 2 3 4 5 6 7 8 9 10 11 12 512 | // ["f 1/1/1 2/2/2 3/3/3", "1", "1", "1", "2", "2", "2", "3", "3", "3", undefined, undefined, undefined] 513 | 514 | state.addFace( 515 | result[ 1 ], result[ 4 ], result[ 7 ], result[ 10 ], 516 | result[ 2 ], result[ 5 ], result[ 8 ], result[ 11 ], 517 | result[ 3 ], result[ 6 ], result[ 9 ], result[ 12 ] 518 | ); 519 | 520 | } else if ( ( result = this.regexp.face_vertex_uv.exec( line ) ) !== null ) { 521 | 522 | // f vertex/uv vertex/uv vertex/uv 523 | // 0 1 2 3 4 5 6 7 8 524 | // ["f 1/1 2/2 3/3", "1", "1", "2", "2", "3", "3", undefined, undefined] 525 | 526 | state.addFace( 527 | result[ 1 ], result[ 3 ], result[ 5 ], result[ 7 ], 528 | result[ 2 ], result[ 4 ], result[ 6 ], result[ 8 ] 529 | ); 530 | 531 | } else if ( ( result = this.regexp.face_vertex_normal.exec( line ) ) !== null ) { 532 | 533 | // f vertex//normal vertex//normal vertex//normal 534 | // 0 1 2 3 4 5 6 7 8 535 | // ["f 1//1 2//2 3//3", "1", "1", "2", "2", "3", "3", undefined, undefined] 536 | 537 | state.addFace( 538 | result[ 1 ], result[ 3 ], result[ 5 ], result[ 7 ], 539 | undefined, undefined, undefined, undefined, 540 | result[ 2 ], result[ 4 ], result[ 6 ], result[ 8 ] 541 | ); 542 | 543 | } else if ( ( result = this.regexp.face_vertex.exec( line ) ) !== null ) { 544 | 545 | // f vertex vertex vertex 546 | // 0 1 2 3 4 547 | // ["f 1 2 3", "1", "2", "3", undefined] 548 | 549 | state.addFace( 550 | result[ 1 ], result[ 2 ], result[ 3 ], result[ 4 ] 551 | ); 552 | 553 | } else { 554 | 555 | throw new Error( "Unexpected face line: '" + line + "'" ); 556 | 557 | } 558 | 559 | } else if ( lineFirstChar === "l" ) { 560 | 561 | var lineParts = line.substring( 1 ).trim().split( " " ); 562 | var lineVertices = [], lineUVs = []; 563 | 564 | if ( line.indexOf( "/" ) === - 1 ) { 565 | 566 | lineVertices = lineParts; 567 | 568 | } else { 569 | 570 | for ( var li = 0, llen = lineParts.length; li < llen; li ++ ) { 571 | 572 | var parts = lineParts[ li ].split( "/" ); 573 | 574 | if ( parts[ 0 ] !== "" ) lineVertices.push( parts[ 0 ] ); 575 | if ( parts[ 1 ] !== "" ) lineUVs.push( parts[ 1 ] ); 576 | 577 | } 578 | 579 | } 580 | state.addLineGeometry( lineVertices, lineUVs ); 581 | 582 | } else if ( ( result = this.regexp.object_pattern.exec( line ) ) !== null ) { 583 | 584 | // o object_name 585 | // or 586 | // g group_name 587 | 588 | // WORKAROUND: https://bugs.chromium.org/p/v8/issues/detail?id=2869 589 | // var name = result[ 0 ].substr( 1 ).trim(); 590 | var name = ( " " + result[ 0 ].substr( 1 ).trim() ).substr( 1 ); 591 | 592 | state.startObject( name ); 593 | 594 | } else if ( this.regexp.material_use_pattern.test( line ) ) { 595 | 596 | // material 597 | 598 | state.object.startMaterial( line.substring( 7 ).trim(), state.materialLibraries ); 599 | 600 | } else if ( this.regexp.material_library_pattern.test( line ) ) { 601 | 602 | // mtl file 603 | 604 | state.materialLibraries.push( line.substring( 7 ).trim() ); 605 | 606 | } else if ( ( result = this.regexp.smoothing_pattern.exec( line ) ) !== null ) { 607 | 608 | // smooth shading 609 | 610 | // @todo Handle files that have varying smooth values for a set of faces inside one geometry, 611 | // but does not define a usemtl for each face set. 612 | // This should be detected and a dummy material created (later MultiMaterial and geometry groups). 613 | // This requires some care to not create extra material on each smooth value for "normal" obj files. 614 | // where explicit usemtl defines geometry groups. 615 | // Example asset: examples/models/obj/cerberus/Cerberus.obj 616 | 617 | var value = result[ 1 ].trim().toLowerCase(); 618 | state.object.smooth = ( value === '1' || value === 'on' ); 619 | 620 | var material = state.object.currentMaterial(); 621 | if ( material ) { 622 | 623 | material.smooth = state.object.smooth; 624 | 625 | } 626 | 627 | } else { 628 | 629 | // Handle null terminated files without exception 630 | if ( line === '\0' ) continue; 631 | 632 | throw new Error( "Unexpected line: '" + line + "'" ); 633 | 634 | } 635 | 636 | } 637 | 638 | state.finalize(); 639 | 640 | var container = new THREE.Group(); 641 | container.materialLibraries = [].concat( state.materialLibraries ); 642 | 643 | for ( var i = 0, l = state.objects.length; i < l; i ++ ) { 644 | 645 | var object = state.objects[ i ]; 646 | var geometry = object.geometry; 647 | var materials = object.materials; 648 | var isLine = ( geometry.type === 'Line' ); 649 | 650 | // Skip o/g line declarations that did not follow with any faces 651 | if ( geometry.vertices.length === 0 ) continue; 652 | 653 | var buffergeometry = new THREE.BufferGeometry(); 654 | 655 | buffergeometry.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( geometry.vertices ), 3 ) ); 656 | 657 | if ( geometry.normals.length > 0 ) { 658 | 659 | buffergeometry.addAttribute( 'normal', new THREE.BufferAttribute( new Float32Array( geometry.normals ), 3 ) ); 660 | 661 | } else { 662 | 663 | buffergeometry.computeVertexNormals(); 664 | 665 | } 666 | 667 | if ( geometry.uvs.length > 0 ) { 668 | 669 | buffergeometry.addAttribute( 'uv', new THREE.BufferAttribute( new Float32Array( geometry.uvs ), 2 ) ); 670 | 671 | } 672 | 673 | // Create materials 674 | 675 | var createdMaterials = []; 676 | 677 | for ( var mi = 0, miLen = materials.length; mi < miLen ; mi++ ) { 678 | 679 | var sourceMaterial = materials[mi]; 680 | var material = undefined; 681 | 682 | if ( this.materials !== null ) { 683 | 684 | material = this.materials.create( sourceMaterial.name ); 685 | 686 | // mtl etc. loaders probably can't create line materials correctly, copy properties to a line material. 687 | if ( isLine && material && ! ( material instanceof THREE.LineBasicMaterial ) ) { 688 | 689 | var materialLine = new THREE.LineBasicMaterial(); 690 | materialLine.copy( material ); 691 | material = materialLine; 692 | 693 | } 694 | 695 | } 696 | 697 | if ( ! material ) { 698 | 699 | material = ( ! isLine ? new THREE.MeshPhongMaterial() : new THREE.LineBasicMaterial() ); 700 | material.name = sourceMaterial.name; 701 | 702 | } 703 | 704 | material.shading = sourceMaterial.smooth ? THREE.SmoothShading : THREE.FlatShading; 705 | 706 | createdMaterials.push(material); 707 | 708 | } 709 | 710 | // Create mesh 711 | 712 | var mesh; 713 | 714 | if ( createdMaterials.length > 1 ) { 715 | 716 | for ( var mi = 0, miLen = materials.length; mi < miLen ; mi++ ) { 717 | 718 | var sourceMaterial = materials[mi]; 719 | buffergeometry.addGroup( sourceMaterial.groupStart, sourceMaterial.groupCount, mi ); 720 | 721 | } 722 | 723 | mesh = ( ! isLine ? new THREE.Mesh( buffergeometry, createdMaterials ) : new THREE.LineSegments( buffergeometry, createdMaterials ) ); 724 | 725 | } else { 726 | 727 | mesh = ( ! isLine ? new THREE.Mesh( buffergeometry, createdMaterials[ 0 ] ) : new THREE.LineSegments( buffergeometry, createdMaterials[ 0 ] ) ); 728 | } 729 | 730 | mesh.name = object.name; 731 | 732 | container.add( mesh ); 733 | 734 | } 735 | 736 | console.timeEnd( 'OBJLoader' ); 737 | 738 | return container; 739 | 740 | } 741 | 742 | }; 743 | -------------------------------------------------------------------------------- /libs/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 | 9 | // This set of controls performs orbiting, dollying (zooming), and panning. 10 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 11 | // 12 | // Orbit - left mouse / touch: one finger move 13 | // Zoom - middle mouse, or mousewheel / touch: two finger spread or squish 14 | // Pan - right mouse, or arrow keys / touch: three finger swipe 15 | 16 | THREE.OrbitControls = function ( object, domElement ) { 17 | 18 | this.object = object; 19 | 20 | this.domElement = ( domElement !== undefined ) ? domElement : document; 21 | 22 | // Set to false to disable this control 23 | this.enabled = true; 24 | 25 | // "target" sets the location of focus, where the object orbits around 26 | this.target = new THREE.Vector3(); 27 | 28 | // How far you can dolly in and out ( PerspectiveCamera only ) 29 | this.minDistance = 0; 30 | this.maxDistance = Infinity; 31 | 32 | // How far you can zoom in and out ( OrthographicCamera only ) 33 | this.minZoom = 0; 34 | this.maxZoom = Infinity; 35 | 36 | // How far you can orbit vertically, upper and lower limits. 37 | // Range is 0 to Math.PI radians. 38 | this.minPolarAngle = 0; // radians 39 | this.maxPolarAngle = Math.PI; // radians 40 | 41 | // How far you can orbit horizontally, upper and lower limits. 42 | // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. 43 | this.minAzimuthAngle = - Infinity; // radians 44 | this.maxAzimuthAngle = Infinity; // radians 45 | 46 | // Set to true to enable damping (inertia) 47 | // If damping is enabled, you must call controls.update() in your animation loop 48 | this.enableDamping = false; 49 | this.dampingFactor = 0.25; 50 | 51 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 52 | // Set to false to disable zooming 53 | this.enableZoom = true; 54 | this.zoomSpeed = 1.0; 55 | 56 | // Set to false to disable rotating 57 | this.enableRotate = true; 58 | this.rotateSpeed = 1.0; 59 | 60 | // Set to false to disable panning 61 | this.enablePan = true; 62 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 63 | 64 | // Set to true to automatically rotate around the target 65 | // If auto-rotate is enabled, you must call controls.update() in your animation loop 66 | this.autoRotate = false; 67 | this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 68 | 69 | // Set to false to disable use of the keys 70 | this.enableKeys = true; 71 | 72 | // The four arrow keys 73 | this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; 74 | 75 | // Mouse buttons 76 | this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT }; 77 | 78 | // for reset 79 | this.target0 = this.target.clone(); 80 | this.position0 = this.object.position.clone(); 81 | this.zoom0 = this.object.zoom; 82 | 83 | // 84 | // public methods 85 | // 86 | 87 | this.getPolarAngle = function () { 88 | 89 | return spherical.phi; 90 | 91 | }; 92 | 93 | this.getAzimuthalAngle = function () { 94 | 95 | return spherical.theta; 96 | 97 | }; 98 | 99 | this.saveState = function () { 100 | 101 | scope.target0.copy( scope.target ); 102 | scope.position0.copy( scope.object.position ); 103 | scope.zoom0 = scope.object.zoom; 104 | 105 | }; 106 | 107 | this.reset = function () { 108 | 109 | scope.target.copy( scope.target0 ); 110 | scope.object.position.copy( scope.position0 ); 111 | scope.object.zoom = scope.zoom0; 112 | 113 | scope.object.updateProjectionMatrix(); 114 | scope.dispatchEvent( changeEvent ); 115 | 116 | scope.update(); 117 | 118 | state = STATE.NONE; 119 | 120 | }; 121 | 122 | // this method is exposed, but perhaps it would be better if we can make it private... 123 | this.update = function () { 124 | 125 | var offset = new THREE.Vector3(); 126 | 127 | // so camera.up is the orbit axis 128 | var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) ); 129 | var quatInverse = quat.clone().inverse(); 130 | 131 | var lastPosition = new THREE.Vector3(); 132 | var lastQuaternion = new THREE.Quaternion(); 133 | 134 | return function update() { 135 | 136 | var position = scope.object.position; 137 | 138 | offset.copy( position ).sub( scope.target ); 139 | 140 | // rotate offset to "y-axis-is-up" space 141 | offset.applyQuaternion( quat ); 142 | 143 | // angle from z-axis around y-axis 144 | spherical.setFromVector3( offset ); 145 | 146 | if ( scope.autoRotate && state === STATE.NONE ) { 147 | 148 | rotateLeft( getAutoRotationAngle() ); 149 | 150 | } 151 | 152 | spherical.theta += sphericalDelta.theta; 153 | spherical.phi += sphericalDelta.phi; 154 | 155 | // restrict theta to be between desired limits 156 | spherical.theta = Math.max( scope.minAzimuthAngle, Math.min( scope.maxAzimuthAngle, spherical.theta ) ); 157 | 158 | // restrict phi to be between desired limits 159 | spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); 160 | 161 | spherical.makeSafe(); 162 | 163 | 164 | spherical.radius *= scale; 165 | 166 | // restrict radius to be between desired limits 167 | spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) ); 168 | 169 | // move target to panned location 170 | scope.target.add( panOffset ); 171 | 172 | offset.setFromSpherical( spherical ); 173 | 174 | // rotate offset back to "camera-up-vector-is-up" space 175 | offset.applyQuaternion( quatInverse ); 176 | 177 | position.copy( scope.target ).add( offset ); 178 | 179 | scope.object.lookAt( scope.target ); 180 | 181 | if ( scope.enableDamping === true ) { 182 | 183 | sphericalDelta.theta *= ( 1 - scope.dampingFactor ); 184 | sphericalDelta.phi *= ( 1 - scope.dampingFactor ); 185 | 186 | } else { 187 | 188 | sphericalDelta.set( 0, 0, 0 ); 189 | 190 | } 191 | 192 | scale = 1; 193 | panOffset.set( 0, 0, 0 ); 194 | 195 | // update condition is: 196 | // min(camera displacement, camera rotation in radians)^2 > EPS 197 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 198 | 199 | if ( zoomChanged || 200 | lastPosition.distanceToSquared( scope.object.position ) > EPS || 201 | 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) { 202 | 203 | scope.dispatchEvent( changeEvent ); 204 | 205 | lastPosition.copy( scope.object.position ); 206 | lastQuaternion.copy( scope.object.quaternion ); 207 | zoomChanged = false; 208 | 209 | return true; 210 | 211 | } 212 | 213 | return false; 214 | 215 | }; 216 | 217 | }(); 218 | 219 | this.dispose = function () { 220 | 221 | scope.domElement.removeEventListener( 'contextmenu', onContextMenu, false ); 222 | scope.domElement.removeEventListener( 'mousedown', onMouseDown, false ); 223 | scope.domElement.removeEventListener( 'wheel', onMouseWheel, false ); 224 | 225 | scope.domElement.removeEventListener( 'touchstart', onTouchStart, false ); 226 | scope.domElement.removeEventListener( 'touchend', onTouchEnd, false ); 227 | scope.domElement.removeEventListener( 'touchmove', onTouchMove, false ); 228 | 229 | document.removeEventListener( 'mousemove', onMouseMove, false ); 230 | document.removeEventListener( 'mouseup', onMouseUp, false ); 231 | 232 | window.removeEventListener( 'keydown', onKeyDown, false ); 233 | 234 | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? 235 | 236 | }; 237 | 238 | // 239 | // internals 240 | // 241 | 242 | var scope = this; 243 | 244 | var changeEvent = { type: 'change' }; 245 | var startEvent = { type: 'start' }; 246 | var endEvent = { type: 'end' }; 247 | 248 | var STATE = { NONE: - 1, ROTATE: 0, DOLLY: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_DOLLY: 4, TOUCH_PAN: 5 }; 249 | 250 | var state = STATE.NONE; 251 | 252 | var EPS = 0.000001; 253 | 254 | // current position in spherical coordinates 255 | var spherical = new THREE.Spherical(); 256 | var sphericalDelta = new THREE.Spherical(); 257 | 258 | var scale = 1; 259 | var panOffset = new THREE.Vector3(); 260 | var zoomChanged = false; 261 | 262 | var rotateStart = new THREE.Vector2(); 263 | var rotateEnd = new THREE.Vector2(); 264 | var rotateDelta = new THREE.Vector2(); 265 | 266 | var panStart = new THREE.Vector2(); 267 | var panEnd = new THREE.Vector2(); 268 | var panDelta = new THREE.Vector2(); 269 | 270 | var dollyStart = new THREE.Vector2(); 271 | var dollyEnd = new THREE.Vector2(); 272 | var dollyDelta = new THREE.Vector2(); 273 | 274 | function getAutoRotationAngle() { 275 | 276 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 277 | 278 | } 279 | 280 | function getZoomScale() { 281 | 282 | return Math.pow( 0.95, scope.zoomSpeed ); 283 | 284 | } 285 | 286 | function rotateLeft( angle ) { 287 | 288 | sphericalDelta.theta -= angle; 289 | 290 | } 291 | 292 | function rotateUp( angle ) { 293 | 294 | sphericalDelta.phi -= angle; 295 | 296 | } 297 | 298 | var panLeft = function () { 299 | 300 | var v = new THREE.Vector3(); 301 | 302 | return function panLeft( distance, objectMatrix ) { 303 | 304 | v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix 305 | v.multiplyScalar( - distance ); 306 | 307 | panOffset.add( v ); 308 | 309 | }; 310 | 311 | }(); 312 | 313 | var panUp = function () { 314 | 315 | var v = new THREE.Vector3(); 316 | 317 | return function panUp( distance, objectMatrix ) { 318 | 319 | v.setFromMatrixColumn( objectMatrix, 1 ); // get Y column of objectMatrix 320 | v.multiplyScalar( distance ); 321 | 322 | panOffset.add( v ); 323 | 324 | }; 325 | 326 | }(); 327 | 328 | // deltaX and deltaY are in pixels; right and down are positive 329 | var pan = function () { 330 | 331 | var offset = new THREE.Vector3(); 332 | 333 | return function pan( deltaX, deltaY ) { 334 | 335 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 336 | 337 | if ( scope.object instanceof THREE.PerspectiveCamera ) { 338 | 339 | // perspective 340 | var position = scope.object.position; 341 | offset.copy( position ).sub( scope.target ); 342 | var targetDistance = offset.length(); 343 | 344 | // half of the fov is center to top of screen 345 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); 346 | 347 | // we actually don't use screenWidth, since perspective camera is fixed to screen height 348 | panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); 349 | panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); 350 | 351 | } else if ( scope.object instanceof THREE.OrthographicCamera ) { 352 | 353 | // orthographic 354 | panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); 355 | panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); 356 | 357 | } else { 358 | 359 | // camera neither orthographic nor perspective 360 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 361 | scope.enablePan = false; 362 | 363 | } 364 | 365 | }; 366 | 367 | }(); 368 | 369 | function dollyIn( dollyScale ) { 370 | 371 | if ( scope.object instanceof THREE.PerspectiveCamera ) { 372 | 373 | scale /= dollyScale; 374 | 375 | } else if ( scope.object instanceof THREE.OrthographicCamera ) { 376 | 377 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) ); 378 | scope.object.updateProjectionMatrix(); 379 | zoomChanged = true; 380 | 381 | } else { 382 | 383 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 384 | scope.enableZoom = false; 385 | 386 | } 387 | 388 | } 389 | 390 | function dollyOut( dollyScale ) { 391 | 392 | if ( scope.object instanceof THREE.PerspectiveCamera ) { 393 | 394 | scale *= dollyScale; 395 | 396 | } else if ( scope.object instanceof THREE.OrthographicCamera ) { 397 | 398 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) ); 399 | scope.object.updateProjectionMatrix(); 400 | zoomChanged = true; 401 | 402 | } else { 403 | 404 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 405 | scope.enableZoom = false; 406 | 407 | } 408 | 409 | } 410 | 411 | // 412 | // event callbacks - update the object state 413 | // 414 | 415 | function handleMouseDownRotate( event ) { 416 | 417 | //console.log( 'handleMouseDownRotate' ); 418 | 419 | rotateStart.set( event.clientX, event.clientY ); 420 | 421 | } 422 | 423 | function handleMouseDownDolly( event ) { 424 | 425 | //console.log( 'handleMouseDownDolly' ); 426 | 427 | dollyStart.set( event.clientX, event.clientY ); 428 | 429 | } 430 | 431 | function handleMouseDownPan( event ) { 432 | 433 | //console.log( 'handleMouseDownPan' ); 434 | 435 | panStart.set( event.clientX, event.clientY ); 436 | 437 | } 438 | 439 | function handleMouseMoveRotate( event ) { 440 | 441 | //console.log( 'handleMouseMoveRotate' ); 442 | 443 | rotateEnd.set( event.clientX, event.clientY ); 444 | rotateDelta.subVectors( rotateEnd, rotateStart ); 445 | 446 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 447 | 448 | // rotating across whole screen goes 360 degrees around 449 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); 450 | 451 | // rotating up and down along whole screen attempts to go 360, but limited to 180 452 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); 453 | 454 | rotateStart.copy( rotateEnd ); 455 | 456 | scope.update(); 457 | 458 | } 459 | 460 | function handleMouseMoveDolly( event ) { 461 | 462 | //console.log( 'handleMouseMoveDolly' ); 463 | 464 | dollyEnd.set( event.clientX, event.clientY ); 465 | 466 | dollyDelta.subVectors( dollyEnd, dollyStart ); 467 | 468 | if ( dollyDelta.y > 0 ) { 469 | 470 | dollyIn( getZoomScale() ); 471 | 472 | } else if ( dollyDelta.y < 0 ) { 473 | 474 | dollyOut( getZoomScale() ); 475 | 476 | } 477 | 478 | dollyStart.copy( dollyEnd ); 479 | 480 | scope.update(); 481 | 482 | } 483 | 484 | function handleMouseMovePan( event ) { 485 | 486 | //console.log( 'handleMouseMovePan' ); 487 | 488 | panEnd.set( event.clientX, event.clientY ); 489 | 490 | panDelta.subVectors( panEnd, panStart ); 491 | 492 | pan( panDelta.x, panDelta.y ); 493 | 494 | panStart.copy( panEnd ); 495 | 496 | scope.update(); 497 | 498 | } 499 | 500 | function handleMouseUp( event ) { 501 | 502 | // console.log( 'handleMouseUp' ); 503 | 504 | } 505 | 506 | function handleMouseWheel( event ) { 507 | 508 | // console.log( 'handleMouseWheel' ); 509 | 510 | if ( event.deltaY < 0 ) { 511 | 512 | dollyOut( getZoomScale() ); 513 | 514 | } else if ( event.deltaY > 0 ) { 515 | 516 | dollyIn( getZoomScale() ); 517 | 518 | } 519 | 520 | scope.update(); 521 | 522 | } 523 | 524 | function handleKeyDown( event ) { 525 | 526 | //console.log( 'handleKeyDown' ); 527 | 528 | switch ( event.keyCode ) { 529 | 530 | case scope.keys.UP: 531 | pan( 0, scope.keyPanSpeed ); 532 | scope.update(); 533 | break; 534 | 535 | case scope.keys.BOTTOM: 536 | pan( 0, - scope.keyPanSpeed ); 537 | scope.update(); 538 | break; 539 | 540 | case scope.keys.LEFT: 541 | pan( scope.keyPanSpeed, 0 ); 542 | scope.update(); 543 | break; 544 | 545 | case scope.keys.RIGHT: 546 | pan( - scope.keyPanSpeed, 0 ); 547 | scope.update(); 548 | break; 549 | 550 | } 551 | 552 | } 553 | 554 | function handleTouchStartRotate( event ) { 555 | 556 | //console.log( 'handleTouchStartRotate' ); 557 | 558 | rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 559 | 560 | } 561 | 562 | function handleTouchStartDolly( event ) { 563 | 564 | //console.log( 'handleTouchStartDolly' ); 565 | 566 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 567 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 568 | 569 | var distance = Math.sqrt( dx * dx + dy * dy ); 570 | 571 | dollyStart.set( 0, distance ); 572 | 573 | } 574 | 575 | function handleTouchStartPan( event ) { 576 | 577 | //console.log( 'handleTouchStartPan' ); 578 | 579 | panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 580 | 581 | } 582 | 583 | function handleTouchMoveRotate( event ) { 584 | 585 | //console.log( 'handleTouchMoveRotate' ); 586 | 587 | rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 588 | rotateDelta.subVectors( rotateEnd, rotateStart ); 589 | 590 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 591 | 592 | // rotating across whole screen goes 360 degrees around 593 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); 594 | 595 | // rotating up and down along whole screen attempts to go 360, but limited to 180 596 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); 597 | 598 | rotateStart.copy( rotateEnd ); 599 | 600 | scope.update(); 601 | 602 | } 603 | 604 | function handleTouchMoveDolly( event ) { 605 | 606 | //console.log( 'handleTouchMoveDolly' ); 607 | 608 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 609 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 610 | 611 | var distance = Math.sqrt( dx * dx + dy * dy ); 612 | 613 | dollyEnd.set( 0, distance ); 614 | 615 | dollyDelta.subVectors( dollyEnd, dollyStart ); 616 | 617 | if ( dollyDelta.y > 0 ) { 618 | 619 | dollyOut( getZoomScale() ); 620 | 621 | } else if ( dollyDelta.y < 0 ) { 622 | 623 | dollyIn( getZoomScale() ); 624 | 625 | } 626 | 627 | dollyStart.copy( dollyEnd ); 628 | 629 | scope.update(); 630 | 631 | } 632 | 633 | function handleTouchMovePan( event ) { 634 | 635 | //console.log( 'handleTouchMovePan' ); 636 | 637 | panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 638 | 639 | panDelta.subVectors( panEnd, panStart ); 640 | 641 | pan( panDelta.x, panDelta.y ); 642 | 643 | panStart.copy( panEnd ); 644 | 645 | scope.update(); 646 | 647 | } 648 | 649 | function handleTouchEnd( event ) { 650 | 651 | //console.log( 'handleTouchEnd' ); 652 | 653 | } 654 | 655 | // 656 | // event handlers - FSM: listen for events and reset state 657 | // 658 | 659 | function onMouseDown( event ) { 660 | 661 | if ( scope.enabled === false ) return; 662 | 663 | event.preventDefault(); 664 | 665 | switch ( event.button ) { 666 | 667 | case scope.mouseButtons.ORBIT: 668 | 669 | if ( scope.enableRotate === false ) return; 670 | 671 | handleMouseDownRotate( event ); 672 | 673 | state = STATE.ROTATE; 674 | 675 | break; 676 | 677 | case scope.mouseButtons.ZOOM: 678 | 679 | if ( scope.enableZoom === false ) return; 680 | 681 | handleMouseDownDolly( event ); 682 | 683 | state = STATE.DOLLY; 684 | 685 | break; 686 | 687 | case scope.mouseButtons.PAN: 688 | 689 | if ( scope.enablePan === false ) return; 690 | 691 | handleMouseDownPan( event ); 692 | 693 | state = STATE.PAN; 694 | 695 | break; 696 | 697 | } 698 | 699 | if ( state !== STATE.NONE ) { 700 | 701 | document.addEventListener( 'mousemove', onMouseMove, false ); 702 | document.addEventListener( 'mouseup', onMouseUp, false ); 703 | 704 | scope.dispatchEvent( startEvent ); 705 | 706 | } 707 | 708 | } 709 | 710 | function onMouseMove( event ) { 711 | 712 | if ( scope.enabled === false ) return; 713 | 714 | event.preventDefault(); 715 | 716 | switch ( state ) { 717 | 718 | case STATE.ROTATE: 719 | 720 | if ( scope.enableRotate === false ) return; 721 | 722 | handleMouseMoveRotate( event ); 723 | 724 | break; 725 | 726 | case STATE.DOLLY: 727 | 728 | if ( scope.enableZoom === false ) return; 729 | 730 | handleMouseMoveDolly( event ); 731 | 732 | break; 733 | 734 | case STATE.PAN: 735 | 736 | if ( scope.enablePan === false ) return; 737 | 738 | handleMouseMovePan( event ); 739 | 740 | break; 741 | 742 | } 743 | 744 | } 745 | 746 | function onMouseUp( event ) { 747 | 748 | if ( scope.enabled === false ) return; 749 | 750 | handleMouseUp( event ); 751 | 752 | document.removeEventListener( 'mousemove', onMouseMove, false ); 753 | document.removeEventListener( 'mouseup', onMouseUp, false ); 754 | 755 | scope.dispatchEvent( endEvent ); 756 | 757 | state = STATE.NONE; 758 | 759 | } 760 | 761 | function onMouseWheel( event ) { 762 | 763 | if ( scope.enabled === false || scope.enableZoom === false || ( state !== STATE.NONE && state !== STATE.ROTATE ) ) return; 764 | 765 | event.preventDefault(); 766 | event.stopPropagation(); 767 | 768 | handleMouseWheel( event ); 769 | 770 | scope.dispatchEvent( startEvent ); // not sure why these are here... 771 | scope.dispatchEvent( endEvent ); 772 | 773 | } 774 | 775 | function onKeyDown( event ) { 776 | 777 | if ( scope.enabled === false || scope.enableKeys === false || scope.enablePan === false ) return; 778 | 779 | handleKeyDown( event ); 780 | 781 | } 782 | 783 | function onTouchStart( event ) { 784 | 785 | if ( scope.enabled === false ) return; 786 | 787 | switch ( event.touches.length ) { 788 | 789 | case 1: // one-fingered touch: rotate 790 | 791 | if ( scope.enableRotate === false ) return; 792 | 793 | handleTouchStartRotate( event ); 794 | 795 | state = STATE.TOUCH_ROTATE; 796 | 797 | break; 798 | 799 | case 2: // two-fingered touch: dolly 800 | 801 | if ( scope.enableZoom === false ) return; 802 | 803 | handleTouchStartDolly( event ); 804 | 805 | state = STATE.TOUCH_DOLLY; 806 | 807 | break; 808 | 809 | case 3: // three-fingered touch: pan 810 | 811 | if ( scope.enablePan === false ) return; 812 | 813 | handleTouchStartPan( event ); 814 | 815 | state = STATE.TOUCH_PAN; 816 | 817 | break; 818 | 819 | default: 820 | 821 | state = STATE.NONE; 822 | 823 | } 824 | 825 | if ( state !== STATE.NONE ) { 826 | 827 | scope.dispatchEvent( startEvent ); 828 | 829 | } 830 | 831 | } 832 | 833 | function onTouchMove( event ) { 834 | 835 | if ( scope.enabled === false ) return; 836 | 837 | event.preventDefault(); 838 | event.stopPropagation(); 839 | 840 | switch ( event.touches.length ) { 841 | 842 | case 1: // one-fingered touch: rotate 843 | 844 | if ( scope.enableRotate === false ) return; 845 | if ( state !== STATE.TOUCH_ROTATE ) return; // is this needed?... 846 | 847 | handleTouchMoveRotate( event ); 848 | 849 | break; 850 | 851 | case 2: // two-fingered touch: dolly 852 | 853 | if ( scope.enableZoom === false ) return; 854 | if ( state !== STATE.TOUCH_DOLLY ) return; // is this needed?... 855 | 856 | handleTouchMoveDolly( event ); 857 | 858 | break; 859 | 860 | case 3: // three-fingered touch: pan 861 | 862 | if ( scope.enablePan === false ) return; 863 | if ( state !== STATE.TOUCH_PAN ) return; // is this needed?... 864 | 865 | handleTouchMovePan( event ); 866 | 867 | break; 868 | 869 | default: 870 | 871 | state = STATE.NONE; 872 | 873 | } 874 | 875 | } 876 | 877 | function onTouchEnd( event ) { 878 | 879 | if ( scope.enabled === false ) return; 880 | 881 | handleTouchEnd( event ); 882 | 883 | scope.dispatchEvent( endEvent ); 884 | 885 | state = STATE.NONE; 886 | 887 | } 888 | 889 | function onContextMenu( event ) { 890 | 891 | event.preventDefault(); 892 | 893 | } 894 | 895 | // 896 | 897 | scope.domElement.addEventListener( 'contextmenu', onContextMenu, false ); 898 | 899 | scope.domElement.addEventListener( 'mousedown', onMouseDown, false ); 900 | scope.domElement.addEventListener( 'wheel', onMouseWheel, false ); 901 | 902 | scope.domElement.addEventListener( 'touchstart', onTouchStart, false ); 903 | scope.domElement.addEventListener( 'touchend', onTouchEnd, false ); 904 | scope.domElement.addEventListener( 'touchmove', onTouchMove, false ); 905 | 906 | window.addEventListener( 'keydown', onKeyDown, false ); 907 | 908 | // force an update at start 909 | 910 | this.update(); 911 | 912 | }; 913 | 914 | THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype ); 915 | THREE.OrbitControls.prototype.constructor = THREE.OrbitControls; 916 | 917 | Object.defineProperties( THREE.OrbitControls.prototype, { 918 | 919 | center: { 920 | 921 | get: function () { 922 | 923 | console.warn( 'THREE.OrbitControls: .center has been renamed to .target' ); 924 | return this.target; 925 | 926 | } 927 | 928 | }, 929 | 930 | // backward compatibility 931 | 932 | noZoom: { 933 | 934 | get: function () { 935 | 936 | console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' ); 937 | return ! this.enableZoom; 938 | 939 | }, 940 | 941 | set: function ( value ) { 942 | 943 | console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' ); 944 | this.enableZoom = ! value; 945 | 946 | } 947 | 948 | }, 949 | 950 | noRotate: { 951 | 952 | get: function () { 953 | 954 | console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' ); 955 | return ! this.enableRotate; 956 | 957 | }, 958 | 959 | set: function ( value ) { 960 | 961 | console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' ); 962 | this.enableRotate = ! value; 963 | 964 | } 965 | 966 | }, 967 | 968 | noPan: { 969 | 970 | get: function () { 971 | 972 | console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' ); 973 | return ! this.enablePan; 974 | 975 | }, 976 | 977 | set: function ( value ) { 978 | 979 | console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' ); 980 | this.enablePan = ! value; 981 | 982 | } 983 | 984 | }, 985 | 986 | noKeys: { 987 | 988 | get: function () { 989 | 990 | console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' ); 991 | return ! this.enableKeys; 992 | 993 | }, 994 | 995 | set: function ( value ) { 996 | 997 | console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' ); 998 | this.enableKeys = ! value; 999 | 1000 | } 1001 | 1002 | }, 1003 | 1004 | staticMoving: { 1005 | 1006 | get: function () { 1007 | 1008 | console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' ); 1009 | return ! this.enableDamping; 1010 | 1011 | }, 1012 | 1013 | set: function ( value ) { 1014 | 1015 | console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' ); 1016 | this.enableDamping = ! value; 1017 | 1018 | } 1019 | 1020 | }, 1021 | 1022 | dynamicDampingFactor: { 1023 | 1024 | get: function () { 1025 | 1026 | console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' ); 1027 | return this.dampingFactor; 1028 | 1029 | }, 1030 | 1031 | set: function ( value ) { 1032 | 1033 | console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' ); 1034 | this.dampingFactor = value; 1035 | 1036 | } 1037 | 1038 | } 1039 | 1040 | } ); 1041 | -------------------------------------------------------------------------------- /libs/bvhtree.js: -------------------------------------------------------------------------------- 1 | /** 2 | * bvh-tree 3 | * A Bounding Volume Hierarchy data structure implementation. 4 | * https://github.com/benraziel/bvh-tree 5 | * 6 | * @author Ben Raziel 7 | */ 8 | var bvhtree = bvhtree || {}; 9 | bvhtree.EPSILON = 1e-6; 10 | 11 | /** 12 | * A 3D Vector class. Based on three.js Vector3 13 | */ 14 | bvhtree.BVHVector3 = function ( x, y, z ) { 15 | this.x = x || 0; 16 | this.y = y || 0; 17 | this.z = z || 0; 18 | 19 | }; 20 | 21 | bvhtree.BVHVector3.prototype = { 22 | 23 | constructor: bvhtree.BVHVector3, 24 | 25 | copy: function ( v ) { 26 | 27 | this.x = v.x; 28 | this.y = v.y; 29 | this.z = v.z; 30 | 31 | return this; 32 | 33 | }, 34 | 35 | set: function ( x, y, z ) { 36 | 37 | this.x = x; 38 | this.y = y; 39 | this.z = z; 40 | 41 | return this; 42 | }, 43 | 44 | setFromArray: function(array, firstElementPos) { 45 | this.x = array[firstElementPos]; 46 | this.y = array[firstElementPos+1]; 47 | this.z = array[firstElementPos+2]; 48 | }, 49 | 50 | add: function ( v ) { 51 | 52 | this.x += v.x; 53 | this.y += v.y; 54 | this.z += v.z; 55 | 56 | return this; 57 | }, 58 | 59 | multiplyScalar: function ( scalar ) { 60 | 61 | this.x *= scalar; 62 | this.y *= scalar; 63 | this.z *= scalar; 64 | 65 | return this; 66 | 67 | }, 68 | 69 | subVectors: function ( a, b ) { 70 | 71 | this.x = a.x - b.x; 72 | this.y = a.y - b.y; 73 | this.z = a.z - b.z; 74 | 75 | return this; 76 | 77 | }, 78 | 79 | dot: function ( v ) { 80 | 81 | return this.x * v.x + this.y * v.y + this.z * v.z; 82 | 83 | }, 84 | 85 | cross: function ( v ) { 86 | var x = this.x, y = this.y, z = this.z; 87 | 88 | this.x = y * v.z - z * v.y; 89 | this.y = z * v.x - x * v.z; 90 | this.z = x * v.y - y * v.x; 91 | 92 | return this; 93 | }, 94 | 95 | crossVectors: function ( a, b ) { 96 | 97 | var ax = a.x, ay = a.y, az = a.z; 98 | var bx = b.x, by = b.y, bz = b.z; 99 | 100 | this.x = ay * bz - az * by; 101 | this.y = az * bx - ax * bz; 102 | this.z = ax * by - ay * bx; 103 | 104 | return this; 105 | }, 106 | 107 | clone: function () { 108 | return new bvhtree.BVHVector3( this.x, this.y, this.z ); 109 | } 110 | }; 111 | 112 | /** 113 | * @typedef {Object} Point A Point in 3D space 114 | * @property {number} x x coordinate of the point 115 | * @property {number} y y coordinate of the point 116 | * @property {number} z z coordinate of the point 117 | * 118 | * @typedef Point[3] Triangle A triangle in 3D space 119 | */ 120 | 121 | /** 122 | * Constructs a bounding volume heirarchy from a list of triangles 123 | * @class 124 | * @param {Triangle[]} triangles an array of triangles to index. Each triangle is represented as an array of 3 xyz coordinates: [{x: X0, y: Y0, z: Z0}, {x: X1, y: Y1, z: Z1}, {x: X2, y: Y2, z: Z2}] 125 | * @param {number} [maxTrianglesPerNode=10] the maximum number of triangles in each node of the BVH tree. Once this value is reached, that node is split into two child nodes. 126 | */ 127 | bvhtree.BVH = function(triangles, maxTrianglesPerNode) { 128 | var trianglesArray = []; 129 | trianglesArray.length = triangles.length * 9; 130 | 131 | for (var i = 0; i < triangles.length; i++) { 132 | var p0 = triangles[i][0]; 133 | var p1 = triangles[i][1]; 134 | var p2 = triangles[i][2]; 135 | 136 | trianglesArray[i*9] = p0.x; 137 | trianglesArray[i*9+1] = p0.y; 138 | trianglesArray[i*9+2] = p0.z; 139 | 140 | trianglesArray[i*9+3] = p1.x; 141 | trianglesArray[i*9+4] = p1.y; 142 | trianglesArray[i*9+5] = p1.z; 143 | 144 | trianglesArray[i*9+6] = p2.x; 145 | trianglesArray[i*9+7] = p2.y; 146 | trianglesArray[i*9+8] = p2.z; 147 | } 148 | 149 | 150 | this._trianglesArray = trianglesArray; 151 | this._maxTrianglesPerNode = maxTrianglesPerNode || 10; 152 | this._bboxArray = this.calcBoundingBoxes(trianglesArray); 153 | 154 | // clone a helper array 155 | this._bboxHelper = new Float32Array(this._bboxArray.length); 156 | this._bboxHelper.set(this._bboxArray); 157 | 158 | // create the root node, add all the triangles to it 159 | var triangleCount = trianglesArray.length / 9; 160 | var extents = this.calcExtents(0, triangleCount, bvhtree.EPSILON); 161 | this._rootNode = new bvhtree.BVHNode(extents[0], extents[1], 0, triangleCount, 0); 162 | 163 | this._nodesToSplit = [this._rootNode]; 164 | 165 | while (this._nodesToSplit.length > 0) { 166 | var node = this._nodesToSplit.pop(); 167 | this.splitNode(node); 168 | } 169 | }; 170 | 171 | /** 172 | * returns a list of all the triangles in the BVH which interected a specific node. 173 | * We use the BVH node structure to first cull out nodes which do not intereset the ray. 174 | * For rays that did intersect, we test intersection of the ray with each triangle 175 | * @param {Point} rayOrigin the origin position of the ray. 176 | * @param {Point} rayDirection the direction vector of the ray. 177 | * @param {Boolean} backfaceCulling if 'true', only intersections with front-faces of the mesh will be performed. 178 | * @return IntersectionResult[] an array of intersection result, one for each triangle which intersected the BVH 179 | * 180 | * @typedef {Object} IntersectionResult 181 | * @property Array[] triangle the triangle which the ray intersected 182 | * @property number triangleIndex the position of the interescting triangle in the input triangle array provided to the BVH constructor. 183 | * @property {Point} intersectionPoint the interesection point of the ray on the triangle. 184 | */ 185 | bvhtree.BVH.prototype.intersectRay = function(rayOrigin, rayDirection, backfaceCulling) { 186 | var nodesToIntersect = [this._rootNode]; 187 | var trianglesInIntersectingNodes = []; // a list of nodes that intersect the ray (according to their bounding box) 188 | var intersectingTriangles = []; 189 | var i; 190 | 191 | var invRayDirection = new bvhtree.BVHVector3( 192 | 1.0 / rayDirection.x, 193 | 1.0 / rayDirection.y, 194 | 1.0 / rayDirection.z 195 | ); 196 | 197 | // go over the BVH tree, and extract the list of triangles that lie in nodes that intersect the ray. 198 | // note: these triangles may not intersect the ray themselves 199 | while (nodesToIntersect.length > 0) { 200 | var node = nodesToIntersect.pop(); 201 | 202 | if (bvhtree.BVH.intersectNodeBox(rayOrigin, invRayDirection, node)) { 203 | if (node._node0) { 204 | nodesToIntersect.push(node._node0); 205 | } 206 | 207 | if (node._node1) { 208 | nodesToIntersect.push(node._node1); 209 | } 210 | 211 | for (i = node._startIndex; i < node._endIndex; i++) { 212 | trianglesInIntersectingNodes.push(this._bboxArray[i*7]); 213 | } 214 | } 215 | } 216 | 217 | // go over the list of candidate triangles, and check each of them using ray triangle intersection 218 | var a = new bvhtree.BVHVector3(); 219 | var b = new bvhtree.BVHVector3(); 220 | var c = new bvhtree.BVHVector3(); 221 | var rayOriginVec3 = new bvhtree.BVHVector3(rayOrigin.x, rayOrigin.y, rayOrigin.z); 222 | var rayDirectionVec3 = new bvhtree.BVHVector3(rayDirection.x, rayDirection.y, rayDirection.z); 223 | 224 | for (i = 0; i < trianglesInIntersectingNodes.length; i++) { 225 | var triIndex = trianglesInIntersectingNodes[i]; 226 | 227 | a.setFromArray(this._trianglesArray, triIndex*9); 228 | b.setFromArray(this._trianglesArray, triIndex*9+3); 229 | c.setFromArray(this._trianglesArray, triIndex*9+6); 230 | 231 | var intersectionPoint = bvhtree.BVH.intersectRayTriangle(a, b, c, rayOriginVec3, rayDirectionVec3, backfaceCulling); 232 | 233 | if (intersectionPoint) { 234 | intersectingTriangles.push({ 235 | triangle: [a.clone(), b.clone(), c.clone()], 236 | triangleIndex: triIndex, 237 | intersectionPoint: intersectionPoint 238 | }); 239 | } 240 | } 241 | 242 | return intersectingTriangles; 243 | }; 244 | 245 | /** 246 | * Gets an array of triangle, and calculates the bounding box for each of them, and adds an index to the triangle's position in the array 247 | * Each bbox is saved as 7 values in a Float32Array: (position, minX, minY, minZ, maxX, maxY, maxZ) 248 | */ 249 | bvhtree.BVH.prototype.calcBoundingBoxes = function(trianglesArray) { 250 | var p0x, p0y, p0z; 251 | var p1x, p1y, p1z; 252 | var p2x, p2y, p2z; 253 | var minX, minY, minZ; 254 | var maxX, maxY, maxZ; 255 | 256 | var triangleCount = trianglesArray.length / 9; 257 | var bboxArray = new Float32Array(triangleCount * 7); 258 | 259 | for (var i = 0; i < triangleCount; i++) { 260 | p0x = trianglesArray[i*9]; 261 | p0y = trianglesArray[i*9+1]; 262 | p0z = trianglesArray[i*9+2]; 263 | p1x = trianglesArray[i*9+3]; 264 | p1y = trianglesArray[i*9+4]; 265 | p1z = trianglesArray[i*9+5]; 266 | p2x = trianglesArray[i*9+6]; 267 | p2y = trianglesArray[i*9+7]; 268 | p2z = trianglesArray[i*9+8]; 269 | 270 | minX = Math.min(Math.min(p0x, p1x), p2x); 271 | minY = Math.min(Math.min(p0y, p1y), p2y); 272 | minZ = Math.min(Math.min(p0z, p1z), p2z); 273 | maxX = Math.max(Math.max(p0x, p1x), p2x); 274 | maxY = Math.max(Math.max(p0y, p1y), p2y); 275 | maxZ = Math.max(Math.max(p0z, p1z), p2z); 276 | 277 | bvhtree.BVH.setBox(bboxArray, i, i, minX, minY, minZ, maxX, maxY, maxZ); 278 | } 279 | 280 | return bboxArray; 281 | }; 282 | 283 | /** 284 | * Calculates the extents (i.e the min and max coordinates) of a list of bounding boxes in the bboxArray 285 | * @param startIndex the index of the first triangle that we want to calc extents for 286 | * @param endIndex the index of the last triangle that we want to calc extents for 287 | * @param expandBy a small epsilon to expand the bbox by, for safety during ray-box intersections 288 | */ 289 | bvhtree.BVH.prototype.calcExtents = function(startIndex, endIndex, expandBy) { 290 | expandBy = expandBy || 0.0; 291 | 292 | if (startIndex >= endIndex) { 293 | return [{'x': 0, 'y': 0, 'z': 0}, {'x': 0, 'y': 0, 'z': 0}]; 294 | } 295 | 296 | var minX = Number.MAX_VALUE; 297 | var minY = Number.MAX_VALUE; 298 | var minZ = Number.MAX_VALUE; 299 | var maxX = -Number.MAX_VALUE; 300 | var maxY = -Number.MAX_VALUE; 301 | var maxZ = -Number.MAX_VALUE; 302 | 303 | for (var i = startIndex; i < endIndex; i++) { 304 | minX = Math.min(this._bboxArray[i*7+1], minX); 305 | minY = Math.min(this._bboxArray[i*7+2], minY); 306 | minZ = Math.min(this._bboxArray[i*7+3], minZ); 307 | maxX = Math.max(this._bboxArray[i*7+4], maxX); 308 | maxY = Math.max(this._bboxArray[i*7+5], maxY); 309 | maxZ = Math.max(this._bboxArray[i*7+6], maxZ); 310 | } 311 | 312 | return [ 313 | {'x': minX - expandBy, 'y': minY - expandBy, 'z': minZ - expandBy}, 314 | {'x': maxX + expandBy, 'y': maxY + expandBy, 'z': maxZ + expandBy} 315 | ]; 316 | }; 317 | 318 | bvhtree.BVH.prototype.splitNode = function(node) { 319 | if ((node.elementCount() <= this._maxTrianglesPerNode) || (node.elementCount() === 0)) { 320 | return; 321 | } 322 | 323 | var startIndex = node._startIndex; 324 | var endIndex = node._endIndex; 325 | 326 | var leftNode = [ [],[],[] ]; 327 | var rightNode = [ [],[],[] ]; 328 | var extentCenters = [node.centerX(), node.centerY(), node.centerZ()]; 329 | 330 | var extentsLength = [ 331 | node._extentsMax.x - node._extentsMin.x, 332 | node._extentsMax.y - node._extentsMin.y, 333 | node._extentsMax.z - node._extentsMin.z 334 | ]; 335 | 336 | var objectCenter = []; 337 | objectCenter.length = 3; 338 | 339 | for (var i = startIndex; i < endIndex; i++) { 340 | objectCenter[0] = (this._bboxArray[i * 7 + 1] + this._bboxArray[i * 7 + 4]) * 0.5; // center = (min + max) / 2 341 | objectCenter[1] = (this._bboxArray[i * 7 + 2] + this._bboxArray[i * 7 + 5]) * 0.5; // center = (min + max) / 2 342 | objectCenter[2] = (this._bboxArray[i * 7 + 3] + this._bboxArray[i * 7 + 6]) * 0.5; // center = (min + max) / 2 343 | 344 | for (var j = 0; j < 3; j++) { 345 | if (objectCenter[j] < extentCenters[j]) { 346 | leftNode[j].push(i); 347 | } 348 | else { 349 | rightNode[j].push(i); 350 | } 351 | } 352 | } 353 | 354 | // check if we couldn't split the node by any of the axes (x, y or z). halt here, dont try to split any more (cause it will always fail, and we'll enter an infinite loop 355 | var splitFailed = []; 356 | splitFailed.length = 3; 357 | 358 | splitFailed[0] = (leftNode[0].length === 0) || (rightNode[0].length === 0); 359 | splitFailed[1] = (leftNode[1].length === 0) || (rightNode[1].length === 0); 360 | splitFailed[2] = (leftNode[2].length === 0) || (rightNode[2].length === 0); 361 | 362 | if (splitFailed[0] && splitFailed[1] && splitFailed[2]) { 363 | return; 364 | } 365 | 366 | // choose the longest split axis. if we can't split by it, choose next best one. 367 | var splitOrder = [0, 1, 2]; 368 | 369 | splitOrder.sort(function(axis0, axis1) { 370 | return (extentsLength[axis1] - extentsLength[axis0]) 371 | }); 372 | 373 | var leftElements; 374 | var rightElements; 375 | 376 | for (j = 0; j < 3; j++) { 377 | var candidateIndex = splitOrder[j]; 378 | 379 | if (!splitFailed[candidateIndex]) { 380 | leftElements = leftNode[candidateIndex]; 381 | rightElements = rightNode[candidateIndex]; 382 | 383 | break; 384 | } 385 | } 386 | 387 | // sort the elements in range (startIndex, endIndex) according to which node they should be at 388 | var node0Start = startIndex; 389 | var node0End = node0Start + leftElements.length; 390 | var node1Start = node0End; 391 | var node1End = endIndex; 392 | var currElement; 393 | 394 | var helperPos = node._startIndex; 395 | var concatenatedElements = leftElements.concat(rightElements); 396 | 397 | for (i = 0; i < concatenatedElements.length; i++) { 398 | currElement = concatenatedElements[i]; 399 | bvhtree.BVH.copyBox(this._bboxArray, currElement, this._bboxHelper, helperPos); 400 | helperPos++; 401 | } 402 | 403 | // copy results back to main array 404 | var subArr = this._bboxHelper.subarray(node._startIndex * 7, node._endIndex * 7); 405 | this._bboxArray.set(subArr, node._startIndex * 7); 406 | 407 | // create 2 new nodes for the node we just split, and add links to them from the parent node 408 | var node0Extents = this.calcExtents(node0Start, node0End, bvhtree.EPSILON); 409 | var node1Extents = this.calcExtents(node1Start, node1End, bvhtree.EPSILON); 410 | 411 | var node0 = new bvhtree.BVHNode(node0Extents[0], node0Extents[1], node0Start, node0End, node._level + 1); 412 | var node1 = new bvhtree.BVHNode(node1Extents[0], node1Extents[1], node1Start, node1End, node._level + 1); 413 | 414 | node._node0 = node0; 415 | node._node1 = node1; 416 | node.clearShapes(); 417 | 418 | // add new nodes to the split queue 419 | this._nodesToSplit.push(node0); 420 | this._nodesToSplit.push(node1); 421 | }; 422 | 423 | bvhtree.BVH._calcTValues = function(minVal, maxVal, rayOriginCoord, invdir) { 424 | var res = {min: 0, max: 0}; 425 | 426 | if ( invdir >= 0 ) { 427 | res.min = ( minVal - rayOriginCoord ) * invdir; 428 | res.max = ( maxVal - rayOriginCoord ) * invdir; 429 | 430 | } else { 431 | res.min = ( maxVal - rayOriginCoord ) * invdir; 432 | res.max = ( minVal - rayOriginCoord ) * invdir; 433 | } 434 | 435 | return res; 436 | }; 437 | 438 | bvhtree.BVH.intersectNodeBox = function(rayOrigin, invRayDirection, node) { 439 | var t = bvhtree.BVH._calcTValues(node._extentsMin.x, node._extentsMax.x, rayOrigin.x, invRayDirection.x); 440 | var ty = bvhtree.BVH._calcTValues(node._extentsMin.y, node._extentsMax.y, rayOrigin.y, invRayDirection.y); 441 | 442 | if ( ( t.min > ty.max ) || ( ty.min > t.max ) ) { 443 | return false; 444 | } 445 | 446 | // These lines also handle the case where tmin or tmax is NaN 447 | // (result of 0 * Infinity). x !== x returns true if x is NaN 448 | if ( ty.min > t.min || t.min !== t.min ) { 449 | t.min = ty.min; 450 | } 451 | 452 | if ( ty.max < t.max || t.max !== t.max ) { 453 | t.max = ty.max; 454 | } 455 | 456 | var tz = bvhtree.BVH._calcTValues(node._extentsMin.z, node._extentsMax.z, rayOrigin.z, invRayDirection.z); 457 | 458 | if ( ( t.min > tz.max ) || ( tz.min > t.max ) ) { 459 | return false; 460 | } 461 | 462 | if ( tz.min > t.min || t.min !== t.min ) { 463 | t.min = tz.min; 464 | } 465 | 466 | if ( tz.max < t.max || t.max !== t.max ) { 467 | t.max = tz.max; 468 | } 469 | 470 | //return point closest to the ray (positive side) 471 | if (t.max < 0 ) { 472 | return false; 473 | } 474 | 475 | return true; 476 | }; 477 | 478 | bvhtree.BVH.intersectRayTriangle = (function () { 479 | // Compute the offset origin, edges, and normal. 480 | var diff = new bvhtree.BVHVector3(); 481 | var edge1 = new bvhtree.BVHVector3(); 482 | var edge2 = new bvhtree.BVHVector3(); 483 | var normal = new bvhtree.BVHVector3(); 484 | 485 | return function (a, b, c, rayOrigin, rayDirection, backfaceCulling) { 486 | 487 | // from http://www.geometrictools.com/LibMathematics/Intersection/Wm5IntrRay3Triangle3.cpp 488 | 489 | edge1.subVectors(b, a); 490 | edge2.subVectors(c, a); 491 | normal.crossVectors(edge1, edge2); 492 | 493 | // Solve Q + t*D = b1*E1 + bL*E2 (Q = kDiff, D = ray direction, 494 | // E1 = kEdge1, E2 = kEdge2, N = Cross(E1,E2)) by 495 | // |Dot(D,N)|*b1 = sign(Dot(D,N))*Dot(D,Cross(Q,E2)) 496 | // |Dot(D,N)|*b2 = sign(Dot(D,N))*Dot(D,Cross(E1,Q)) 497 | // |Dot(D,N)|*t = -sign(Dot(D,N))*Dot(Q,N) 498 | var DdN = rayDirection.dot(normal); 499 | var sign; 500 | 501 | if (DdN > 0) { 502 | 503 | if (backfaceCulling) { 504 | return null; 505 | } 506 | 507 | sign = 1; 508 | 509 | } else if (DdN < 0) { 510 | 511 | sign = -1; 512 | DdN = -DdN; 513 | 514 | } else { 515 | 516 | return null; 517 | 518 | } 519 | 520 | diff.subVectors(rayOrigin, a); 521 | var DdQxE2 = sign * rayDirection.dot(edge2.crossVectors(diff, edge2)); 522 | 523 | // b1 < 0, no intersection 524 | if (DdQxE2 < 0) { 525 | return null; 526 | } 527 | 528 | var DdE1xQ = sign * rayDirection.dot(edge1.cross(diff)); 529 | 530 | // b2 < 0, no intersection 531 | if (DdE1xQ < 0) { 532 | return null; 533 | } 534 | 535 | // b1+b2 > 1, no intersection 536 | if (DdQxE2 + DdE1xQ > DdN) { 537 | return null; 538 | } 539 | 540 | // Line intersects triangle, check if ray does. 541 | var QdN = -sign * diff.dot(normal); 542 | 543 | // t < 0, no intersection 544 | if (QdN < 0) { 545 | return null; 546 | } 547 | 548 | // Ray intersects triangle. 549 | var t = QdN / DdN; 550 | var result = new bvhtree.BVHVector3(); 551 | return result.copy( rayDirection ).multiplyScalar( t ).add( rayOrigin ); 552 | }; 553 | }()); 554 | 555 | bvhtree.BVH.setBox = function(bboxArray, pos, triangleId, minX, minY, minZ, maxX, maxY, maxZ) { 556 | bboxArray[pos*7] = triangleId; 557 | bboxArray[pos*7+1] = minX; 558 | bboxArray[pos*7+2] = minY; 559 | bboxArray[pos*7+3] = minZ; 560 | bboxArray[pos*7+4] = maxX; 561 | bboxArray[pos*7+5] = maxY; 562 | bboxArray[pos*7+6] = maxZ; 563 | }; 564 | 565 | bvhtree.BVH.copyBox = function(sourceArray, sourcePos, destArray, destPos) { 566 | destArray[destPos*7] = sourceArray[sourcePos*7]; 567 | destArray[destPos*7+1] = sourceArray[sourcePos*7+1]; 568 | destArray[destPos*7+2] = sourceArray[sourcePos*7+2]; 569 | destArray[destPos*7+3] = sourceArray[sourcePos*7+3]; 570 | destArray[destPos*7+4] = sourceArray[sourcePos*7+4]; 571 | destArray[destPos*7+5] = sourceArray[sourcePos*7+5]; 572 | destArray[destPos*7+6] = sourceArray[sourcePos*7+6]; 573 | }; 574 | 575 | bvhtree.BVH.getBox = function(bboxArray, pos, outputBox) { 576 | outputBox.triangleId = bboxArray[pos*7]; 577 | outputBox.minX = bboxArray[pos*7+1]; 578 | outputBox.minY = bboxArray[pos*7+2]; 579 | outputBox.minZ = bboxArray[pos*7+3]; 580 | outputBox.maxX = bboxArray[pos*7+4]; 581 | outputBox.maxY = bboxArray[pos*7+5]; 582 | outputBox.maxZ = bboxArray[pos*7+6]; 583 | }; 584 | 585 | 586 | /** 587 | * A node in the BVH structure 588 | * @class 589 | * @param {Point} extentsMin the min coords of this node's bounding box ({x,y,z}) 590 | * @param {Point} extentsMax the max coords of this node's bounding box ({x,y,z}) 591 | * @param {number} startIndex an index in the bbox array, where the first element of this node is located 592 | * @param {number} endIndex an index in the bbox array, where the last of this node is located, plus 1 (meaning that its non-inclusive). 593 | * @param {number} the distance of this node from the root for the bvh tree. root node has level=0, its children have level=1 etc. 594 | */ 595 | bvhtree.BVHNode = function(extentsMin, extentsMax, startIndex, endIndex, level) { 596 | this._extentsMin = extentsMin; 597 | this._extentsMax = extentsMax; 598 | this._startIndex = startIndex; 599 | this._endIndex = endIndex; 600 | this._level = level; 601 | this._node0 = null; 602 | this._node1 = null; 603 | }; 604 | 605 | bvhtree.BVHNode.prototype.elementCount = function() { 606 | return this._endIndex - this._startIndex; 607 | }; 608 | 609 | bvhtree.BVHNode.prototype.centerX = function() { 610 | return (this._extentsMin.x + this._extentsMax.x) * 0.5; 611 | }; 612 | 613 | bvhtree.BVHNode.prototype.centerY = function() { 614 | return (this._extentsMin.y + this._extentsMax.y) * 0.5; 615 | }; 616 | 617 | bvhtree.BVHNode.prototype.centerZ = function() { 618 | return (this._extentsMin.z + this._extentsMax.z) * 0.5; 619 | }; 620 | 621 | bvhtree.BVHNode.prototype.clearShapes = function() { 622 | this._startIndex = -1; 623 | this._endIndex = -1; 624 | }; 625 | 626 | bvhtree.BVHNode.calcBoundingSphereRadius = function(extentsMin, extentsMax) { 627 | var centerX = (extentsMin.x + extentsMax.x) * 0.5; 628 | var centerY = (extentsMin.y + extentsMax.y) * 0.5; 629 | var centerZ = (extentsMin.z + extentsMax.z) * 0.5; 630 | 631 | var extentsMinDistSqr = 632 | (centerX - extentsMin.x) * (centerX - extentsMin.x) + 633 | (centerY - extentsMin.y) * (centerY - extentsMin.y) + 634 | (centerZ - extentsMin.z) * (centerZ - extentsMin.z); 635 | 636 | var extentsMaxDistSqr = 637 | (centerX - extentsMax.x) * (centerX - extentsMax.x) + 638 | (centerY - extentsMax.y) * (centerY - extentsMax.y) + 639 | (centerZ - extentsMax.z) * (centerZ - extentsMax.z); 640 | 641 | return Math.sqrt(Math.max(extentsMinDistSqr, extentsMaxDistSqr)); 642 | }; 643 | 644 | // commonjs module definiton 645 | if (typeof module !== 'undefined' && module.exports) { 646 | module.exports.BVH = bvhtree.BVH; 647 | module.exports.intersectRay = bvhtree.intersectRay; 648 | } 649 | -------------------------------------------------------------------------------- /main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |