├── README.md ├── index.html ├── models └── pistol.glb ├── vendor ├── GLTFLoader.js ├── OrbitControls.js └── three.min.js ├── viewer.css └── viewer.js /README.md: -------------------------------------------------------------------------------- 1 | # GLB-Viewer 2 | 3D Viewer Web application for viewing glb files inspired by Autodesk Forge Viewer 3 | 4 | ## Features 5 | 1. Importing glb files 6 | 2. Measure distance b/w two points 7 | 3. Explode faces or separate objects of the model 8 | 4. Fully responsive with mobile support 9 | 5. Standard 3D views with orientation cube 10 | 11 | ## Demo 12 | https://glb-viewer.blogspot.com/ 13 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Viewer3D 4 | 5 | 6 | 7 | 8 | 19 | 20 | 21 |
22 |
23 |
24 |
25 |
26 |
Model Browser
27 |
28 | 29 | 33 |
34 |
35 |
36 |
37 |
Screenshot
38 |
39 |
40 |
41 |
Share
42 |
43 |
44 |
45 |
46 |
47 | 56 | 75 |
76 |
77 |
78 |
79 | 82 |
83 |
Loading...
84 |
85 |
86 |
87 | 113 |
114 | 115 | 116 | 117 | 118 | 119 | 127 | 128 | -------------------------------------------------------------------------------- /models/pistol.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytezeroseven/GLB-Viewer/c77af4667b8f1abcbadf1293dfa968508725e3cd/models/pistol.glb -------------------------------------------------------------------------------- /vendor/GLTFLoader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Rich Tibbett / https://github.com/richtr 3 | * @author mrdoob / http://mrdoob.com/ 4 | * @author Tony Parisi / http://www.tonyparisi.com/ 5 | * @author Takahiro / https://github.com/takahirox 6 | * @author Don McCurdy / https://www.donmccurdy.com 7 | */ 8 | 9 | THREE.GLTFLoader = ( function () { 10 | 11 | function GLTFLoader( manager ) { 12 | 13 | THREE.Loader.call( this, manager ); 14 | 15 | this.dracoLoader = null; 16 | this.ddsLoader = null; 17 | 18 | } 19 | 20 | GLTFLoader.prototype = Object.assign( Object.create( THREE.Loader.prototype ), { 21 | 22 | constructor: GLTFLoader, 23 | 24 | load: function ( url, onLoad, onProgress, onError ) { 25 | 26 | var scope = this; 27 | 28 | var resourcePath; 29 | 30 | if ( this.resourcePath !== '' ) { 31 | 32 | resourcePath = this.resourcePath; 33 | 34 | } else if ( this.path !== '' ) { 35 | 36 | resourcePath = this.path; 37 | 38 | } else { 39 | 40 | resourcePath = THREE.LoaderUtils.extractUrlBase( url ); 41 | 42 | } 43 | 44 | // Tells the LoadingManager to track an extra item, which resolves after 45 | // the model is fully loaded. This means the count of items loaded will 46 | // be incorrect, but ensures manager.onLoad() does not fire early. 47 | scope.manager.itemStart( url ); 48 | 49 | var _onError = function ( e ) { 50 | 51 | if ( onError ) { 52 | 53 | onError( e ); 54 | 55 | } else { 56 | 57 | console.error( e ); 58 | 59 | } 60 | 61 | scope.manager.itemError( url ); 62 | scope.manager.itemEnd( url ); 63 | 64 | }; 65 | 66 | var loader = new THREE.FileLoader( scope.manager ); 67 | 68 | loader.setPath( this.path ); 69 | loader.setResponseType( 'arraybuffer' ); 70 | 71 | if ( scope.crossOrigin === 'use-credentials' ) { 72 | 73 | loader.setWithCredentials( true ); 74 | 75 | } 76 | 77 | loader.load( url, function ( data ) { 78 | 79 | try { 80 | 81 | scope.parse( data, resourcePath, function ( gltf ) { 82 | 83 | onLoad( gltf ); 84 | 85 | scope.manager.itemEnd( url ); 86 | 87 | }, _onError ); 88 | 89 | } catch ( e ) { 90 | 91 | _onError( e ); 92 | 93 | } 94 | 95 | }, onProgress, _onError ); 96 | 97 | }, 98 | 99 | setDRACOLoader: function ( dracoLoader ) { 100 | 101 | this.dracoLoader = dracoLoader; 102 | return this; 103 | 104 | }, 105 | 106 | setDDSLoader: function ( ddsLoader ) { 107 | 108 | this.ddsLoader = ddsLoader; 109 | return this; 110 | 111 | }, 112 | 113 | parse: function ( data, path, onLoad, onError ) { 114 | 115 | var content; 116 | var extensions = {}; 117 | 118 | if ( typeof data === 'string' ) { 119 | 120 | content = data; 121 | 122 | } else { 123 | 124 | var magic = THREE.LoaderUtils.decodeText( new Uint8Array( data, 0, 4 ) ); 125 | 126 | if ( magic === BINARY_EXTENSION_HEADER_MAGIC ) { 127 | 128 | try { 129 | 130 | extensions[ EXTENSIONS.KHR_BINARY_GLTF ] = new GLTFBinaryExtension( data ); 131 | 132 | } catch ( error ) { 133 | 134 | if ( onError ) onError( error ); 135 | return; 136 | 137 | } 138 | 139 | content = extensions[ EXTENSIONS.KHR_BINARY_GLTF ].content; 140 | 141 | } else { 142 | 143 | content = THREE.LoaderUtils.decodeText( new Uint8Array( data ) ); 144 | 145 | } 146 | 147 | } 148 | 149 | var json = JSON.parse( content ); 150 | 151 | if ( json.asset === undefined || json.asset.version[ 0 ] < 2 ) { 152 | 153 | if ( onError ) onError( new Error( 'THREE.GLTFLoader: Unsupported asset. glTF versions >=2.0 are supported.' ) ); 154 | return; 155 | 156 | } 157 | 158 | if ( json.extensionsUsed ) { 159 | 160 | for ( var i = 0; i < json.extensionsUsed.length; ++ i ) { 161 | 162 | var extensionName = json.extensionsUsed[ i ]; 163 | var extensionsRequired = json.extensionsRequired || []; 164 | 165 | switch ( extensionName ) { 166 | 167 | case EXTENSIONS.KHR_LIGHTS_PUNCTUAL: 168 | extensions[ extensionName ] = new GLTFLightsExtension( json ); 169 | break; 170 | 171 | case EXTENSIONS.KHR_MATERIALS_UNLIT: 172 | extensions[ extensionName ] = new GLTFMaterialsUnlitExtension(); 173 | break; 174 | 175 | case EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: 176 | extensions[ extensionName ] = new GLTFMaterialsPbrSpecularGlossinessExtension(); 177 | break; 178 | 179 | case EXTENSIONS.KHR_DRACO_MESH_COMPRESSION: 180 | extensions[ extensionName ] = new GLTFDracoMeshCompressionExtension( json, this.dracoLoader ); 181 | break; 182 | 183 | case EXTENSIONS.MSFT_TEXTURE_DDS: 184 | extensions[ extensionName ] = new GLTFTextureDDSExtension( this.ddsLoader ); 185 | break; 186 | 187 | case EXTENSIONS.KHR_TEXTURE_TRANSFORM: 188 | extensions[ extensionName ] = new GLTFTextureTransformExtension(); 189 | break; 190 | 191 | case EXTENSIONS.KHR_MESH_QUANTIZATION: 192 | extensions[ extensionName ] = new GLTFMeshQuantizationExtension(); 193 | break; 194 | 195 | default: 196 | 197 | if ( extensionsRequired.indexOf( extensionName ) >= 0 ) { 198 | 199 | console.warn( 'THREE.GLTFLoader: Unknown extension "' + extensionName + '".' ); 200 | 201 | } 202 | 203 | } 204 | 205 | } 206 | 207 | } 208 | 209 | var parser = new GLTFParser( json, extensions, { 210 | 211 | path: path || this.resourcePath || '', 212 | crossOrigin: this.crossOrigin, 213 | manager: this.manager 214 | 215 | } ); 216 | 217 | parser.parse( onLoad, onError ); 218 | 219 | } 220 | 221 | } ); 222 | 223 | /* GLTFREGISTRY */ 224 | 225 | function GLTFRegistry() { 226 | 227 | var objects = {}; 228 | 229 | return { 230 | 231 | get: function ( key ) { 232 | 233 | return objects[ key ]; 234 | 235 | }, 236 | 237 | add: function ( key, object ) { 238 | 239 | objects[ key ] = object; 240 | 241 | }, 242 | 243 | remove: function ( key ) { 244 | 245 | delete objects[ key ]; 246 | 247 | }, 248 | 249 | removeAll: function () { 250 | 251 | objects = {}; 252 | 253 | } 254 | 255 | }; 256 | 257 | } 258 | 259 | /*********************************/ 260 | /********** EXTENSIONS ***********/ 261 | /*********************************/ 262 | 263 | var EXTENSIONS = { 264 | KHR_BINARY_GLTF: 'KHR_binary_glTF', 265 | KHR_DRACO_MESH_COMPRESSION: 'KHR_draco_mesh_compression', 266 | KHR_LIGHTS_PUNCTUAL: 'KHR_lights_punctual', 267 | KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: 'KHR_materials_pbrSpecularGlossiness', 268 | KHR_MATERIALS_UNLIT: 'KHR_materials_unlit', 269 | KHR_TEXTURE_TRANSFORM: 'KHR_texture_transform', 270 | KHR_MESH_QUANTIZATION: 'KHR_mesh_quantization', 271 | MSFT_TEXTURE_DDS: 'MSFT_texture_dds' 272 | }; 273 | 274 | /** 275 | * DDS Texture Extension 276 | * 277 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/MSFT_texture_dds 278 | * 279 | */ 280 | function GLTFTextureDDSExtension( ddsLoader ) { 281 | 282 | if ( ! ddsLoader ) { 283 | 284 | throw new Error( 'THREE.GLTFLoader: Attempting to load .dds texture without importing THREE.DDSLoader' ); 285 | 286 | } 287 | 288 | this.name = EXTENSIONS.MSFT_TEXTURE_DDS; 289 | this.ddsLoader = ddsLoader; 290 | 291 | } 292 | 293 | /** 294 | * Punctual Lights Extension 295 | * 296 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual 297 | */ 298 | function GLTFLightsExtension( json ) { 299 | 300 | this.name = EXTENSIONS.KHR_LIGHTS_PUNCTUAL; 301 | 302 | var extension = ( json.extensions && json.extensions[ EXTENSIONS.KHR_LIGHTS_PUNCTUAL ] ) || {}; 303 | this.lightDefs = extension.lights || []; 304 | 305 | } 306 | 307 | GLTFLightsExtension.prototype.loadLight = function ( lightIndex ) { 308 | 309 | var lightDef = this.lightDefs[ lightIndex ]; 310 | var lightNode; 311 | 312 | var color = new THREE.Color( 0xffffff ); 313 | if ( lightDef.color !== undefined ) color.fromArray( lightDef.color ); 314 | 315 | var range = lightDef.range !== undefined ? lightDef.range : 0; 316 | 317 | switch ( lightDef.type ) { 318 | 319 | case 'directional': 320 | lightNode = new THREE.DirectionalLight( color ); 321 | lightNode.target.position.set( 0, 0, - 1 ); 322 | lightNode.add( lightNode.target ); 323 | break; 324 | 325 | case 'point': 326 | lightNode = new THREE.PointLight( color ); 327 | lightNode.distance = range; 328 | break; 329 | 330 | case 'spot': 331 | lightNode = new THREE.SpotLight( color ); 332 | lightNode.distance = range; 333 | // Handle spotlight properties. 334 | lightDef.spot = lightDef.spot || {}; 335 | lightDef.spot.innerConeAngle = lightDef.spot.innerConeAngle !== undefined ? lightDef.spot.innerConeAngle : 0; 336 | lightDef.spot.outerConeAngle = lightDef.spot.outerConeAngle !== undefined ? lightDef.spot.outerConeAngle : Math.PI / 4.0; 337 | lightNode.angle = lightDef.spot.outerConeAngle; 338 | lightNode.penumbra = 1.0 - lightDef.spot.innerConeAngle / lightDef.spot.outerConeAngle; 339 | lightNode.target.position.set( 0, 0, - 1 ); 340 | lightNode.add( lightNode.target ); 341 | break; 342 | 343 | default: 344 | throw new Error( 'THREE.GLTFLoader: Unexpected light type, "' + lightDef.type + '".' ); 345 | 346 | } 347 | 348 | // Some lights (e.g. spot) default to a position other than the origin. Reset the position 349 | // here, because node-level parsing will only override position if explicitly specified. 350 | lightNode.position.set( 0, 0, 0 ); 351 | 352 | lightNode.decay = 2; 353 | 354 | if ( lightDef.intensity !== undefined ) lightNode.intensity = lightDef.intensity; 355 | 356 | lightNode.name = lightDef.name || ( 'light_' + lightIndex ); 357 | 358 | return Promise.resolve( lightNode ); 359 | 360 | }; 361 | 362 | /** 363 | * Unlit Materials Extension 364 | * 365 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_unlit 366 | */ 367 | function GLTFMaterialsUnlitExtension() { 368 | 369 | this.name = EXTENSIONS.KHR_MATERIALS_UNLIT; 370 | 371 | } 372 | 373 | GLTFMaterialsUnlitExtension.prototype.getMaterialType = function () { 374 | 375 | return THREE.MeshBasicMaterial; 376 | 377 | }; 378 | 379 | GLTFMaterialsUnlitExtension.prototype.extendParams = function ( materialParams, materialDef, parser ) { 380 | 381 | var pending = []; 382 | 383 | materialParams.color = new THREE.Color( 1.0, 1.0, 1.0 ); 384 | materialParams.opacity = 1.0; 385 | 386 | var metallicRoughness = materialDef.pbrMetallicRoughness; 387 | 388 | if ( metallicRoughness ) { 389 | 390 | if ( Array.isArray( metallicRoughness.baseColorFactor ) ) { 391 | 392 | var array = metallicRoughness.baseColorFactor; 393 | 394 | materialParams.color.fromArray( array ); 395 | materialParams.opacity = array[ 3 ]; 396 | 397 | } 398 | 399 | if ( metallicRoughness.baseColorTexture !== undefined ) { 400 | 401 | pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture ) ); 402 | 403 | } 404 | 405 | } 406 | 407 | return Promise.all( pending ); 408 | 409 | }; 410 | 411 | /* BINARY EXTENSION */ 412 | var BINARY_EXTENSION_HEADER_MAGIC = 'glTF'; 413 | var BINARY_EXTENSION_HEADER_LENGTH = 12; 414 | var BINARY_EXTENSION_CHUNK_TYPES = { JSON: 0x4E4F534A, BIN: 0x004E4942 }; 415 | 416 | function GLTFBinaryExtension( data ) { 417 | 418 | this.name = EXTENSIONS.KHR_BINARY_GLTF; 419 | this.content = null; 420 | this.body = null; 421 | 422 | var headerView = new DataView( data, 0, BINARY_EXTENSION_HEADER_LENGTH ); 423 | 424 | this.header = { 425 | magic: THREE.LoaderUtils.decodeText( new Uint8Array( data.slice( 0, 4 ) ) ), 426 | version: headerView.getUint32( 4, true ), 427 | length: headerView.getUint32( 8, true ) 428 | }; 429 | 430 | if ( this.header.magic !== BINARY_EXTENSION_HEADER_MAGIC ) { 431 | 432 | throw new Error( 'THREE.GLTFLoader: Unsupported glTF-Binary header.' ); 433 | 434 | } else if ( this.header.version < 2.0 ) { 435 | 436 | throw new Error( 'THREE.GLTFLoader: Legacy binary file detected.' ); 437 | 438 | } 439 | 440 | var chunkView = new DataView( data, BINARY_EXTENSION_HEADER_LENGTH ); 441 | var chunkIndex = 0; 442 | 443 | while ( chunkIndex < chunkView.byteLength ) { 444 | 445 | var chunkLength = chunkView.getUint32( chunkIndex, true ); 446 | chunkIndex += 4; 447 | 448 | var chunkType = chunkView.getUint32( chunkIndex, true ); 449 | chunkIndex += 4; 450 | 451 | if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.JSON ) { 452 | 453 | var contentArray = new Uint8Array( data, BINARY_EXTENSION_HEADER_LENGTH + chunkIndex, chunkLength ); 454 | this.content = THREE.LoaderUtils.decodeText( contentArray ); 455 | 456 | } else if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.BIN ) { 457 | 458 | var byteOffset = BINARY_EXTENSION_HEADER_LENGTH + chunkIndex; 459 | this.body = data.slice( byteOffset, byteOffset + chunkLength ); 460 | 461 | } 462 | 463 | // Clients must ignore chunks with unknown types. 464 | 465 | chunkIndex += chunkLength; 466 | 467 | } 468 | 469 | if ( this.content === null ) { 470 | 471 | throw new Error( 'THREE.GLTFLoader: JSON content not found.' ); 472 | 473 | } 474 | 475 | } 476 | 477 | /** 478 | * DRACO Mesh Compression Extension 479 | * 480 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_draco_mesh_compression 481 | */ 482 | function GLTFDracoMeshCompressionExtension( json, dracoLoader ) { 483 | 484 | if ( ! dracoLoader ) { 485 | 486 | throw new Error( 'THREE.GLTFLoader: No DRACOLoader instance provided.' ); 487 | 488 | } 489 | 490 | this.name = EXTENSIONS.KHR_DRACO_MESH_COMPRESSION; 491 | this.json = json; 492 | this.dracoLoader = dracoLoader; 493 | this.dracoLoader.preload(); 494 | 495 | } 496 | 497 | GLTFDracoMeshCompressionExtension.prototype.decodePrimitive = function ( primitive, parser ) { 498 | 499 | var json = this.json; 500 | var dracoLoader = this.dracoLoader; 501 | var bufferViewIndex = primitive.extensions[ this.name ].bufferView; 502 | var gltfAttributeMap = primitive.extensions[ this.name ].attributes; 503 | var threeAttributeMap = {}; 504 | var attributeNormalizedMap = {}; 505 | var attributeTypeMap = {}; 506 | 507 | for ( var attributeName in gltfAttributeMap ) { 508 | 509 | var threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase(); 510 | 511 | threeAttributeMap[ threeAttributeName ] = gltfAttributeMap[ attributeName ]; 512 | 513 | } 514 | 515 | for ( attributeName in primitive.attributes ) { 516 | 517 | var threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase(); 518 | 519 | if ( gltfAttributeMap[ attributeName ] !== undefined ) { 520 | 521 | var accessorDef = json.accessors[ primitive.attributes[ attributeName ] ]; 522 | var componentType = WEBGL_COMPONENT_TYPES[ accessorDef.componentType ]; 523 | 524 | attributeTypeMap[ threeAttributeName ] = componentType; 525 | attributeNormalizedMap[ threeAttributeName ] = accessorDef.normalized === true; 526 | 527 | } 528 | 529 | } 530 | 531 | return parser.getDependency( 'bufferView', bufferViewIndex ).then( function ( bufferView ) { 532 | 533 | return new Promise( function ( resolve ) { 534 | 535 | dracoLoader.decodeDracoFile( bufferView, function ( geometry ) { 536 | 537 | for ( var attributeName in geometry.attributes ) { 538 | 539 | var attribute = geometry.attributes[ attributeName ]; 540 | var normalized = attributeNormalizedMap[ attributeName ]; 541 | 542 | if ( normalized !== undefined ) attribute.normalized = normalized; 543 | 544 | } 545 | 546 | resolve( geometry ); 547 | 548 | }, threeAttributeMap, attributeTypeMap ); 549 | 550 | } ); 551 | 552 | } ); 553 | 554 | }; 555 | 556 | /** 557 | * Texture Transform Extension 558 | * 559 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_transform 560 | */ 561 | function GLTFTextureTransformExtension() { 562 | 563 | this.name = EXTENSIONS.KHR_TEXTURE_TRANSFORM; 564 | 565 | } 566 | 567 | GLTFTextureTransformExtension.prototype.extendTexture = function ( texture, transform ) { 568 | 569 | texture = texture.clone(); 570 | 571 | if ( transform.offset !== undefined ) { 572 | 573 | texture.offset.fromArray( transform.offset ); 574 | 575 | } 576 | 577 | if ( transform.rotation !== undefined ) { 578 | 579 | texture.rotation = transform.rotation; 580 | 581 | } 582 | 583 | if ( transform.scale !== undefined ) { 584 | 585 | texture.repeat.fromArray( transform.scale ); 586 | 587 | } 588 | 589 | if ( transform.texCoord !== undefined ) { 590 | 591 | console.warn( 'THREE.GLTFLoader: Custom UV sets in "' + this.name + '" extension not yet supported.' ); 592 | 593 | } 594 | 595 | texture.needsUpdate = true; 596 | 597 | return texture; 598 | 599 | }; 600 | 601 | /** 602 | * Specular-Glossiness Extension 603 | * 604 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness 605 | */ 606 | 607 | /** 608 | * A sub class of THREE.StandardMaterial with some of the functionality 609 | * changed via the `onBeforeCompile` callback 610 | * @pailhead 611 | */ 612 | 613 | function GLTFMeshStandardSGMaterial( params ) { 614 | 615 | THREE.MeshStandardMaterial.call( this ); 616 | 617 | this.isGLTFSpecularGlossinessMaterial = true; 618 | 619 | //various chunks that need replacing 620 | var specularMapParsFragmentChunk = [ 621 | '#ifdef USE_SPECULARMAP', 622 | ' uniform sampler2D specularMap;', 623 | '#endif' 624 | ].join( '\n' ); 625 | 626 | var glossinessMapParsFragmentChunk = [ 627 | '#ifdef USE_GLOSSINESSMAP', 628 | ' uniform sampler2D glossinessMap;', 629 | '#endif' 630 | ].join( '\n' ); 631 | 632 | var specularMapFragmentChunk = [ 633 | 'vec3 specularFactor = specular;', 634 | '#ifdef USE_SPECULARMAP', 635 | ' vec4 texelSpecular = texture2D( specularMap, vUv );', 636 | ' texelSpecular = sRGBToLinear( texelSpecular );', 637 | ' // reads channel RGB, compatible with a glTF Specular-Glossiness (RGBA) texture', 638 | ' specularFactor *= texelSpecular.rgb;', 639 | '#endif' 640 | ].join( '\n' ); 641 | 642 | var glossinessMapFragmentChunk = [ 643 | 'float glossinessFactor = glossiness;', 644 | '#ifdef USE_GLOSSINESSMAP', 645 | ' vec4 texelGlossiness = texture2D( glossinessMap, vUv );', 646 | ' // reads channel A, compatible with a glTF Specular-Glossiness (RGBA) texture', 647 | ' glossinessFactor *= texelGlossiness.a;', 648 | '#endif' 649 | ].join( '\n' ); 650 | 651 | var lightPhysicalFragmentChunk = [ 652 | 'PhysicalMaterial material;', 653 | 'material.diffuseColor = diffuseColor.rgb;', 654 | 'vec3 dxy = max( abs( dFdx( geometryNormal ) ), abs( dFdy( geometryNormal ) ) );', 655 | 'float geometryRoughness = max( max( dxy.x, dxy.y ), dxy.z );', 656 | 'material.specularRoughness = max( 1.0 - glossinessFactor, 0.0525 );// 0.0525 corresponds to the base mip of a 256 cubemap.', 657 | 'material.specularRoughness += geometryRoughness;', 658 | 'material.specularRoughness = min( material.specularRoughness, 1.0 );', 659 | 'material.specularColor = specularFactor.rgb;', 660 | ].join( '\n' ); 661 | 662 | var uniforms = { 663 | specular: { value: new THREE.Color().setHex( 0xffffff ) }, 664 | glossiness: { value: 1 }, 665 | specularMap: { value: null }, 666 | glossinessMap: { value: null } 667 | }; 668 | 669 | this._extraUniforms = uniforms; 670 | 671 | // please see #14031 or #13198 for an alternate approach 672 | this.onBeforeCompile = function ( shader ) { 673 | 674 | for ( var uniformName in uniforms ) { 675 | 676 | shader.uniforms[ uniformName ] = uniforms[ uniformName ]; 677 | 678 | } 679 | 680 | shader.fragmentShader = shader.fragmentShader.replace( 'uniform float roughness;', 'uniform vec3 specular;' ); 681 | shader.fragmentShader = shader.fragmentShader.replace( 'uniform float metalness;', 'uniform float glossiness;' ); 682 | shader.fragmentShader = shader.fragmentShader.replace( '#include ', specularMapParsFragmentChunk ); 683 | shader.fragmentShader = shader.fragmentShader.replace( '#include ', glossinessMapParsFragmentChunk ); 684 | shader.fragmentShader = shader.fragmentShader.replace( '#include ', specularMapFragmentChunk ); 685 | shader.fragmentShader = shader.fragmentShader.replace( '#include ', glossinessMapFragmentChunk ); 686 | shader.fragmentShader = shader.fragmentShader.replace( '#include ', lightPhysicalFragmentChunk ); 687 | 688 | }; 689 | 690 | /*eslint-disable*/ 691 | Object.defineProperties( 692 | this, 693 | { 694 | specular: { 695 | get: function () { return uniforms.specular.value; }, 696 | set: function ( v ) { uniforms.specular.value = v; } 697 | }, 698 | specularMap: { 699 | get: function () { return uniforms.specularMap.value; }, 700 | set: function ( v ) { uniforms.specularMap.value = v; } 701 | }, 702 | glossiness: { 703 | get: function () { return uniforms.glossiness.value; }, 704 | set: function ( v ) { uniforms.glossiness.value = v; } 705 | }, 706 | glossinessMap: { 707 | get: function () { return uniforms.glossinessMap.value; }, 708 | set: function ( v ) { 709 | 710 | uniforms.glossinessMap.value = v; 711 | //how about something like this - @pailhead 712 | if ( v ) { 713 | 714 | this.defines.USE_GLOSSINESSMAP = ''; 715 | // set USE_ROUGHNESSMAP to enable vUv 716 | this.defines.USE_ROUGHNESSMAP = ''; 717 | 718 | } else { 719 | 720 | delete this.defines.USE_ROUGHNESSMAP; 721 | delete this.defines.USE_GLOSSINESSMAP; 722 | 723 | } 724 | 725 | } 726 | } 727 | } 728 | ); 729 | 730 | /*eslint-enable*/ 731 | delete this.metalness; 732 | delete this.roughness; 733 | delete this.metalnessMap; 734 | delete this.roughnessMap; 735 | 736 | this.setValues( params ); 737 | 738 | } 739 | 740 | GLTFMeshStandardSGMaterial.prototype = Object.create( THREE.MeshStandardMaterial.prototype ); 741 | GLTFMeshStandardSGMaterial.prototype.constructor = GLTFMeshStandardSGMaterial; 742 | 743 | GLTFMeshStandardSGMaterial.prototype.copy = function ( source ) { 744 | 745 | THREE.MeshStandardMaterial.prototype.copy.call( this, source ); 746 | this.specularMap = source.specularMap; 747 | this.specular.copy( source.specular ); 748 | this.glossinessMap = source.glossinessMap; 749 | this.glossiness = source.glossiness; 750 | delete this.metalness; 751 | delete this.roughness; 752 | delete this.metalnessMap; 753 | delete this.roughnessMap; 754 | return this; 755 | 756 | }; 757 | 758 | function GLTFMaterialsPbrSpecularGlossinessExtension() { 759 | 760 | return { 761 | 762 | name: EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS, 763 | 764 | specularGlossinessParams: [ 765 | 'color', 766 | 'map', 767 | 'lightMap', 768 | 'lightMapIntensity', 769 | 'aoMap', 770 | 'aoMapIntensity', 771 | 'emissive', 772 | 'emissiveIntensity', 773 | 'emissiveMap', 774 | 'bumpMap', 775 | 'bumpScale', 776 | 'normalMap', 777 | 'normalMapType', 778 | 'displacementMap', 779 | 'displacementScale', 780 | 'displacementBias', 781 | 'specularMap', 782 | 'specular', 783 | 'glossinessMap', 784 | 'glossiness', 785 | 'alphaMap', 786 | 'envMap', 787 | 'envMapIntensity', 788 | 'refractionRatio', 789 | ], 790 | 791 | getMaterialType: function () { 792 | 793 | return GLTFMeshStandardSGMaterial; 794 | 795 | }, 796 | 797 | extendParams: function ( materialParams, materialDef, parser ) { 798 | 799 | var pbrSpecularGlossiness = materialDef.extensions[ this.name ]; 800 | 801 | materialParams.color = new THREE.Color( 1.0, 1.0, 1.0 ); 802 | materialParams.opacity = 1.0; 803 | 804 | var pending = []; 805 | 806 | if ( Array.isArray( pbrSpecularGlossiness.diffuseFactor ) ) { 807 | 808 | var array = pbrSpecularGlossiness.diffuseFactor; 809 | 810 | materialParams.color.fromArray( array ); 811 | materialParams.opacity = array[ 3 ]; 812 | 813 | } 814 | 815 | if ( pbrSpecularGlossiness.diffuseTexture !== undefined ) { 816 | 817 | pending.push( parser.assignTexture( materialParams, 'map', pbrSpecularGlossiness.diffuseTexture ) ); 818 | 819 | } 820 | 821 | materialParams.emissive = new THREE.Color( 0.0, 0.0, 0.0 ); 822 | materialParams.glossiness = pbrSpecularGlossiness.glossinessFactor !== undefined ? pbrSpecularGlossiness.glossinessFactor : 1.0; 823 | materialParams.specular = new THREE.Color( 1.0, 1.0, 1.0 ); 824 | 825 | if ( Array.isArray( pbrSpecularGlossiness.specularFactor ) ) { 826 | 827 | materialParams.specular.fromArray( pbrSpecularGlossiness.specularFactor ); 828 | 829 | } 830 | 831 | if ( pbrSpecularGlossiness.specularGlossinessTexture !== undefined ) { 832 | 833 | var specGlossMapDef = pbrSpecularGlossiness.specularGlossinessTexture; 834 | pending.push( parser.assignTexture( materialParams, 'glossinessMap', specGlossMapDef ) ); 835 | pending.push( parser.assignTexture( materialParams, 'specularMap', specGlossMapDef ) ); 836 | 837 | } 838 | 839 | return Promise.all( pending ); 840 | 841 | }, 842 | 843 | createMaterial: function ( materialParams ) { 844 | 845 | var material = new GLTFMeshStandardSGMaterial( materialParams ); 846 | material.fog = true; 847 | 848 | material.color = materialParams.color; 849 | 850 | material.map = materialParams.map === undefined ? null : materialParams.map; 851 | 852 | material.lightMap = null; 853 | material.lightMapIntensity = 1.0; 854 | 855 | material.aoMap = materialParams.aoMap === undefined ? null : materialParams.aoMap; 856 | material.aoMapIntensity = 1.0; 857 | 858 | material.emissive = materialParams.emissive; 859 | material.emissiveIntensity = 1.0; 860 | material.emissiveMap = materialParams.emissiveMap === undefined ? null : materialParams.emissiveMap; 861 | 862 | material.bumpMap = materialParams.bumpMap === undefined ? null : materialParams.bumpMap; 863 | material.bumpScale = 1; 864 | 865 | material.normalMap = materialParams.normalMap === undefined ? null : materialParams.normalMap; 866 | material.normalMapType = THREE.TangentSpaceNormalMap; 867 | 868 | if ( materialParams.normalScale ) material.normalScale = materialParams.normalScale; 869 | 870 | material.displacementMap = null; 871 | material.displacementScale = 1; 872 | material.displacementBias = 0; 873 | 874 | material.specularMap = materialParams.specularMap === undefined ? null : materialParams.specularMap; 875 | material.specular = materialParams.specular; 876 | 877 | material.glossinessMap = materialParams.glossinessMap === undefined ? null : materialParams.glossinessMap; 878 | material.glossiness = materialParams.glossiness; 879 | 880 | material.alphaMap = null; 881 | 882 | material.envMap = materialParams.envMap === undefined ? null : materialParams.envMap; 883 | material.envMapIntensity = 1.0; 884 | 885 | material.refractionRatio = 0.98; 886 | 887 | return material; 888 | 889 | }, 890 | 891 | }; 892 | 893 | } 894 | 895 | /** 896 | * Mesh Quantization Extension 897 | * 898 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_mesh_quantization 899 | */ 900 | function GLTFMeshQuantizationExtension() { 901 | 902 | this.name = EXTENSIONS.KHR_MESH_QUANTIZATION; 903 | 904 | } 905 | 906 | /*********************************/ 907 | /********** INTERPOLATION ********/ 908 | /*********************************/ 909 | 910 | // Spline Interpolation 911 | // Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#appendix-c-spline-interpolation 912 | function GLTFCubicSplineInterpolant( parameterPositions, sampleValues, sampleSize, resultBuffer ) { 913 | 914 | THREE.Interpolant.call( this, parameterPositions, sampleValues, sampleSize, resultBuffer ); 915 | 916 | } 917 | 918 | GLTFCubicSplineInterpolant.prototype = Object.create( THREE.Interpolant.prototype ); 919 | GLTFCubicSplineInterpolant.prototype.constructor = GLTFCubicSplineInterpolant; 920 | 921 | GLTFCubicSplineInterpolant.prototype.copySampleValue_ = function ( index ) { 922 | 923 | // Copies a sample value to the result buffer. See description of glTF 924 | // CUBICSPLINE values layout in interpolate_() function below. 925 | 926 | var result = this.resultBuffer, 927 | values = this.sampleValues, 928 | valueSize = this.valueSize, 929 | offset = index * valueSize * 3 + valueSize; 930 | 931 | for ( var i = 0; i !== valueSize; i ++ ) { 932 | 933 | result[ i ] = values[ offset + i ]; 934 | 935 | } 936 | 937 | return result; 938 | 939 | }; 940 | 941 | GLTFCubicSplineInterpolant.prototype.beforeStart_ = GLTFCubicSplineInterpolant.prototype.copySampleValue_; 942 | 943 | GLTFCubicSplineInterpolant.prototype.afterEnd_ = GLTFCubicSplineInterpolant.prototype.copySampleValue_; 944 | 945 | GLTFCubicSplineInterpolant.prototype.interpolate_ = function ( i1, t0, t, t1 ) { 946 | 947 | var result = this.resultBuffer; 948 | var values = this.sampleValues; 949 | var stride = this.valueSize; 950 | 951 | var stride2 = stride * 2; 952 | var stride3 = stride * 3; 953 | 954 | var td = t1 - t0; 955 | 956 | var p = ( t - t0 ) / td; 957 | var pp = p * p; 958 | var ppp = pp * p; 959 | 960 | var offset1 = i1 * stride3; 961 | var offset0 = offset1 - stride3; 962 | 963 | var s2 = - 2 * ppp + 3 * pp; 964 | var s3 = ppp - pp; 965 | var s0 = 1 - s2; 966 | var s1 = s3 - pp + p; 967 | 968 | // Layout of keyframe output values for CUBICSPLINE animations: 969 | // [ inTangent_1, splineVertex_1, outTangent_1, inTangent_2, splineVertex_2, ... ] 970 | for ( var i = 0; i !== stride; i ++ ) { 971 | 972 | var p0 = values[ offset0 + i + stride ]; // splineVertex_k 973 | var m0 = values[ offset0 + i + stride2 ] * td; // outTangent_k * (t_k+1 - t_k) 974 | var p1 = values[ offset1 + i + stride ]; // splineVertex_k+1 975 | var m1 = values[ offset1 + i ] * td; // inTangent_k+1 * (t_k+1 - t_k) 976 | 977 | result[ i ] = s0 * p0 + s1 * m0 + s2 * p1 + s3 * m1; 978 | 979 | } 980 | 981 | return result; 982 | 983 | }; 984 | 985 | /*********************************/ 986 | /********** INTERNALS ************/ 987 | /*********************************/ 988 | 989 | /* CONSTANTS */ 990 | 991 | var WEBGL_CONSTANTS = { 992 | FLOAT: 5126, 993 | //FLOAT_MAT2: 35674, 994 | FLOAT_MAT3: 35675, 995 | FLOAT_MAT4: 35676, 996 | FLOAT_VEC2: 35664, 997 | FLOAT_VEC3: 35665, 998 | FLOAT_VEC4: 35666, 999 | LINEAR: 9729, 1000 | REPEAT: 10497, 1001 | SAMPLER_2D: 35678, 1002 | POINTS: 0, 1003 | LINES: 1, 1004 | LINE_LOOP: 2, 1005 | LINE_STRIP: 3, 1006 | TRIANGLES: 4, 1007 | TRIANGLE_STRIP: 5, 1008 | TRIANGLE_FAN: 6, 1009 | UNSIGNED_BYTE: 5121, 1010 | UNSIGNED_SHORT: 5123 1011 | }; 1012 | 1013 | var WEBGL_COMPONENT_TYPES = { 1014 | 5120: Int8Array, 1015 | 5121: Uint8Array, 1016 | 5122: Int16Array, 1017 | 5123: Uint16Array, 1018 | 5125: Uint32Array, 1019 | 5126: Float32Array 1020 | }; 1021 | 1022 | var WEBGL_FILTERS = { 1023 | 9728: THREE.NearestFilter, 1024 | 9729: THREE.LinearFilter, 1025 | 9984: THREE.NearestMipmapNearestFilter, 1026 | 9985: THREE.LinearMipmapNearestFilter, 1027 | 9986: THREE.NearestMipmapLinearFilter, 1028 | 9987: THREE.LinearMipmapLinearFilter 1029 | }; 1030 | 1031 | var WEBGL_WRAPPINGS = { 1032 | 33071: THREE.ClampToEdgeWrapping, 1033 | 33648: THREE.MirroredRepeatWrapping, 1034 | 10497: THREE.RepeatWrapping 1035 | }; 1036 | 1037 | var WEBGL_TYPE_SIZES = { 1038 | 'SCALAR': 1, 1039 | 'VEC2': 2, 1040 | 'VEC3': 3, 1041 | 'VEC4': 4, 1042 | 'MAT2': 4, 1043 | 'MAT3': 9, 1044 | 'MAT4': 16 1045 | }; 1046 | 1047 | var ATTRIBUTES = { 1048 | POSITION: 'position', 1049 | NORMAL: 'normal', 1050 | TANGENT: 'tangent', 1051 | TEXCOORD_0: 'uv', 1052 | TEXCOORD_1: 'uv2', 1053 | COLOR_0: 'color', 1054 | WEIGHTS_0: 'skinWeight', 1055 | JOINTS_0: 'skinIndex', 1056 | }; 1057 | 1058 | var PATH_PROPERTIES = { 1059 | scale: 'scale', 1060 | translation: 'position', 1061 | rotation: 'quaternion', 1062 | weights: 'morphTargetInfluences' 1063 | }; 1064 | 1065 | var INTERPOLATION = { 1066 | CUBICSPLINE: undefined, // We use a custom interpolant (GLTFCubicSplineInterpolation) for CUBICSPLINE tracks. Each 1067 | // keyframe track will be initialized with a default interpolation type, then modified. 1068 | LINEAR: THREE.InterpolateLinear, 1069 | STEP: THREE.InterpolateDiscrete 1070 | }; 1071 | 1072 | var ALPHA_MODES = { 1073 | OPAQUE: 'OPAQUE', 1074 | MASK: 'MASK', 1075 | BLEND: 'BLEND' 1076 | }; 1077 | 1078 | var MIME_TYPE_FORMATS = { 1079 | 'image/png': THREE.RGBAFormat, 1080 | 'image/jpeg': THREE.RGBFormat 1081 | }; 1082 | 1083 | /* UTILITY FUNCTIONS */ 1084 | 1085 | function resolveURL( url, path ) { 1086 | 1087 | // Invalid URL 1088 | if ( typeof url !== 'string' || url === '' ) return ''; 1089 | 1090 | // Host Relative URL 1091 | if ( /^https?:\/\//i.test( path ) && /^\//.test( url ) ) { 1092 | 1093 | path = path.replace( /(^https?:\/\/[^\/]+).*/i, '$1' ); 1094 | 1095 | } 1096 | 1097 | // Absolute URL http://,https://,// 1098 | if ( /^(https?:)?\/\//i.test( url ) ) return url; 1099 | 1100 | // Data URI 1101 | if ( /^data:.*,.*$/i.test( url ) ) return url; 1102 | 1103 | // Blob URL 1104 | if ( /^blob:.*$/i.test( url ) ) return url; 1105 | 1106 | // Relative URL 1107 | return path + url; 1108 | 1109 | } 1110 | 1111 | /** 1112 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#default-material 1113 | */ 1114 | function createDefaultMaterial( cache ) { 1115 | 1116 | if ( cache[ 'DefaultMaterial' ] === undefined ) { 1117 | 1118 | cache[ 'DefaultMaterial' ] = new THREE.MeshStandardMaterial( { 1119 | color: 0xFFFFFF, 1120 | emissive: 0x000000, 1121 | metalness: 1, 1122 | roughness: 1, 1123 | transparent: false, 1124 | depthTest: true, 1125 | side: THREE.FrontSide 1126 | } ); 1127 | 1128 | } 1129 | 1130 | return cache[ 'DefaultMaterial' ]; 1131 | 1132 | } 1133 | 1134 | function addUnknownExtensionsToUserData( knownExtensions, object, objectDef ) { 1135 | 1136 | // Add unknown glTF extensions to an object's userData. 1137 | 1138 | for ( var name in objectDef.extensions ) { 1139 | 1140 | if ( knownExtensions[ name ] === undefined ) { 1141 | 1142 | object.userData.gltfExtensions = object.userData.gltfExtensions || {}; 1143 | object.userData.gltfExtensions[ name ] = objectDef.extensions[ name ]; 1144 | 1145 | } 1146 | 1147 | } 1148 | 1149 | } 1150 | 1151 | /** 1152 | * @param {THREE.Object3D|THREE.Material|THREE.BufferGeometry} object 1153 | * @param {GLTF.definition} gltfDef 1154 | */ 1155 | function assignExtrasToUserData( object, gltfDef ) { 1156 | 1157 | if ( gltfDef.extras !== undefined ) { 1158 | 1159 | if ( typeof gltfDef.extras === 'object' ) { 1160 | 1161 | Object.assign( object.userData, gltfDef.extras ); 1162 | 1163 | } else { 1164 | 1165 | console.warn( 'THREE.GLTFLoader: Ignoring primitive type .extras, ' + gltfDef.extras ); 1166 | 1167 | } 1168 | 1169 | } 1170 | 1171 | } 1172 | 1173 | /** 1174 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#morph-targets 1175 | * 1176 | * @param {THREE.BufferGeometry} geometry 1177 | * @param {Array} targets 1178 | * @param {GLTFParser} parser 1179 | * @return {Promise} 1180 | */ 1181 | function addMorphTargets( geometry, targets, parser ) { 1182 | 1183 | var hasMorphPosition = false; 1184 | var hasMorphNormal = false; 1185 | 1186 | for ( var i = 0, il = targets.length; i < il; i ++ ) { 1187 | 1188 | var target = targets[ i ]; 1189 | 1190 | if ( target.POSITION !== undefined ) hasMorphPosition = true; 1191 | if ( target.NORMAL !== undefined ) hasMorphNormal = true; 1192 | 1193 | if ( hasMorphPosition && hasMorphNormal ) break; 1194 | 1195 | } 1196 | 1197 | if ( ! hasMorphPosition && ! hasMorphNormal ) return Promise.resolve( geometry ); 1198 | 1199 | var pendingPositionAccessors = []; 1200 | var pendingNormalAccessors = []; 1201 | 1202 | for ( var i = 0, il = targets.length; i < il; i ++ ) { 1203 | 1204 | var target = targets[ i ]; 1205 | 1206 | if ( hasMorphPosition ) { 1207 | 1208 | var pendingAccessor = target.POSITION !== undefined 1209 | ? parser.getDependency( 'accessor', target.POSITION ) 1210 | : geometry.attributes.position; 1211 | 1212 | pendingPositionAccessors.push( pendingAccessor ); 1213 | 1214 | } 1215 | 1216 | if ( hasMorphNormal ) { 1217 | 1218 | var pendingAccessor = target.NORMAL !== undefined 1219 | ? parser.getDependency( 'accessor', target.NORMAL ) 1220 | : geometry.attributes.normal; 1221 | 1222 | pendingNormalAccessors.push( pendingAccessor ); 1223 | 1224 | } 1225 | 1226 | } 1227 | 1228 | return Promise.all( [ 1229 | Promise.all( pendingPositionAccessors ), 1230 | Promise.all( pendingNormalAccessors ) 1231 | ] ).then( function ( accessors ) { 1232 | 1233 | var morphPositions = accessors[ 0 ]; 1234 | var morphNormals = accessors[ 1 ]; 1235 | 1236 | if ( hasMorphPosition ) geometry.morphAttributes.position = morphPositions; 1237 | if ( hasMorphNormal ) geometry.morphAttributes.normal = morphNormals; 1238 | geometry.morphTargetsRelative = true; 1239 | 1240 | return geometry; 1241 | 1242 | } ); 1243 | 1244 | } 1245 | 1246 | /** 1247 | * @param {THREE.Mesh} mesh 1248 | * @param {GLTF.Mesh} meshDef 1249 | */ 1250 | function updateMorphTargets( mesh, meshDef ) { 1251 | 1252 | mesh.updateMorphTargets(); 1253 | 1254 | if ( meshDef.weights !== undefined ) { 1255 | 1256 | for ( var i = 0, il = meshDef.weights.length; i < il; i ++ ) { 1257 | 1258 | mesh.morphTargetInfluences[ i ] = meshDef.weights[ i ]; 1259 | 1260 | } 1261 | 1262 | } 1263 | 1264 | // .extras has user-defined data, so check that .extras.targetNames is an array. 1265 | if ( meshDef.extras && Array.isArray( meshDef.extras.targetNames ) ) { 1266 | 1267 | var targetNames = meshDef.extras.targetNames; 1268 | 1269 | if ( mesh.morphTargetInfluences.length === targetNames.length ) { 1270 | 1271 | mesh.morphTargetDictionary = {}; 1272 | 1273 | for ( var i = 0, il = targetNames.length; i < il; i ++ ) { 1274 | 1275 | mesh.morphTargetDictionary[ targetNames[ i ] ] = i; 1276 | 1277 | } 1278 | 1279 | } else { 1280 | 1281 | console.warn( 'THREE.GLTFLoader: Invalid extras.targetNames length. Ignoring names.' ); 1282 | 1283 | } 1284 | 1285 | } 1286 | 1287 | } 1288 | 1289 | function createPrimitiveKey( primitiveDef ) { 1290 | 1291 | var dracoExtension = primitiveDef.extensions && primitiveDef.extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ]; 1292 | var geometryKey; 1293 | 1294 | if ( dracoExtension ) { 1295 | 1296 | geometryKey = 'draco:' + dracoExtension.bufferView 1297 | + ':' + dracoExtension.indices 1298 | + ':' + createAttributesKey( dracoExtension.attributes ); 1299 | 1300 | } else { 1301 | 1302 | geometryKey = primitiveDef.indices + ':' + createAttributesKey( primitiveDef.attributes ) + ':' + primitiveDef.mode; 1303 | 1304 | } 1305 | 1306 | return geometryKey; 1307 | 1308 | } 1309 | 1310 | function createAttributesKey( attributes ) { 1311 | 1312 | var attributesKey = ''; 1313 | 1314 | var keys = Object.keys( attributes ).sort(); 1315 | 1316 | for ( var i = 0, il = keys.length; i < il; i ++ ) { 1317 | 1318 | attributesKey += keys[ i ] + ':' + attributes[ keys[ i ] ] + ';'; 1319 | 1320 | } 1321 | 1322 | return attributesKey; 1323 | 1324 | } 1325 | 1326 | /* GLTF PARSER */ 1327 | 1328 | function GLTFParser( json, extensions, options ) { 1329 | 1330 | this.json = json || {}; 1331 | this.extensions = extensions || {}; 1332 | this.options = options || {}; 1333 | 1334 | // loader object cache 1335 | this.cache = new GLTFRegistry(); 1336 | 1337 | // BufferGeometry caching 1338 | this.primitiveCache = {}; 1339 | 1340 | this.textureLoader = new THREE.TextureLoader( this.options.manager ); 1341 | this.textureLoader.setCrossOrigin( this.options.crossOrigin ); 1342 | 1343 | this.fileLoader = new THREE.FileLoader( this.options.manager ); 1344 | this.fileLoader.setResponseType( 'arraybuffer' ); 1345 | 1346 | if ( this.options.crossOrigin === 'use-credentials' ) { 1347 | 1348 | this.fileLoader.setWithCredentials( true ); 1349 | 1350 | } 1351 | 1352 | } 1353 | 1354 | GLTFParser.prototype.parse = function ( onLoad, onError ) { 1355 | 1356 | var parser = this; 1357 | var json = this.json; 1358 | var extensions = this.extensions; 1359 | 1360 | // Clear the loader cache 1361 | this.cache.removeAll(); 1362 | 1363 | // Mark the special nodes/meshes in json for efficient parse 1364 | this.markDefs(); 1365 | 1366 | Promise.all( [ 1367 | 1368 | this.getDependencies( 'scene' ), 1369 | this.getDependencies( 'animation' ), 1370 | this.getDependencies( 'camera' ), 1371 | 1372 | ] ).then( function ( dependencies ) { 1373 | 1374 | var result = { 1375 | scene: dependencies[ 0 ][ json.scene || 0 ], 1376 | scenes: dependencies[ 0 ], 1377 | animations: dependencies[ 1 ], 1378 | cameras: dependencies[ 2 ], 1379 | asset: json.asset, 1380 | parser: parser, 1381 | userData: {} 1382 | }; 1383 | 1384 | addUnknownExtensionsToUserData( extensions, result, json ); 1385 | 1386 | assignExtrasToUserData( result, json ); 1387 | 1388 | onLoad( result ); 1389 | 1390 | } ).catch( onError ); 1391 | 1392 | }; 1393 | 1394 | /** 1395 | * Marks the special nodes/meshes in json for efficient parse. 1396 | */ 1397 | GLTFParser.prototype.markDefs = function () { 1398 | 1399 | var nodeDefs = this.json.nodes || []; 1400 | var skinDefs = this.json.skins || []; 1401 | var meshDefs = this.json.meshes || []; 1402 | 1403 | var meshReferences = {}; 1404 | var meshUses = {}; 1405 | 1406 | // Nothing in the node definition indicates whether it is a Bone or an 1407 | // Object3D. Use the skins' joint references to mark bones. 1408 | for ( var skinIndex = 0, skinLength = skinDefs.length; skinIndex < skinLength; skinIndex ++ ) { 1409 | 1410 | var joints = skinDefs[ skinIndex ].joints; 1411 | 1412 | for ( var i = 0, il = joints.length; i < il; i ++ ) { 1413 | 1414 | nodeDefs[ joints[ i ] ].isBone = true; 1415 | 1416 | } 1417 | 1418 | } 1419 | 1420 | // Meshes can (and should) be reused by multiple nodes in a glTF asset. To 1421 | // avoid having more than one THREE.Mesh with the same name, count 1422 | // references and rename instances below. 1423 | // 1424 | // Example: CesiumMilkTruck sample model reuses "Wheel" meshes. 1425 | for ( var nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex ++ ) { 1426 | 1427 | var nodeDef = nodeDefs[ nodeIndex ]; 1428 | 1429 | if ( nodeDef.mesh !== undefined ) { 1430 | 1431 | if ( meshReferences[ nodeDef.mesh ] === undefined ) { 1432 | 1433 | meshReferences[ nodeDef.mesh ] = meshUses[ nodeDef.mesh ] = 0; 1434 | 1435 | } 1436 | 1437 | meshReferences[ nodeDef.mesh ] ++; 1438 | 1439 | // Nothing in the mesh definition indicates whether it is 1440 | // a SkinnedMesh or Mesh. Use the node's mesh reference 1441 | // to mark SkinnedMesh if node has skin. 1442 | if ( nodeDef.skin !== undefined ) { 1443 | 1444 | meshDefs[ nodeDef.mesh ].isSkinnedMesh = true; 1445 | 1446 | } 1447 | 1448 | } 1449 | 1450 | } 1451 | 1452 | this.json.meshReferences = meshReferences; 1453 | this.json.meshUses = meshUses; 1454 | 1455 | }; 1456 | 1457 | /** 1458 | * Requests the specified dependency asynchronously, with caching. 1459 | * @param {string} type 1460 | * @param {number} index 1461 | * @return {Promise} 1462 | */ 1463 | GLTFParser.prototype.getDependency = function ( type, index ) { 1464 | 1465 | var cacheKey = type + ':' + index; 1466 | var dependency = this.cache.get( cacheKey ); 1467 | 1468 | if ( ! dependency ) { 1469 | 1470 | switch ( type ) { 1471 | 1472 | case 'scene': 1473 | dependency = this.loadScene( index ); 1474 | break; 1475 | 1476 | case 'node': 1477 | dependency = this.loadNode( index ); 1478 | break; 1479 | 1480 | case 'mesh': 1481 | dependency = this.loadMesh( index ); 1482 | break; 1483 | 1484 | case 'accessor': 1485 | dependency = this.loadAccessor( index ); 1486 | break; 1487 | 1488 | case 'bufferView': 1489 | dependency = this.loadBufferView( index ); 1490 | break; 1491 | 1492 | case 'buffer': 1493 | dependency = this.loadBuffer( index ); 1494 | break; 1495 | 1496 | case 'material': 1497 | dependency = this.loadMaterial( index ); 1498 | break; 1499 | 1500 | case 'texture': 1501 | dependency = this.loadTexture( index ); 1502 | break; 1503 | 1504 | case 'skin': 1505 | dependency = this.loadSkin( index ); 1506 | break; 1507 | 1508 | case 'animation': 1509 | dependency = this.loadAnimation( index ); 1510 | break; 1511 | 1512 | case 'camera': 1513 | dependency = this.loadCamera( index ); 1514 | break; 1515 | 1516 | case 'light': 1517 | dependency = this.extensions[ EXTENSIONS.KHR_LIGHTS_PUNCTUAL ].loadLight( index ); 1518 | break; 1519 | 1520 | default: 1521 | throw new Error( 'Unknown type: ' + type ); 1522 | 1523 | } 1524 | 1525 | this.cache.add( cacheKey, dependency ); 1526 | 1527 | } 1528 | 1529 | return dependency; 1530 | 1531 | }; 1532 | 1533 | /** 1534 | * Requests all dependencies of the specified type asynchronously, with caching. 1535 | * @param {string} type 1536 | * @return {Promise>} 1537 | */ 1538 | GLTFParser.prototype.getDependencies = function ( type ) { 1539 | 1540 | var dependencies = this.cache.get( type ); 1541 | 1542 | if ( ! dependencies ) { 1543 | 1544 | var parser = this; 1545 | var defs = this.json[ type + ( type === 'mesh' ? 'es' : 's' ) ] || []; 1546 | 1547 | dependencies = Promise.all( defs.map( function ( def, index ) { 1548 | 1549 | return parser.getDependency( type, index ); 1550 | 1551 | } ) ); 1552 | 1553 | this.cache.add( type, dependencies ); 1554 | 1555 | } 1556 | 1557 | return dependencies; 1558 | 1559 | }; 1560 | 1561 | /** 1562 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views 1563 | * @param {number} bufferIndex 1564 | * @return {Promise} 1565 | */ 1566 | GLTFParser.prototype.loadBuffer = function ( bufferIndex ) { 1567 | 1568 | var bufferDef = this.json.buffers[ bufferIndex ]; 1569 | var loader = this.fileLoader; 1570 | 1571 | if ( bufferDef.type && bufferDef.type !== 'arraybuffer' ) { 1572 | 1573 | throw new Error( 'THREE.GLTFLoader: ' + bufferDef.type + ' buffer type is not supported.' ); 1574 | 1575 | } 1576 | 1577 | // If present, GLB container is required to be the first buffer. 1578 | if ( bufferDef.uri === undefined && bufferIndex === 0 ) { 1579 | 1580 | return Promise.resolve( this.extensions[ EXTENSIONS.KHR_BINARY_GLTF ].body ); 1581 | 1582 | } 1583 | 1584 | var options = this.options; 1585 | 1586 | return new Promise( function ( resolve, reject ) { 1587 | 1588 | loader.load( resolveURL( bufferDef.uri, options.path ), resolve, undefined, function () { 1589 | 1590 | reject( new Error( 'THREE.GLTFLoader: Failed to load buffer "' + bufferDef.uri + '".' ) ); 1591 | 1592 | } ); 1593 | 1594 | } ); 1595 | 1596 | }; 1597 | 1598 | /** 1599 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views 1600 | * @param {number} bufferViewIndex 1601 | * @return {Promise} 1602 | */ 1603 | GLTFParser.prototype.loadBufferView = function ( bufferViewIndex ) { 1604 | 1605 | var bufferViewDef = this.json.bufferViews[ bufferViewIndex ]; 1606 | 1607 | return this.getDependency( 'buffer', bufferViewDef.buffer ).then( function ( buffer ) { 1608 | 1609 | var byteLength = bufferViewDef.byteLength || 0; 1610 | var byteOffset = bufferViewDef.byteOffset || 0; 1611 | return buffer.slice( byteOffset, byteOffset + byteLength ); 1612 | 1613 | } ); 1614 | 1615 | }; 1616 | 1617 | /** 1618 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#accessors 1619 | * @param {number} accessorIndex 1620 | * @return {Promise} 1621 | */ 1622 | GLTFParser.prototype.loadAccessor = function ( accessorIndex ) { 1623 | 1624 | var parser = this; 1625 | var json = this.json; 1626 | 1627 | var accessorDef = this.json.accessors[ accessorIndex ]; 1628 | 1629 | if ( accessorDef.bufferView === undefined && accessorDef.sparse === undefined ) { 1630 | 1631 | // Ignore empty accessors, which may be used to declare runtime 1632 | // information about attributes coming from another source (e.g. Draco 1633 | // compression extension). 1634 | return Promise.resolve( null ); 1635 | 1636 | } 1637 | 1638 | var pendingBufferViews = []; 1639 | 1640 | if ( accessorDef.bufferView !== undefined ) { 1641 | 1642 | pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.bufferView ) ); 1643 | 1644 | } else { 1645 | 1646 | pendingBufferViews.push( null ); 1647 | 1648 | } 1649 | 1650 | if ( accessorDef.sparse !== undefined ) { 1651 | 1652 | pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.sparse.indices.bufferView ) ); 1653 | pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.sparse.values.bufferView ) ); 1654 | 1655 | } 1656 | 1657 | return Promise.all( pendingBufferViews ).then( function ( bufferViews ) { 1658 | 1659 | var bufferView = bufferViews[ 0 ]; 1660 | 1661 | var itemSize = WEBGL_TYPE_SIZES[ accessorDef.type ]; 1662 | var TypedArray = WEBGL_COMPONENT_TYPES[ accessorDef.componentType ]; 1663 | 1664 | // For VEC3: itemSize is 3, elementBytes is 4, itemBytes is 12. 1665 | var elementBytes = TypedArray.BYTES_PER_ELEMENT; 1666 | var itemBytes = elementBytes * itemSize; 1667 | var byteOffset = accessorDef.byteOffset || 0; 1668 | var byteStride = accessorDef.bufferView !== undefined ? json.bufferViews[ accessorDef.bufferView ].byteStride : undefined; 1669 | var normalized = accessorDef.normalized === true; 1670 | var array, bufferAttribute; 1671 | 1672 | // The buffer is not interleaved if the stride is the item size in bytes. 1673 | if ( byteStride && byteStride !== itemBytes ) { 1674 | 1675 | // Each "slice" of the buffer, as defined by 'count' elements of 'byteStride' bytes, gets its own InterleavedBuffer 1676 | // This makes sure that IBA.count reflects accessor.count properly 1677 | var ibSlice = Math.floor( byteOffset / byteStride ); 1678 | var ibCacheKey = 'InterleavedBuffer:' + accessorDef.bufferView + ':' + accessorDef.componentType + ':' + ibSlice + ':' + accessorDef.count; 1679 | var ib = parser.cache.get( ibCacheKey ); 1680 | 1681 | if ( ! ib ) { 1682 | 1683 | array = new TypedArray( bufferView, ibSlice * byteStride, accessorDef.count * byteStride / elementBytes ); 1684 | 1685 | // Integer parameters to IB/IBA are in array elements, not bytes. 1686 | ib = new THREE.InterleavedBuffer( array, byteStride / elementBytes ); 1687 | 1688 | parser.cache.add( ibCacheKey, ib ); 1689 | 1690 | } 1691 | 1692 | bufferAttribute = new THREE.InterleavedBufferAttribute( ib, itemSize, ( byteOffset % byteStride ) / elementBytes, normalized ); 1693 | 1694 | } else { 1695 | 1696 | if ( bufferView === null ) { 1697 | 1698 | array = new TypedArray( accessorDef.count * itemSize ); 1699 | 1700 | } else { 1701 | 1702 | array = new TypedArray( bufferView, byteOffset, accessorDef.count * itemSize ); 1703 | 1704 | } 1705 | 1706 | bufferAttribute = new THREE.BufferAttribute( array, itemSize, normalized ); 1707 | 1708 | } 1709 | 1710 | // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#sparse-accessors 1711 | if ( accessorDef.sparse !== undefined ) { 1712 | 1713 | var itemSizeIndices = WEBGL_TYPE_SIZES.SCALAR; 1714 | var TypedArrayIndices = WEBGL_COMPONENT_TYPES[ accessorDef.sparse.indices.componentType ]; 1715 | 1716 | var byteOffsetIndices = accessorDef.sparse.indices.byteOffset || 0; 1717 | var byteOffsetValues = accessorDef.sparse.values.byteOffset || 0; 1718 | 1719 | var sparseIndices = new TypedArrayIndices( bufferViews[ 1 ], byteOffsetIndices, accessorDef.sparse.count * itemSizeIndices ); 1720 | var sparseValues = new TypedArray( bufferViews[ 2 ], byteOffsetValues, accessorDef.sparse.count * itemSize ); 1721 | 1722 | if ( bufferView !== null ) { 1723 | 1724 | // Avoid modifying the original ArrayBuffer, if the bufferView wasn't initialized with zeroes. 1725 | bufferAttribute = new THREE.BufferAttribute( bufferAttribute.array.slice(), bufferAttribute.itemSize, bufferAttribute.normalized ); 1726 | 1727 | } 1728 | 1729 | for ( var i = 0, il = sparseIndices.length; i < il; i ++ ) { 1730 | 1731 | var index = sparseIndices[ i ]; 1732 | 1733 | bufferAttribute.setX( index, sparseValues[ i * itemSize ] ); 1734 | if ( itemSize >= 2 ) bufferAttribute.setY( index, sparseValues[ i * itemSize + 1 ] ); 1735 | if ( itemSize >= 3 ) bufferAttribute.setZ( index, sparseValues[ i * itemSize + 2 ] ); 1736 | if ( itemSize >= 4 ) bufferAttribute.setW( index, sparseValues[ i * itemSize + 3 ] ); 1737 | if ( itemSize >= 5 ) throw new Error( 'THREE.GLTFLoader: Unsupported itemSize in sparse BufferAttribute.' ); 1738 | 1739 | } 1740 | 1741 | } 1742 | 1743 | return bufferAttribute; 1744 | 1745 | } ); 1746 | 1747 | }; 1748 | 1749 | /** 1750 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#textures 1751 | * @param {number} textureIndex 1752 | * @return {Promise} 1753 | */ 1754 | GLTFParser.prototype.loadTexture = function ( textureIndex ) { 1755 | 1756 | var parser = this; 1757 | var json = this.json; 1758 | var options = this.options; 1759 | var textureLoader = this.textureLoader; 1760 | 1761 | var URL = window.URL || window.webkitURL; 1762 | 1763 | var textureDef = json.textures[ textureIndex ]; 1764 | 1765 | var textureExtensions = textureDef.extensions || {}; 1766 | 1767 | var source; 1768 | 1769 | if ( textureExtensions[ EXTENSIONS.MSFT_TEXTURE_DDS ] ) { 1770 | 1771 | source = json.images[ textureExtensions[ EXTENSIONS.MSFT_TEXTURE_DDS ].source ]; 1772 | 1773 | } else { 1774 | 1775 | source = json.images[ textureDef.source ]; 1776 | 1777 | } 1778 | 1779 | var sourceURI = source.uri; 1780 | var isObjectURL = false; 1781 | 1782 | if ( source.bufferView !== undefined ) { 1783 | 1784 | // Load binary image data from bufferView, if provided. 1785 | 1786 | sourceURI = parser.getDependency( 'bufferView', source.bufferView ).then( function ( bufferView ) { 1787 | 1788 | isObjectURL = true; 1789 | var blob = new Blob( [ bufferView ], { type: source.mimeType } ); 1790 | sourceURI = URL.createObjectURL( blob ); 1791 | return sourceURI; 1792 | 1793 | } ); 1794 | 1795 | } 1796 | 1797 | return Promise.resolve( sourceURI ).then( function ( sourceURI ) { 1798 | 1799 | // Load Texture resource. 1800 | 1801 | var loader = options.manager.getHandler( sourceURI ); 1802 | 1803 | if ( ! loader ) { 1804 | 1805 | loader = textureExtensions[ EXTENSIONS.MSFT_TEXTURE_DDS ] 1806 | ? parser.extensions[ EXTENSIONS.MSFT_TEXTURE_DDS ].ddsLoader 1807 | : textureLoader; 1808 | 1809 | } 1810 | 1811 | return new Promise( function ( resolve, reject ) { 1812 | 1813 | loader.load( resolveURL( sourceURI, options.path ), resolve, undefined, reject ); 1814 | 1815 | } ); 1816 | 1817 | } ).then( function ( texture ) { 1818 | 1819 | // Clean up resources and configure Texture. 1820 | 1821 | if ( isObjectURL === true ) { 1822 | 1823 | URL.revokeObjectURL( sourceURI ); 1824 | 1825 | } 1826 | 1827 | texture.flipY = false; 1828 | 1829 | if ( textureDef.name ) texture.name = textureDef.name; 1830 | 1831 | // Ignore unknown mime types, like DDS files. 1832 | if ( source.mimeType in MIME_TYPE_FORMATS ) { 1833 | 1834 | texture.format = MIME_TYPE_FORMATS[ source.mimeType ]; 1835 | 1836 | } 1837 | 1838 | var samplers = json.samplers || {}; 1839 | var sampler = samplers[ textureDef.sampler ] || {}; 1840 | 1841 | texture.magFilter = WEBGL_FILTERS[ sampler.magFilter ] || THREE.LinearFilter; 1842 | texture.minFilter = WEBGL_FILTERS[ sampler.minFilter ] || THREE.LinearMipmapLinearFilter; 1843 | texture.wrapS = WEBGL_WRAPPINGS[ sampler.wrapS ] || THREE.RepeatWrapping; 1844 | texture.wrapT = WEBGL_WRAPPINGS[ sampler.wrapT ] || THREE.RepeatWrapping; 1845 | 1846 | return texture; 1847 | 1848 | } ); 1849 | 1850 | }; 1851 | 1852 | /** 1853 | * Asynchronously assigns a texture to the given material parameters. 1854 | * @param {Object} materialParams 1855 | * @param {string} mapName 1856 | * @param {Object} mapDef 1857 | * @return {Promise} 1858 | */ 1859 | GLTFParser.prototype.assignTexture = function ( materialParams, mapName, mapDef ) { 1860 | 1861 | var parser = this; 1862 | 1863 | return this.getDependency( 'texture', mapDef.index ).then( function ( texture ) { 1864 | 1865 | if ( ! texture.isCompressedTexture ) { 1866 | 1867 | switch ( mapName ) { 1868 | 1869 | case 'aoMap': 1870 | case 'emissiveMap': 1871 | case 'metalnessMap': 1872 | case 'normalMap': 1873 | case 'roughnessMap': 1874 | texture.format = THREE.RGBFormat; 1875 | break; 1876 | 1877 | } 1878 | 1879 | } 1880 | 1881 | // Materials sample aoMap from UV set 1 and other maps from UV set 0 - this can't be configured 1882 | // However, we will copy UV set 0 to UV set 1 on demand for aoMap 1883 | if ( mapDef.texCoord !== undefined && mapDef.texCoord != 0 && ! ( mapName === 'aoMap' && mapDef.texCoord == 1 ) ) { 1884 | 1885 | console.warn( 'THREE.GLTFLoader: Custom UV set ' + mapDef.texCoord + ' for texture ' + mapName + ' not yet supported.' ); 1886 | 1887 | } 1888 | 1889 | if ( parser.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ] ) { 1890 | 1891 | var transform = mapDef.extensions !== undefined ? mapDef.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ] : undefined; 1892 | 1893 | if ( transform ) { 1894 | 1895 | texture = parser.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ].extendTexture( texture, transform ); 1896 | 1897 | } 1898 | 1899 | } 1900 | 1901 | materialParams[ mapName ] = texture; 1902 | 1903 | } ); 1904 | 1905 | }; 1906 | 1907 | /** 1908 | * Assigns final material to a Mesh, Line, or Points instance. The instance 1909 | * already has a material (generated from the glTF material options alone) 1910 | * but reuse of the same glTF material may require multiple threejs materials 1911 | * to accomodate different primitive types, defines, etc. New materials will 1912 | * be created if necessary, and reused from a cache. 1913 | * @param {THREE.Object3D} mesh Mesh, Line, or Points instance. 1914 | */ 1915 | GLTFParser.prototype.assignFinalMaterial = function ( mesh ) { 1916 | 1917 | var geometry = mesh.geometry; 1918 | var material = mesh.material; 1919 | var extensions = this.extensions; 1920 | 1921 | var useVertexTangents = geometry.attributes.tangent !== undefined; 1922 | var useVertexColors = geometry.attributes.color !== undefined; 1923 | var useFlatShading = geometry.attributes.normal === undefined; 1924 | var useSkinning = mesh.isSkinnedMesh === true; 1925 | var useMorphTargets = Object.keys( geometry.morphAttributes ).length > 0; 1926 | var useMorphNormals = useMorphTargets && geometry.morphAttributes.normal !== undefined; 1927 | 1928 | if ( mesh.isPoints ) { 1929 | 1930 | var cacheKey = 'PointsMaterial:' + material.uuid; 1931 | 1932 | var pointsMaterial = this.cache.get( cacheKey ); 1933 | 1934 | if ( ! pointsMaterial ) { 1935 | 1936 | pointsMaterial = new THREE.PointsMaterial(); 1937 | THREE.Material.prototype.copy.call( pointsMaterial, material ); 1938 | pointsMaterial.color.copy( material.color ); 1939 | pointsMaterial.map = material.map; 1940 | pointsMaterial.sizeAttenuation = false; // glTF spec says points should be 1px 1941 | 1942 | this.cache.add( cacheKey, pointsMaterial ); 1943 | 1944 | } 1945 | 1946 | material = pointsMaterial; 1947 | 1948 | } else if ( mesh.isLine ) { 1949 | 1950 | var cacheKey = 'LineBasicMaterial:' + material.uuid; 1951 | 1952 | var lineMaterial = this.cache.get( cacheKey ); 1953 | 1954 | if ( ! lineMaterial ) { 1955 | 1956 | lineMaterial = new THREE.LineBasicMaterial(); 1957 | THREE.Material.prototype.copy.call( lineMaterial, material ); 1958 | lineMaterial.color.copy( material.color ); 1959 | 1960 | this.cache.add( cacheKey, lineMaterial ); 1961 | 1962 | } 1963 | 1964 | material = lineMaterial; 1965 | 1966 | } 1967 | 1968 | // Clone the material if it will be modified 1969 | if ( useVertexTangents || useVertexColors || useFlatShading || useSkinning || useMorphTargets ) { 1970 | 1971 | var cacheKey = 'ClonedMaterial:' + material.uuid + ':'; 1972 | 1973 | if ( material.isGLTFSpecularGlossinessMaterial ) cacheKey += 'specular-glossiness:'; 1974 | if ( useSkinning ) cacheKey += 'skinning:'; 1975 | if ( useVertexTangents ) cacheKey += 'vertex-tangents:'; 1976 | if ( useVertexColors ) cacheKey += 'vertex-colors:'; 1977 | if ( useFlatShading ) cacheKey += 'flat-shading:'; 1978 | if ( useMorphTargets ) cacheKey += 'morph-targets:'; 1979 | if ( useMorphNormals ) cacheKey += 'morph-normals:'; 1980 | 1981 | var cachedMaterial = this.cache.get( cacheKey ); 1982 | 1983 | if ( ! cachedMaterial ) { 1984 | 1985 | cachedMaterial = material.clone(); 1986 | 1987 | if ( useSkinning ) cachedMaterial.skinning = true; 1988 | if ( useVertexTangents ) cachedMaterial.vertexTangents = true; 1989 | if ( useVertexColors ) cachedMaterial.vertexColors = true; 1990 | if ( useFlatShading ) cachedMaterial.flatShading = true; 1991 | if ( useMorphTargets ) cachedMaterial.morphTargets = true; 1992 | if ( useMorphNormals ) cachedMaterial.morphNormals = true; 1993 | 1994 | this.cache.add( cacheKey, cachedMaterial ); 1995 | 1996 | } 1997 | 1998 | material = cachedMaterial; 1999 | 2000 | } 2001 | 2002 | // workarounds for mesh and geometry 2003 | 2004 | if ( material.aoMap && geometry.attributes.uv2 === undefined && geometry.attributes.uv !== undefined ) { 2005 | 2006 | geometry.setAttribute( 'uv2', new THREE.BufferAttribute( geometry.attributes.uv.array, 2 ) ); 2007 | 2008 | } 2009 | 2010 | // https://github.com/mrdoob/three.js/issues/11438#issuecomment-507003995 2011 | if ( material.normalScale && ! useVertexTangents ) { 2012 | 2013 | material.normalScale.y = - material.normalScale.y; 2014 | 2015 | } 2016 | 2017 | mesh.material = material; 2018 | 2019 | }; 2020 | 2021 | /** 2022 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#materials 2023 | * @param {number} materialIndex 2024 | * @return {Promise} 2025 | */ 2026 | GLTFParser.prototype.loadMaterial = function ( materialIndex ) { 2027 | 2028 | var parser = this; 2029 | var json = this.json; 2030 | var extensions = this.extensions; 2031 | var materialDef = json.materials[ materialIndex ]; 2032 | 2033 | var materialType; 2034 | var materialParams = {}; 2035 | var materialExtensions = materialDef.extensions || {}; 2036 | 2037 | var pending = []; 2038 | 2039 | if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ] ) { 2040 | 2041 | var sgExtension = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ]; 2042 | materialType = sgExtension.getMaterialType(); 2043 | pending.push( sgExtension.extendParams( materialParams, materialDef, parser ) ); 2044 | 2045 | } else if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ] ) { 2046 | 2047 | var kmuExtension = extensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ]; 2048 | materialType = kmuExtension.getMaterialType(); 2049 | pending.push( kmuExtension.extendParams( materialParams, materialDef, parser ) ); 2050 | 2051 | } else { 2052 | 2053 | // Specification: 2054 | // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-material 2055 | 2056 | materialType = THREE.MeshStandardMaterial; 2057 | 2058 | var metallicRoughness = materialDef.pbrMetallicRoughness || {}; 2059 | 2060 | materialParams.color = new THREE.Color( 1.0, 1.0, 1.0 ); 2061 | materialParams.opacity = 1.0; 2062 | 2063 | if ( Array.isArray( metallicRoughness.baseColorFactor ) ) { 2064 | 2065 | var array = metallicRoughness.baseColorFactor; 2066 | 2067 | materialParams.color.fromArray( array ); 2068 | materialParams.opacity = array[ 3 ]; 2069 | 2070 | } 2071 | 2072 | if ( metallicRoughness.baseColorTexture !== undefined ) { 2073 | 2074 | pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture ) ); 2075 | 2076 | } 2077 | 2078 | materialParams.metalness = metallicRoughness.metallicFactor !== undefined ? metallicRoughness.metallicFactor : 1.0; 2079 | materialParams.roughness = metallicRoughness.roughnessFactor !== undefined ? metallicRoughness.roughnessFactor : 1.0; 2080 | 2081 | if ( metallicRoughness.metallicRoughnessTexture !== undefined ) { 2082 | 2083 | pending.push( parser.assignTexture( materialParams, 'metalnessMap', metallicRoughness.metallicRoughnessTexture ) ); 2084 | pending.push( parser.assignTexture( materialParams, 'roughnessMap', metallicRoughness.metallicRoughnessTexture ) ); 2085 | 2086 | } 2087 | 2088 | } 2089 | 2090 | if ( materialDef.doubleSided === true ) { 2091 | 2092 | materialParams.side = THREE.DoubleSide; 2093 | 2094 | } 2095 | 2096 | var alphaMode = materialDef.alphaMode; 2097 | 2098 | if ( alphaMode === ALPHA_MODES.BLEND ) { 2099 | 2100 | materialParams.transparent = true; 2101 | 2102 | } else if ( alphaMode === ALPHA_MODES.MASK ) { 2103 | 2104 | materialParams.alphaTest = materialDef.alphaCutoff !== undefined ? materialDef.alphaCutoff : 0.5; 2105 | 2106 | } 2107 | 2108 | if ( materialDef.normalTexture !== undefined && materialType !== THREE.MeshBasicMaterial ) { 2109 | 2110 | pending.push( parser.assignTexture( materialParams, 'normalMap', materialDef.normalTexture ) ); 2111 | 2112 | materialParams.normalScale = new THREE.Vector2( 1, 1 ); 2113 | 2114 | if ( materialDef.normalTexture.scale !== undefined ) { 2115 | 2116 | materialParams.normalScale.set( materialDef.normalTexture.scale, materialDef.normalTexture.scale ); 2117 | 2118 | } 2119 | 2120 | } 2121 | 2122 | if ( materialDef.occlusionTexture !== undefined && materialType !== THREE.MeshBasicMaterial ) { 2123 | 2124 | pending.push( parser.assignTexture( materialParams, 'aoMap', materialDef.occlusionTexture ) ); 2125 | 2126 | if ( materialDef.occlusionTexture.strength !== undefined ) { 2127 | 2128 | materialParams.aoMapIntensity = materialDef.occlusionTexture.strength; 2129 | 2130 | } 2131 | 2132 | } 2133 | 2134 | if ( materialDef.emissiveFactor !== undefined && materialType !== THREE.MeshBasicMaterial ) { 2135 | 2136 | materialParams.emissive = new THREE.Color().fromArray( materialDef.emissiveFactor ); 2137 | 2138 | } 2139 | 2140 | if ( materialDef.emissiveTexture !== undefined && materialType !== THREE.MeshBasicMaterial ) { 2141 | 2142 | pending.push( parser.assignTexture( materialParams, 'emissiveMap', materialDef.emissiveTexture ) ); 2143 | 2144 | } 2145 | 2146 | return Promise.all( pending ).then( function () { 2147 | 2148 | var material; 2149 | 2150 | if ( materialType === GLTFMeshStandardSGMaterial ) { 2151 | 2152 | material = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].createMaterial( materialParams ); 2153 | 2154 | } else { 2155 | 2156 | material = new materialType( materialParams ); 2157 | 2158 | } 2159 | 2160 | if ( materialDef.name ) material.name = materialDef.name; 2161 | 2162 | // baseColorTexture, emissiveTexture, and specularGlossinessTexture use sRGB encoding. 2163 | if ( material.map ) material.map.encoding = THREE.sRGBEncoding; 2164 | if ( material.emissiveMap ) material.emissiveMap.encoding = THREE.sRGBEncoding; 2165 | 2166 | assignExtrasToUserData( material, materialDef ); 2167 | 2168 | if ( materialDef.extensions ) addUnknownExtensionsToUserData( extensions, material, materialDef ); 2169 | 2170 | return material; 2171 | 2172 | } ); 2173 | 2174 | }; 2175 | 2176 | /** 2177 | * @param {THREE.BufferGeometry} geometry 2178 | * @param {GLTF.Primitive} primitiveDef 2179 | * @param {GLTFParser} parser 2180 | */ 2181 | function computeBounds( geometry, primitiveDef, parser ) { 2182 | 2183 | var attributes = primitiveDef.attributes; 2184 | 2185 | var box = new THREE.Box3(); 2186 | 2187 | if ( attributes.POSITION !== undefined ) { 2188 | 2189 | var accessor = parser.json.accessors[ attributes.POSITION ]; 2190 | 2191 | var min = accessor.min; 2192 | var max = accessor.max; 2193 | 2194 | // glTF requires 'min' and 'max', but VRM (which extends glTF) currently ignores that requirement. 2195 | 2196 | if ( min !== undefined && max !== undefined ) { 2197 | 2198 | box.set( 2199 | new THREE.Vector3( min[ 0 ], min[ 1 ], min[ 2 ] ), 2200 | new THREE.Vector3( max[ 0 ], max[ 1 ], max[ 2 ] ) ); 2201 | 2202 | } else { 2203 | 2204 | console.warn( 'THREE.GLTFLoader: Missing min/max properties for accessor POSITION.' ); 2205 | 2206 | return; 2207 | 2208 | } 2209 | 2210 | } else { 2211 | 2212 | return; 2213 | 2214 | } 2215 | 2216 | var targets = primitiveDef.targets; 2217 | 2218 | if ( targets !== undefined ) { 2219 | 2220 | var vector = new THREE.Vector3(); 2221 | 2222 | for ( var i = 0, il = targets.length; i < il; i ++ ) { 2223 | 2224 | var target = targets[ i ]; 2225 | 2226 | if ( target.POSITION !== undefined ) { 2227 | 2228 | var accessor = parser.json.accessors[ target.POSITION ]; 2229 | var min = accessor.min; 2230 | var max = accessor.max; 2231 | 2232 | // glTF requires 'min' and 'max', but VRM (which extends glTF) currently ignores that requirement. 2233 | 2234 | if ( min !== undefined && max !== undefined ) { 2235 | 2236 | // we need to get max of absolute components because target weight is [-1,1] 2237 | vector.setX( Math.max( Math.abs( min[ 0 ] ), Math.abs( max[ 0 ] ) ) ); 2238 | vector.setY( Math.max( Math.abs( min[ 1 ] ), Math.abs( max[ 1 ] ) ) ); 2239 | vector.setZ( Math.max( Math.abs( min[ 2 ] ), Math.abs( max[ 2 ] ) ) ); 2240 | 2241 | box.expandByVector( vector ); 2242 | 2243 | } else { 2244 | 2245 | console.warn( 'THREE.GLTFLoader: Missing min/max properties for accessor POSITION.' ); 2246 | 2247 | } 2248 | 2249 | } 2250 | 2251 | } 2252 | 2253 | } 2254 | 2255 | geometry.boundingBox = box; 2256 | 2257 | var sphere = new THREE.Sphere(); 2258 | 2259 | box.getCenter( sphere.center ); 2260 | sphere.radius = box.min.distanceTo( box.max ) / 2; 2261 | 2262 | geometry.boundingSphere = sphere; 2263 | 2264 | } 2265 | 2266 | /** 2267 | * @param {THREE.BufferGeometry} geometry 2268 | * @param {GLTF.Primitive} primitiveDef 2269 | * @param {GLTFParser} parser 2270 | * @return {Promise} 2271 | */ 2272 | function addPrimitiveAttributes( geometry, primitiveDef, parser ) { 2273 | 2274 | var attributes = primitiveDef.attributes; 2275 | 2276 | var pending = []; 2277 | 2278 | function assignAttributeAccessor( accessorIndex, attributeName ) { 2279 | 2280 | return parser.getDependency( 'accessor', accessorIndex ) 2281 | .then( function ( accessor ) { 2282 | 2283 | geometry.setAttribute( attributeName, accessor ); 2284 | 2285 | } ); 2286 | 2287 | } 2288 | 2289 | for ( var gltfAttributeName in attributes ) { 2290 | 2291 | var threeAttributeName = ATTRIBUTES[ gltfAttributeName ] || gltfAttributeName.toLowerCase(); 2292 | 2293 | // Skip attributes already provided by e.g. Draco extension. 2294 | if ( threeAttributeName in geometry.attributes ) continue; 2295 | 2296 | pending.push( assignAttributeAccessor( attributes[ gltfAttributeName ], threeAttributeName ) ); 2297 | 2298 | } 2299 | 2300 | if ( primitiveDef.indices !== undefined && ! geometry.index ) { 2301 | 2302 | var accessor = parser.getDependency( 'accessor', primitiveDef.indices ).then( function ( accessor ) { 2303 | 2304 | geometry.setIndex( accessor ); 2305 | 2306 | } ); 2307 | 2308 | pending.push( accessor ); 2309 | 2310 | } 2311 | 2312 | assignExtrasToUserData( geometry, primitiveDef ); 2313 | 2314 | computeBounds( geometry, primitiveDef, parser ); 2315 | 2316 | return Promise.all( pending ).then( function () { 2317 | 2318 | return primitiveDef.targets !== undefined 2319 | ? addMorphTargets( geometry, primitiveDef.targets, parser ) 2320 | : geometry; 2321 | 2322 | } ); 2323 | 2324 | } 2325 | 2326 | /** 2327 | * @param {THREE.BufferGeometry} geometry 2328 | * @param {Number} drawMode 2329 | * @return {THREE.BufferGeometry} 2330 | */ 2331 | function toTrianglesDrawMode( geometry, drawMode ) { 2332 | 2333 | var index = geometry.getIndex(); 2334 | 2335 | // generate index if not present 2336 | 2337 | if ( index === null ) { 2338 | 2339 | var indices = []; 2340 | 2341 | var position = geometry.getAttribute( 'position' ); 2342 | 2343 | if ( position !== undefined ) { 2344 | 2345 | for ( var i = 0; i < position.count; i ++ ) { 2346 | 2347 | indices.push( i ); 2348 | 2349 | } 2350 | 2351 | geometry.setIndex( indices ); 2352 | index = geometry.getIndex(); 2353 | 2354 | } else { 2355 | 2356 | console.error( 'THREE.GLTFLoader.toTrianglesDrawMode(): Undefined position attribute. Processing not possible.' ); 2357 | return geometry; 2358 | 2359 | } 2360 | 2361 | } 2362 | 2363 | // 2364 | 2365 | var numberOfTriangles = index.count - 2; 2366 | var newIndices = []; 2367 | 2368 | if ( drawMode === THREE.TriangleFanDrawMode ) { 2369 | 2370 | // gl.TRIANGLE_FAN 2371 | 2372 | for ( var i = 1; i <= numberOfTriangles; i ++ ) { 2373 | 2374 | newIndices.push( index.getX( 0 ) ); 2375 | newIndices.push( index.getX( i ) ); 2376 | newIndices.push( index.getX( i + 1 ) ); 2377 | 2378 | } 2379 | 2380 | } else { 2381 | 2382 | // gl.TRIANGLE_STRIP 2383 | 2384 | for ( var i = 0; i < numberOfTriangles; i ++ ) { 2385 | 2386 | if ( i % 2 === 0 ) { 2387 | 2388 | newIndices.push( index.getX( i ) ); 2389 | newIndices.push( index.getX( i + 1 ) ); 2390 | newIndices.push( index.getX( i + 2 ) ); 2391 | 2392 | 2393 | } else { 2394 | 2395 | newIndices.push( index.getX( i + 2 ) ); 2396 | newIndices.push( index.getX( i + 1 ) ); 2397 | newIndices.push( index.getX( i ) ); 2398 | 2399 | } 2400 | 2401 | } 2402 | 2403 | } 2404 | 2405 | if ( ( newIndices.length / 3 ) !== numberOfTriangles ) { 2406 | 2407 | console.error( 'THREE.GLTFLoader.toTrianglesDrawMode(): Unable to generate correct amount of triangles.' ); 2408 | 2409 | } 2410 | 2411 | // build final geometry 2412 | 2413 | var newGeometry = geometry.clone(); 2414 | newGeometry.setIndex( newIndices ); 2415 | 2416 | return newGeometry; 2417 | 2418 | } 2419 | 2420 | /** 2421 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#geometry 2422 | * 2423 | * Creates BufferGeometries from primitives. 2424 | * 2425 | * @param {Array} primitives 2426 | * @return {Promise>} 2427 | */ 2428 | GLTFParser.prototype.loadGeometries = function ( primitives ) { 2429 | 2430 | var parser = this; 2431 | var extensions = this.extensions; 2432 | var cache = this.primitiveCache; 2433 | 2434 | function createDracoPrimitive( primitive ) { 2435 | 2436 | return extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ] 2437 | .decodePrimitive( primitive, parser ) 2438 | .then( function ( geometry ) { 2439 | 2440 | return addPrimitiveAttributes( geometry, primitive, parser ); 2441 | 2442 | } ); 2443 | 2444 | } 2445 | 2446 | var pending = []; 2447 | 2448 | for ( var i = 0, il = primitives.length; i < il; i ++ ) { 2449 | 2450 | var primitive = primitives[ i ]; 2451 | var cacheKey = createPrimitiveKey( primitive ); 2452 | 2453 | // See if we've already created this geometry 2454 | var cached = cache[ cacheKey ]; 2455 | 2456 | if ( cached ) { 2457 | 2458 | // Use the cached geometry if it exists 2459 | pending.push( cached.promise ); 2460 | 2461 | } else { 2462 | 2463 | var geometryPromise; 2464 | 2465 | if ( primitive.extensions && primitive.extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ] ) { 2466 | 2467 | // Use DRACO geometry if available 2468 | geometryPromise = createDracoPrimitive( primitive ); 2469 | 2470 | } else { 2471 | 2472 | // Otherwise create a new geometry 2473 | geometryPromise = addPrimitiveAttributes( new THREE.BufferGeometry(), primitive, parser ); 2474 | 2475 | } 2476 | 2477 | // Cache this geometry 2478 | cache[ cacheKey ] = { primitive: primitive, promise: geometryPromise }; 2479 | 2480 | pending.push( geometryPromise ); 2481 | 2482 | } 2483 | 2484 | } 2485 | 2486 | return Promise.all( pending ); 2487 | 2488 | }; 2489 | 2490 | /** 2491 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#meshes 2492 | * @param {number} meshIndex 2493 | * @return {Promise} 2494 | */ 2495 | GLTFParser.prototype.loadMesh = function ( meshIndex ) { 2496 | 2497 | var parser = this; 2498 | var json = this.json; 2499 | 2500 | var meshDef = json.meshes[ meshIndex ]; 2501 | var primitives = meshDef.primitives; 2502 | 2503 | var pending = []; 2504 | 2505 | for ( var i = 0, il = primitives.length; i < il; i ++ ) { 2506 | 2507 | var material = primitives[ i ].material === undefined 2508 | ? createDefaultMaterial( this.cache ) 2509 | : this.getDependency( 'material', primitives[ i ].material ); 2510 | 2511 | pending.push( material ); 2512 | 2513 | } 2514 | 2515 | pending.push( parser.loadGeometries( primitives ) ); 2516 | 2517 | return Promise.all( pending ).then( function ( results ) { 2518 | 2519 | var materials = results.slice( 0, results.length - 1 ); 2520 | var geometries = results[ results.length - 1 ]; 2521 | 2522 | var meshes = []; 2523 | 2524 | for ( var i = 0, il = geometries.length; i < il; i ++ ) { 2525 | 2526 | var geometry = geometries[ i ]; 2527 | var primitive = primitives[ i ]; 2528 | 2529 | // 1. create Mesh 2530 | 2531 | var mesh; 2532 | 2533 | var material = materials[ i ]; 2534 | 2535 | if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLES || 2536 | primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP || 2537 | primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN || 2538 | primitive.mode === undefined ) { 2539 | 2540 | // .isSkinnedMesh isn't in glTF spec. See .markDefs() 2541 | mesh = meshDef.isSkinnedMesh === true 2542 | ? new THREE.SkinnedMesh( geometry, material ) 2543 | : new THREE.Mesh( geometry, material ); 2544 | 2545 | if ( mesh.isSkinnedMesh === true && ! mesh.geometry.attributes.skinWeight.normalized ) { 2546 | 2547 | // we normalize floating point skin weight array to fix malformed assets (see #15319) 2548 | // it's important to skip this for non-float32 data since normalizeSkinWeights assumes non-normalized inputs 2549 | mesh.normalizeSkinWeights(); 2550 | 2551 | } 2552 | 2553 | if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ) { 2554 | 2555 | mesh.geometry = toTrianglesDrawMode( mesh.geometry, THREE.TriangleStripDrawMode ); 2556 | 2557 | } else if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN ) { 2558 | 2559 | mesh.geometry = toTrianglesDrawMode( mesh.geometry, THREE.TriangleFanDrawMode ); 2560 | 2561 | } 2562 | 2563 | } else if ( primitive.mode === WEBGL_CONSTANTS.LINES ) { 2564 | 2565 | mesh = new THREE.LineSegments( geometry, material ); 2566 | 2567 | } else if ( primitive.mode === WEBGL_CONSTANTS.LINE_STRIP ) { 2568 | 2569 | mesh = new THREE.Line( geometry, material ); 2570 | 2571 | } else if ( primitive.mode === WEBGL_CONSTANTS.LINE_LOOP ) { 2572 | 2573 | mesh = new THREE.LineLoop( geometry, material ); 2574 | 2575 | } else if ( primitive.mode === WEBGL_CONSTANTS.POINTS ) { 2576 | 2577 | mesh = new THREE.Points( geometry, material ); 2578 | 2579 | } else { 2580 | 2581 | throw new Error( 'THREE.GLTFLoader: Primitive mode unsupported: ' + primitive.mode ); 2582 | 2583 | } 2584 | 2585 | if ( Object.keys( mesh.geometry.morphAttributes ).length > 0 ) { 2586 | 2587 | updateMorphTargets( mesh, meshDef ); 2588 | 2589 | } 2590 | 2591 | mesh.name = meshDef.name || ( 'mesh_' + meshIndex ); 2592 | 2593 | if ( geometries.length > 1 ) mesh.name += '_' + i; 2594 | 2595 | assignExtrasToUserData( mesh, meshDef ); 2596 | 2597 | parser.assignFinalMaterial( mesh ); 2598 | 2599 | meshes.push( mesh ); 2600 | 2601 | } 2602 | 2603 | if ( meshes.length === 1 ) { 2604 | 2605 | return meshes[ 0 ]; 2606 | 2607 | } 2608 | 2609 | var group = new THREE.Group(); 2610 | 2611 | for ( var i = 0, il = meshes.length; i < il; i ++ ) { 2612 | 2613 | group.add( meshes[ i ] ); 2614 | 2615 | } 2616 | 2617 | return group; 2618 | 2619 | } ); 2620 | 2621 | }; 2622 | 2623 | /** 2624 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#cameras 2625 | * @param {number} cameraIndex 2626 | * @return {Promise} 2627 | */ 2628 | GLTFParser.prototype.loadCamera = function ( cameraIndex ) { 2629 | 2630 | var camera; 2631 | var cameraDef = this.json.cameras[ cameraIndex ]; 2632 | var params = cameraDef[ cameraDef.type ]; 2633 | 2634 | if ( ! params ) { 2635 | 2636 | console.warn( 'THREE.GLTFLoader: Missing camera parameters.' ); 2637 | return; 2638 | 2639 | } 2640 | 2641 | if ( cameraDef.type === 'perspective' ) { 2642 | 2643 | camera = new THREE.PerspectiveCamera( THREE.MathUtils.radToDeg( params.yfov ), params.aspectRatio || 1, params.znear || 1, params.zfar || 2e6 ); 2644 | 2645 | } else if ( cameraDef.type === 'orthographic' ) { 2646 | 2647 | camera = new THREE.OrthographicCamera( params.xmag / - 2, params.xmag / 2, params.ymag / 2, params.ymag / - 2, params.znear, params.zfar ); 2648 | 2649 | } 2650 | 2651 | if ( cameraDef.name ) camera.name = cameraDef.name; 2652 | 2653 | assignExtrasToUserData( camera, cameraDef ); 2654 | 2655 | return Promise.resolve( camera ); 2656 | 2657 | }; 2658 | 2659 | /** 2660 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#skins 2661 | * @param {number} skinIndex 2662 | * @return {Promise} 2663 | */ 2664 | GLTFParser.prototype.loadSkin = function ( skinIndex ) { 2665 | 2666 | var skinDef = this.json.skins[ skinIndex ]; 2667 | 2668 | var skinEntry = { joints: skinDef.joints }; 2669 | 2670 | if ( skinDef.inverseBindMatrices === undefined ) { 2671 | 2672 | return Promise.resolve( skinEntry ); 2673 | 2674 | } 2675 | 2676 | return this.getDependency( 'accessor', skinDef.inverseBindMatrices ).then( function ( accessor ) { 2677 | 2678 | skinEntry.inverseBindMatrices = accessor; 2679 | 2680 | return skinEntry; 2681 | 2682 | } ); 2683 | 2684 | }; 2685 | 2686 | /** 2687 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#animations 2688 | * @param {number} animationIndex 2689 | * @return {Promise} 2690 | */ 2691 | GLTFParser.prototype.loadAnimation = function ( animationIndex ) { 2692 | 2693 | var json = this.json; 2694 | 2695 | var animationDef = json.animations[ animationIndex ]; 2696 | 2697 | var pendingNodes = []; 2698 | var pendingInputAccessors = []; 2699 | var pendingOutputAccessors = []; 2700 | var pendingSamplers = []; 2701 | var pendingTargets = []; 2702 | 2703 | for ( var i = 0, il = animationDef.channels.length; i < il; i ++ ) { 2704 | 2705 | var channel = animationDef.channels[ i ]; 2706 | var sampler = animationDef.samplers[ channel.sampler ]; 2707 | var target = channel.target; 2708 | var name = target.node !== undefined ? target.node : target.id; // NOTE: target.id is deprecated. 2709 | var input = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.input ] : sampler.input; 2710 | var output = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.output ] : sampler.output; 2711 | 2712 | pendingNodes.push( this.getDependency( 'node', name ) ); 2713 | pendingInputAccessors.push( this.getDependency( 'accessor', input ) ); 2714 | pendingOutputAccessors.push( this.getDependency( 'accessor', output ) ); 2715 | pendingSamplers.push( sampler ); 2716 | pendingTargets.push( target ); 2717 | 2718 | } 2719 | 2720 | return Promise.all( [ 2721 | 2722 | Promise.all( pendingNodes ), 2723 | Promise.all( pendingInputAccessors ), 2724 | Promise.all( pendingOutputAccessors ), 2725 | Promise.all( pendingSamplers ), 2726 | Promise.all( pendingTargets ) 2727 | 2728 | ] ).then( function ( dependencies ) { 2729 | 2730 | var nodes = dependencies[ 0 ]; 2731 | var inputAccessors = dependencies[ 1 ]; 2732 | var outputAccessors = dependencies[ 2 ]; 2733 | var samplers = dependencies[ 3 ]; 2734 | var targets = dependencies[ 4 ]; 2735 | 2736 | var tracks = []; 2737 | 2738 | for ( var i = 0, il = nodes.length; i < il; i ++ ) { 2739 | 2740 | var node = nodes[ i ]; 2741 | var inputAccessor = inputAccessors[ i ]; 2742 | var outputAccessor = outputAccessors[ i ]; 2743 | var sampler = samplers[ i ]; 2744 | var target = targets[ i ]; 2745 | 2746 | if ( node === undefined ) continue; 2747 | 2748 | node.updateMatrix(); 2749 | node.matrixAutoUpdate = true; 2750 | 2751 | var TypedKeyframeTrack; 2752 | 2753 | switch ( PATH_PROPERTIES[ target.path ] ) { 2754 | 2755 | case PATH_PROPERTIES.weights: 2756 | 2757 | TypedKeyframeTrack = THREE.NumberKeyframeTrack; 2758 | break; 2759 | 2760 | case PATH_PROPERTIES.rotation: 2761 | 2762 | TypedKeyframeTrack = THREE.QuaternionKeyframeTrack; 2763 | break; 2764 | 2765 | case PATH_PROPERTIES.position: 2766 | case PATH_PROPERTIES.scale: 2767 | default: 2768 | 2769 | TypedKeyframeTrack = THREE.VectorKeyframeTrack; 2770 | break; 2771 | 2772 | } 2773 | 2774 | var targetName = node.name ? node.name : node.uuid; 2775 | 2776 | var interpolation = sampler.interpolation !== undefined ? INTERPOLATION[ sampler.interpolation ] : THREE.InterpolateLinear; 2777 | 2778 | var targetNames = []; 2779 | 2780 | if ( PATH_PROPERTIES[ target.path ] === PATH_PROPERTIES.weights ) { 2781 | 2782 | // Node may be a THREE.Group (glTF mesh with several primitives) or a THREE.Mesh. 2783 | node.traverse( function ( object ) { 2784 | 2785 | if ( object.isMesh === true && object.morphTargetInfluences ) { 2786 | 2787 | targetNames.push( object.name ? object.name : object.uuid ); 2788 | 2789 | } 2790 | 2791 | } ); 2792 | 2793 | } else { 2794 | 2795 | targetNames.push( targetName ); 2796 | 2797 | } 2798 | 2799 | var outputArray = outputAccessor.array; 2800 | 2801 | if ( outputAccessor.normalized ) { 2802 | 2803 | var scale; 2804 | 2805 | if ( outputArray.constructor === Int8Array ) { 2806 | 2807 | scale = 1 / 127; 2808 | 2809 | } else if ( outputArray.constructor === Uint8Array ) { 2810 | 2811 | scale = 1 / 255; 2812 | 2813 | } else if ( outputArray.constructor == Int16Array ) { 2814 | 2815 | scale = 1 / 32767; 2816 | 2817 | } else if ( outputArray.constructor === Uint16Array ) { 2818 | 2819 | scale = 1 / 65535; 2820 | 2821 | } else { 2822 | 2823 | throw new Error( 'THREE.GLTFLoader: Unsupported output accessor component type.' ); 2824 | 2825 | } 2826 | 2827 | var scaled = new Float32Array( outputArray.length ); 2828 | 2829 | for ( var j = 0, jl = outputArray.length; j < jl; j ++ ) { 2830 | 2831 | scaled[ j ] = outputArray[ j ] * scale; 2832 | 2833 | } 2834 | 2835 | outputArray = scaled; 2836 | 2837 | } 2838 | 2839 | for ( var j = 0, jl = targetNames.length; j < jl; j ++ ) { 2840 | 2841 | var track = new TypedKeyframeTrack( 2842 | targetNames[ j ] + '.' + PATH_PROPERTIES[ target.path ], 2843 | inputAccessor.array, 2844 | outputArray, 2845 | interpolation 2846 | ); 2847 | 2848 | // Override interpolation with custom factory method. 2849 | if ( sampler.interpolation === 'CUBICSPLINE' ) { 2850 | 2851 | track.createInterpolant = function InterpolantFactoryMethodGLTFCubicSpline( result ) { 2852 | 2853 | // A CUBICSPLINE keyframe in glTF has three output values for each input value, 2854 | // representing inTangent, splineVertex, and outTangent. As a result, track.getValueSize() 2855 | // must be divided by three to get the interpolant's sampleSize argument. 2856 | 2857 | return new GLTFCubicSplineInterpolant( this.times, this.values, this.getValueSize() / 3, result ); 2858 | 2859 | }; 2860 | 2861 | // Mark as CUBICSPLINE. `track.getInterpolation()` doesn't support custom interpolants. 2862 | track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline = true; 2863 | 2864 | } 2865 | 2866 | tracks.push( track ); 2867 | 2868 | } 2869 | 2870 | } 2871 | 2872 | var name = animationDef.name ? animationDef.name : 'animation_' + animationIndex; 2873 | 2874 | return new THREE.AnimationClip( name, undefined, tracks ); 2875 | 2876 | } ); 2877 | 2878 | }; 2879 | 2880 | /** 2881 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodes-and-hierarchy 2882 | * @param {number} nodeIndex 2883 | * @return {Promise} 2884 | */ 2885 | GLTFParser.prototype.loadNode = function ( nodeIndex ) { 2886 | 2887 | var json = this.json; 2888 | var extensions = this.extensions; 2889 | var parser = this; 2890 | 2891 | var meshReferences = json.meshReferences; 2892 | var meshUses = json.meshUses; 2893 | 2894 | var nodeDef = json.nodes[ nodeIndex ]; 2895 | 2896 | return ( function () { 2897 | 2898 | var pending = []; 2899 | 2900 | if ( nodeDef.mesh !== undefined ) { 2901 | 2902 | pending.push( parser.getDependency( 'mesh', nodeDef.mesh ).then( function ( mesh ) { 2903 | 2904 | var node; 2905 | 2906 | if ( meshReferences[ nodeDef.mesh ] > 1 ) { 2907 | 2908 | var instanceNum = meshUses[ nodeDef.mesh ] ++; 2909 | 2910 | node = mesh.clone(); 2911 | node.name += '_instance_' + instanceNum; 2912 | 2913 | } else { 2914 | 2915 | node = mesh; 2916 | 2917 | } 2918 | 2919 | // if weights are provided on the node, override weights on the mesh. 2920 | if ( nodeDef.weights !== undefined ) { 2921 | 2922 | node.traverse( function ( o ) { 2923 | 2924 | if ( ! o.isMesh ) return; 2925 | 2926 | for ( var i = 0, il = nodeDef.weights.length; i < il; i ++ ) { 2927 | 2928 | o.morphTargetInfluences[ i ] = nodeDef.weights[ i ]; 2929 | 2930 | } 2931 | 2932 | } ); 2933 | 2934 | } 2935 | 2936 | return node; 2937 | 2938 | } ) ); 2939 | 2940 | } 2941 | 2942 | if ( nodeDef.camera !== undefined ) { 2943 | 2944 | pending.push( parser.getDependency( 'camera', nodeDef.camera ) ); 2945 | 2946 | } 2947 | 2948 | if ( nodeDef.extensions 2949 | && nodeDef.extensions[ EXTENSIONS.KHR_LIGHTS_PUNCTUAL ] 2950 | && nodeDef.extensions[ EXTENSIONS.KHR_LIGHTS_PUNCTUAL ].light !== undefined ) { 2951 | 2952 | pending.push( parser.getDependency( 'light', nodeDef.extensions[ EXTENSIONS.KHR_LIGHTS_PUNCTUAL ].light ) ); 2953 | 2954 | } 2955 | 2956 | return Promise.all( pending ); 2957 | 2958 | }() ).then( function ( objects ) { 2959 | 2960 | var node; 2961 | 2962 | // .isBone isn't in glTF spec. See .markDefs 2963 | if ( nodeDef.isBone === true ) { 2964 | 2965 | node = new THREE.Bone(); 2966 | 2967 | } else if ( objects.length > 1 ) { 2968 | 2969 | node = new THREE.Group(); 2970 | 2971 | } else if ( objects.length === 1 ) { 2972 | 2973 | node = objects[ 0 ]; 2974 | 2975 | } else { 2976 | 2977 | node = new THREE.Object3D(); 2978 | 2979 | } 2980 | 2981 | if ( node !== objects[ 0 ] ) { 2982 | 2983 | for ( var i = 0, il = objects.length; i < il; i ++ ) { 2984 | 2985 | node.add( objects[ i ] ); 2986 | 2987 | } 2988 | 2989 | } 2990 | 2991 | if ( nodeDef.name ) { 2992 | 2993 | node.userData.name = nodeDef.name; 2994 | node.name = THREE.PropertyBinding.sanitizeNodeName( nodeDef.name ); 2995 | 2996 | } 2997 | 2998 | assignExtrasToUserData( node, nodeDef ); 2999 | 3000 | if ( nodeDef.extensions ) addUnknownExtensionsToUserData( extensions, node, nodeDef ); 3001 | 3002 | if ( nodeDef.matrix !== undefined ) { 3003 | 3004 | var matrix = new THREE.Matrix4(); 3005 | matrix.fromArray( nodeDef.matrix ); 3006 | node.applyMatrix4( matrix ); 3007 | 3008 | } else { 3009 | 3010 | if ( nodeDef.translation !== undefined ) { 3011 | 3012 | node.position.fromArray( nodeDef.translation ); 3013 | 3014 | } 3015 | 3016 | if ( nodeDef.rotation !== undefined ) { 3017 | 3018 | node.quaternion.fromArray( nodeDef.rotation ); 3019 | 3020 | } 3021 | 3022 | if ( nodeDef.scale !== undefined ) { 3023 | 3024 | node.scale.fromArray( nodeDef.scale ); 3025 | 3026 | } 3027 | 3028 | } 3029 | 3030 | return node; 3031 | 3032 | } ); 3033 | 3034 | }; 3035 | 3036 | /** 3037 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#scenes 3038 | * @param {number} sceneIndex 3039 | * @return {Promise} 3040 | */ 3041 | GLTFParser.prototype.loadScene = function () { 3042 | 3043 | // scene node hierachy builder 3044 | 3045 | function buildNodeHierachy( nodeId, parentObject, json, parser ) { 3046 | 3047 | var nodeDef = json.nodes[ nodeId ]; 3048 | 3049 | return parser.getDependency( 'node', nodeId ).then( function ( node ) { 3050 | 3051 | if ( nodeDef.skin === undefined ) return node; 3052 | 3053 | // build skeleton here as well 3054 | 3055 | var skinEntry; 3056 | 3057 | return parser.getDependency( 'skin', nodeDef.skin ).then( function ( skin ) { 3058 | 3059 | skinEntry = skin; 3060 | 3061 | var pendingJoints = []; 3062 | 3063 | for ( var i = 0, il = skinEntry.joints.length; i < il; i ++ ) { 3064 | 3065 | pendingJoints.push( parser.getDependency( 'node', skinEntry.joints[ i ] ) ); 3066 | 3067 | } 3068 | 3069 | return Promise.all( pendingJoints ); 3070 | 3071 | } ).then( function ( jointNodes ) { 3072 | 3073 | node.traverse( function ( mesh ) { 3074 | 3075 | if ( ! mesh.isMesh ) return; 3076 | 3077 | var bones = []; 3078 | var boneInverses = []; 3079 | 3080 | for ( var j = 0, jl = jointNodes.length; j < jl; j ++ ) { 3081 | 3082 | var jointNode = jointNodes[ j ]; 3083 | 3084 | if ( jointNode ) { 3085 | 3086 | bones.push( jointNode ); 3087 | 3088 | var mat = new THREE.Matrix4(); 3089 | 3090 | if ( skinEntry.inverseBindMatrices !== undefined ) { 3091 | 3092 | mat.fromArray( skinEntry.inverseBindMatrices.array, j * 16 ); 3093 | 3094 | } 3095 | 3096 | boneInverses.push( mat ); 3097 | 3098 | } else { 3099 | 3100 | console.warn( 'THREE.GLTFLoader: Joint "%s" could not be found.', skinEntry.joints[ j ] ); 3101 | 3102 | } 3103 | 3104 | } 3105 | 3106 | mesh.bind( new THREE.Skeleton( bones, boneInverses ), mesh.matrixWorld ); 3107 | 3108 | } ); 3109 | 3110 | return node; 3111 | 3112 | } ); 3113 | 3114 | } ).then( function ( node ) { 3115 | 3116 | // build node hierachy 3117 | 3118 | parentObject.add( node ); 3119 | 3120 | var pending = []; 3121 | 3122 | if ( nodeDef.children ) { 3123 | 3124 | var children = nodeDef.children; 3125 | 3126 | for ( var i = 0, il = children.length; i < il; i ++ ) { 3127 | 3128 | var child = children[ i ]; 3129 | pending.push( buildNodeHierachy( child, node, json, parser ) ); 3130 | 3131 | } 3132 | 3133 | } 3134 | 3135 | return Promise.all( pending ); 3136 | 3137 | } ); 3138 | 3139 | } 3140 | 3141 | return function loadScene( sceneIndex ) { 3142 | 3143 | var json = this.json; 3144 | var extensions = this.extensions; 3145 | var sceneDef = this.json.scenes[ sceneIndex ]; 3146 | var parser = this; 3147 | 3148 | // Loader returns Group, not Scene. 3149 | // See: https://github.com/mrdoob/three.js/issues/18342#issuecomment-578981172 3150 | var scene = new THREE.Group(); 3151 | if ( sceneDef.name ) scene.name = sceneDef.name; 3152 | 3153 | assignExtrasToUserData( scene, sceneDef ); 3154 | 3155 | if ( sceneDef.extensions ) addUnknownExtensionsToUserData( extensions, scene, sceneDef ); 3156 | 3157 | var nodeIds = sceneDef.nodes || []; 3158 | 3159 | var pending = []; 3160 | 3161 | for ( var i = 0, il = nodeIds.length; i < il; i ++ ) { 3162 | 3163 | pending.push( buildNodeHierachy( nodeIds[ i ], scene, json, parser ) ); 3164 | 3165 | } 3166 | 3167 | return Promise.all( pending ).then( function () { 3168 | 3169 | return scene; 3170 | 3171 | } ); 3172 | 3173 | }; 3174 | 3175 | }(); 3176 | 3177 | return GLTFLoader; 3178 | 3179 | } )(); 3180 | -------------------------------------------------------------------------------- /vendor/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 | * @author ScieCode / http://github.com/sciecode 8 | */ 9 | 10 | // This set of controls performs orbiting, dollying (zooming), and panning. 11 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 12 | // 13 | // Orbit - left mouse / touch: one-finger move 14 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 15 | // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move 16 | 17 | THREE.OrbitControls = function ( object, domElement ) { 18 | 19 | if ( domElement === undefined ) console.warn( 'THREE.OrbitControls: The second parameter "domElement" is now mandatory.' ); 20 | if ( domElement === document ) console.error( 'THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.' ); 21 | 22 | this.object = object; 23 | this.domElement = domElement; 24 | 25 | // Set to false to disable this control 26 | this.enabled = true; 27 | 28 | // "target" sets the location of focus, where the object orbits around 29 | this.target = new THREE.Vector3(); 30 | 31 | // How far you can dolly in and out ( PerspectiveCamera only ) 32 | this.minDistance = 0; 33 | this.maxDistance = Infinity; 34 | 35 | // How far you can zoom in and out ( OrthographicCamera only ) 36 | this.minZoom = 0; 37 | this.maxZoom = Infinity; 38 | 39 | // How far you can orbit vertically, upper and lower limits. 40 | // Range is 0 to Math.PI radians. 41 | this.minPolarAngle = 0; // radians 42 | this.maxPolarAngle = Math.PI; // radians 43 | 44 | // How far you can orbit horizontally, upper and lower limits. 45 | // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. 46 | this.minAzimuthAngle = - Infinity; // radians 47 | this.maxAzimuthAngle = Infinity; // radians 48 | 49 | // Set to true to enable damping (inertia) 50 | // If damping is enabled, you must call controls.update() in your animation loop 51 | this.enableDamping = false; 52 | this.dampingFactor = 0.05; 53 | 54 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 55 | // Set to false to disable zooming 56 | this.enableZoom = true; 57 | this.zoomSpeed = 1.0; 58 | 59 | // Set to false to disable rotating 60 | this.enableRotate = true; 61 | this.rotateSpeed = 1.0; 62 | 63 | // Set to false to disable panning 64 | this.enablePan = true; 65 | this.panSpeed = 1.0; 66 | this.screenSpacePanning = false; // if true, pan in screen-space 67 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 68 | 69 | // Set to true to automatically rotate around the target 70 | // If auto-rotate is enabled, you must call controls.update() in your animation loop 71 | this.autoRotate = false; 72 | this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 73 | 74 | // Set to false to disable use of the keys 75 | this.enableKeys = true; 76 | 77 | // The four arrow keys 78 | this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; 79 | 80 | // Mouse buttons 81 | this.mouseButtons = { LEFT: THREE.MOUSE.ROTATE, MIDDLE: THREE.MOUSE.DOLLY, RIGHT: THREE.MOUSE.PAN }; 82 | 83 | // Touch fingers 84 | this.touches = { ONE: THREE.TOUCH.ROTATE, TWO: THREE.TOUCH.DOLLY_PAN }; 85 | 86 | // for reset 87 | this.target0 = this.target.clone(); 88 | this.position0 = this.object.position.clone(); 89 | this.zoom0 = this.object.zoom; 90 | 91 | // 92 | // public methods 93 | // 94 | 95 | this.getPolarAngle = function () { 96 | 97 | return spherical.phi; 98 | 99 | }; 100 | 101 | this.getAzimuthalAngle = function () { 102 | 103 | return spherical.theta; 104 | 105 | }; 106 | 107 | this.saveState = function () { 108 | 109 | scope.target0.copy( scope.target ); 110 | scope.position0.copy( scope.object.position ); 111 | scope.zoom0 = scope.object.zoom; 112 | 113 | }; 114 | 115 | this.reset = function () { 116 | 117 | scope.target.copy( scope.target0 ); 118 | scope.object.position.copy( scope.position0 ); 119 | scope.object.zoom = scope.zoom0; 120 | 121 | scope.object.updateProjectionMatrix(); 122 | scope.dispatchEvent( changeEvent ); 123 | 124 | scope.update(); 125 | 126 | state = STATE.NONE; 127 | 128 | }; 129 | 130 | // this method is exposed, but perhaps it would be better if we can make it private... 131 | this.update = function () { 132 | 133 | var offset = new THREE.Vector3(); 134 | 135 | // so camera.up is the orbit axis 136 | var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) ); 137 | var quatInverse = quat.clone().inverse(); 138 | 139 | var lastPosition = new THREE.Vector3(); 140 | var lastQuaternion = new THREE.Quaternion(); 141 | 142 | return function update() { 143 | 144 | var position = scope.object.position; 145 | 146 | offset.copy( position ).sub( scope.target ); 147 | 148 | // rotate offset to "y-axis-is-up" space 149 | offset.applyQuaternion( quat ); 150 | 151 | // angle from z-axis around y-axis 152 | spherical.setFromVector3( offset ); 153 | 154 | if ( scope.autoRotate && state === STATE.NONE ) { 155 | 156 | rotateLeft( getAutoRotationAngle() ); 157 | 158 | } 159 | 160 | if ( scope.enableDamping ) { 161 | 162 | spherical.theta += sphericalDelta.theta * scope.dampingFactor; 163 | spherical.phi += sphericalDelta.phi * scope.dampingFactor; 164 | 165 | } else { 166 | 167 | spherical.theta += sphericalDelta.theta; 168 | spherical.phi += sphericalDelta.phi; 169 | 170 | } 171 | 172 | // restrict theta to be between desired limits 173 | spherical.theta = Math.max( scope.minAzimuthAngle, Math.min( scope.maxAzimuthAngle, spherical.theta ) ); 174 | 175 | // restrict phi to be between desired limits 176 | spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); 177 | 178 | spherical.makeSafe(); 179 | 180 | 181 | spherical.radius *= scale; 182 | 183 | // restrict radius to be between desired limits 184 | spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) ); 185 | 186 | // move target to panned location 187 | 188 | if ( scope.enableDamping === true ) { 189 | 190 | scope.target.addScaledVector( panOffset, scope.dampingFactor ); 191 | 192 | } else { 193 | 194 | scope.target.add( panOffset ); 195 | 196 | } 197 | 198 | offset.setFromSpherical( spherical ); 199 | 200 | // rotate offset back to "camera-up-vector-is-up" space 201 | offset.applyQuaternion( quatInverse ); 202 | 203 | position.copy( scope.target ).add( offset ); 204 | 205 | scope.object.lookAt( scope.target ); 206 | 207 | if ( scope.enableDamping === true ) { 208 | 209 | sphericalDelta.theta *= ( 1 - scope.dampingFactor ); 210 | sphericalDelta.phi *= ( 1 - scope.dampingFactor ); 211 | 212 | panOffset.multiplyScalar( 1 - scope.dampingFactor ); 213 | 214 | } else { 215 | 216 | sphericalDelta.set( 0, 0, 0 ); 217 | 218 | panOffset.set( 0, 0, 0 ); 219 | 220 | } 221 | 222 | scale = 1; 223 | 224 | // update condition is: 225 | // min(camera displacement, camera rotation in radians)^2 > EPS 226 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 227 | 228 | if ( zoomChanged || 229 | lastPosition.distanceToSquared( scope.object.position ) > EPS || 230 | 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) { 231 | 232 | scope.dispatchEvent( changeEvent ); 233 | 234 | lastPosition.copy( scope.object.position ); 235 | lastQuaternion.copy( scope.object.quaternion ); 236 | zoomChanged = false; 237 | 238 | return true; 239 | 240 | } 241 | 242 | return false; 243 | 244 | }; 245 | 246 | }(); 247 | 248 | this.dispose = function () { 249 | 250 | scope.domElement.removeEventListener( 'contextmenu', onContextMenu, false ); 251 | scope.domElement.removeEventListener( 'mousedown', onMouseDown, false ); 252 | scope.domElement.removeEventListener( 'wheel', onMouseWheel, false ); 253 | 254 | scope.domElement.removeEventListener( 'touchstart', onTouchStart, false ); 255 | scope.domElement.removeEventListener( 'touchend', onTouchEnd, false ); 256 | scope.domElement.removeEventListener( 'touchmove', onTouchMove, false ); 257 | 258 | document.removeEventListener( 'mousemove', onMouseMove, false ); 259 | document.removeEventListener( 'mouseup', onMouseUp, false ); 260 | 261 | scope.domElement.removeEventListener( 'keydown', onKeyDown, false ); 262 | 263 | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? 264 | 265 | }; 266 | 267 | // 268 | // internals 269 | // 270 | 271 | var scope = this; 272 | 273 | var changeEvent = { type: 'change' }; 274 | var startEvent = { type: 'start' }; 275 | var endEvent = { type: 'end' }; 276 | 277 | var STATE = { 278 | NONE: - 1, 279 | ROTATE: 0, 280 | DOLLY: 1, 281 | PAN: 2, 282 | TOUCH_ROTATE: 3, 283 | TOUCH_PAN: 4, 284 | TOUCH_DOLLY_PAN: 5, 285 | TOUCH_DOLLY_ROTATE: 6 286 | }; 287 | 288 | var state = STATE.NONE; 289 | 290 | var EPS = 0.000001; 291 | 292 | // current position in spherical coordinates 293 | var spherical = new THREE.Spherical(); 294 | var sphericalDelta = new THREE.Spherical(); 295 | 296 | var scale = 1; 297 | var panOffset = new THREE.Vector3(); 298 | this.panOffset = panOffset; 299 | var zoomChanged = false; 300 | 301 | var rotateStart = new THREE.Vector2(); 302 | var rotateEnd = new THREE.Vector2(); 303 | var rotateDelta = new THREE.Vector2(); 304 | 305 | var panStart = new THREE.Vector2(); 306 | var panEnd = new THREE.Vector2(); 307 | var panDelta = new THREE.Vector2(); 308 | 309 | var dollyStart = new THREE.Vector2(); 310 | var dollyEnd = new THREE.Vector2(); 311 | var dollyDelta = new THREE.Vector2(); 312 | 313 | function getAutoRotationAngle() { 314 | 315 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 316 | 317 | } 318 | 319 | function getZoomScale() { 320 | 321 | return Math.pow( 0.95, scope.zoomSpeed ); 322 | 323 | } 324 | 325 | function rotateLeft( angle ) { 326 | 327 | sphericalDelta.theta -= angle; 328 | 329 | } 330 | 331 | function rotateUp( angle ) { 332 | 333 | sphericalDelta.phi -= angle; 334 | 335 | } 336 | 337 | var panLeft = function () { 338 | 339 | var v = new THREE.Vector3(); 340 | 341 | return function panLeft( distance, objectMatrix ) { 342 | 343 | v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix 344 | v.multiplyScalar( - distance ); 345 | 346 | panOffset.add( v ); 347 | 348 | }; 349 | 350 | }(); 351 | 352 | var panUp = function () { 353 | 354 | var v = new THREE.Vector3(); 355 | 356 | return function panUp( distance, objectMatrix ) { 357 | 358 | if ( scope.screenSpacePanning === true ) { 359 | 360 | v.setFromMatrixColumn( objectMatrix, 1 ); 361 | 362 | } else { 363 | 364 | v.setFromMatrixColumn( objectMatrix, 0 ); 365 | v.crossVectors( scope.object.up, v ); 366 | 367 | } 368 | 369 | v.multiplyScalar( distance ); 370 | 371 | panOffset.add( v ); 372 | 373 | }; 374 | 375 | }(); 376 | 377 | // deltaX and deltaY are in pixels; right and down are positive 378 | var pan = function () { 379 | 380 | var offset = new THREE.Vector3(); 381 | 382 | return function pan( deltaX, deltaY ) { 383 | 384 | var element = scope.domElement; 385 | 386 | if ( scope.object.isPerspectiveCamera ) { 387 | 388 | // perspective 389 | var position = scope.object.position; 390 | offset.copy( position ).sub( scope.target ); 391 | var targetDistance = offset.length(); 392 | 393 | // half of the fov is center to top of screen 394 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); 395 | 396 | // we use only clientHeight here so aspect ratio does not distort speed 397 | panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); 398 | panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); 399 | 400 | } else if ( scope.object.isOrthographicCamera ) { 401 | 402 | // orthographic 403 | panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); 404 | panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); 405 | 406 | } else { 407 | 408 | // camera neither orthographic nor perspective 409 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 410 | scope.enablePan = false; 411 | 412 | } 413 | 414 | }; 415 | 416 | }(); 417 | 418 | function dollyIn( dollyScale ) { 419 | 420 | if ( scope.object.isPerspectiveCamera ) { 421 | 422 | scale /= dollyScale; 423 | 424 | } else if ( scope.object.isOrthographicCamera ) { 425 | 426 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) ); 427 | scope.object.updateProjectionMatrix(); 428 | zoomChanged = true; 429 | 430 | } else { 431 | 432 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 433 | scope.enableZoom = false; 434 | 435 | } 436 | 437 | } 438 | 439 | function dollyOut( dollyScale ) { 440 | 441 | if ( scope.object.isPerspectiveCamera ) { 442 | 443 | scale *= dollyScale; 444 | 445 | } else if ( scope.object.isOrthographicCamera ) { 446 | 447 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) ); 448 | scope.object.updateProjectionMatrix(); 449 | zoomChanged = true; 450 | 451 | } else { 452 | 453 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 454 | scope.enableZoom = false; 455 | 456 | } 457 | 458 | } 459 | 460 | // 461 | // event callbacks - update the object state 462 | // 463 | 464 | function handleMouseDownRotate( event ) { 465 | 466 | rotateStart.set( event.clientX, event.clientY ); 467 | 468 | } 469 | 470 | function handleMouseDownDolly( event ) { 471 | 472 | dollyStart.set( event.clientX, event.clientY ); 473 | 474 | } 475 | 476 | function handleMouseDownPan( event ) { 477 | 478 | panStart.set( event.clientX, event.clientY ); 479 | 480 | } 481 | 482 | function handleMouseMoveRotate( event ) { 483 | 484 | rotateEnd.set( event.clientX, event.clientY ); 485 | 486 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 487 | 488 | var element = scope.domElement; 489 | 490 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 491 | 492 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 493 | 494 | rotateStart.copy( rotateEnd ); 495 | 496 | scope.update(); 497 | 498 | } 499 | 500 | function handleMouseMoveDolly( event ) { 501 | 502 | dollyEnd.set( event.clientX, event.clientY ); 503 | 504 | dollyDelta.subVectors( dollyEnd, dollyStart ); 505 | 506 | if ( dollyDelta.y > 0 ) { 507 | 508 | dollyIn( getZoomScale() ); 509 | 510 | } else if ( dollyDelta.y < 0 ) { 511 | 512 | dollyOut( getZoomScale() ); 513 | 514 | } 515 | 516 | dollyStart.copy( dollyEnd ); 517 | 518 | scope.update(); 519 | 520 | } 521 | 522 | function handleMouseMovePan( event ) { 523 | 524 | panEnd.set( event.clientX, event.clientY ); 525 | 526 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 527 | 528 | pan( panDelta.x, panDelta.y ); 529 | 530 | panStart.copy( panEnd ); 531 | 532 | scope.update(); 533 | 534 | } 535 | 536 | function handleMouseUp( /*event*/ ) { 537 | 538 | // no-op 539 | 540 | } 541 | 542 | function handleMouseWheel( event ) { 543 | 544 | if ( event.deltaY < 0 ) { 545 | 546 | dollyOut( getZoomScale() ); 547 | 548 | } else if ( event.deltaY > 0 ) { 549 | 550 | dollyIn( getZoomScale() ); 551 | 552 | } 553 | 554 | scope.update(); 555 | 556 | } 557 | 558 | function handleKeyDown( event ) { 559 | 560 | var needsUpdate = false; 561 | 562 | switch ( event.keyCode ) { 563 | 564 | case scope.keys.UP: 565 | pan( 0, scope.keyPanSpeed ); 566 | needsUpdate = true; 567 | break; 568 | 569 | case scope.keys.BOTTOM: 570 | pan( 0, - scope.keyPanSpeed ); 571 | needsUpdate = true; 572 | break; 573 | 574 | case scope.keys.LEFT: 575 | pan( scope.keyPanSpeed, 0 ); 576 | needsUpdate = true; 577 | break; 578 | 579 | case scope.keys.RIGHT: 580 | pan( - scope.keyPanSpeed, 0 ); 581 | needsUpdate = true; 582 | break; 583 | 584 | } 585 | 586 | if ( needsUpdate ) { 587 | 588 | // prevent the browser from scrolling on cursor keys 589 | event.preventDefault(); 590 | 591 | scope.update(); 592 | 593 | } 594 | 595 | 596 | } 597 | 598 | function handleTouchStartRotate( event ) { 599 | 600 | if ( event.touches.length == 1 ) { 601 | 602 | rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 603 | 604 | } else { 605 | 606 | var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); 607 | var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); 608 | 609 | rotateStart.set( x, y ); 610 | 611 | } 612 | 613 | } 614 | 615 | function handleTouchStartPan( event ) { 616 | 617 | if ( event.touches.length == 1 ) { 618 | 619 | panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 620 | 621 | } else { 622 | 623 | var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); 624 | var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); 625 | 626 | panStart.set( x, y ); 627 | 628 | } 629 | 630 | } 631 | 632 | function handleTouchStartDolly( event ) { 633 | 634 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 635 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 636 | 637 | var distance = Math.sqrt( dx * dx + dy * dy ); 638 | 639 | dollyStart.set( 0, distance ); 640 | 641 | } 642 | 643 | function handleTouchStartDollyPan( event ) { 644 | 645 | if ( scope.enableZoom ) handleTouchStartDolly( event ); 646 | 647 | if ( scope.enablePan ) handleTouchStartPan( event ); 648 | 649 | } 650 | 651 | function handleTouchStartDollyRotate( event ) { 652 | 653 | if ( scope.enableZoom ) handleTouchStartDolly( event ); 654 | 655 | if ( scope.enableRotate ) handleTouchStartRotate( event ); 656 | 657 | } 658 | 659 | function handleTouchMoveRotate( event ) { 660 | 661 | if ( event.touches.length == 1 ) { 662 | 663 | rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 664 | 665 | } else { 666 | 667 | var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); 668 | var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); 669 | 670 | rotateEnd.set( x, y ); 671 | 672 | } 673 | 674 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 675 | 676 | var element = scope.domElement; 677 | 678 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 679 | 680 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 681 | 682 | rotateStart.copy( rotateEnd ); 683 | 684 | } 685 | 686 | function handleTouchMovePan( event ) { 687 | 688 | if ( event.touches.length == 1 ) { 689 | 690 | panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 691 | 692 | } else { 693 | 694 | var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); 695 | var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); 696 | 697 | panEnd.set( x, y ); 698 | 699 | } 700 | 701 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 702 | 703 | pan( panDelta.x, panDelta.y ); 704 | 705 | panStart.copy( panEnd ); 706 | 707 | } 708 | 709 | function handleTouchMoveDolly( event ) { 710 | 711 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 712 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 713 | 714 | var distance = Math.sqrt( dx * dx + dy * dy ); 715 | 716 | dollyEnd.set( 0, distance ); 717 | 718 | dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) ); 719 | 720 | dollyIn( dollyDelta.y ); 721 | 722 | dollyStart.copy( dollyEnd ); 723 | 724 | } 725 | 726 | function handleTouchMoveDollyPan( event ) { 727 | 728 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 729 | 730 | if ( scope.enablePan ) handleTouchMovePan( event ); 731 | 732 | } 733 | 734 | function handleTouchMoveDollyRotate( event ) { 735 | 736 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 737 | 738 | if ( scope.enableRotate ) handleTouchMoveRotate( event ); 739 | 740 | } 741 | 742 | function handleTouchEnd( /*event*/ ) { 743 | 744 | // no-op 745 | 746 | } 747 | 748 | // 749 | // event handlers - FSM: listen for events and reset state 750 | // 751 | 752 | function onMouseDown( event ) { 753 | 754 | if ( scope.enabled === false ) return; 755 | 756 | // Prevent the browser from scrolling. 757 | event.preventDefault(); 758 | 759 | // Manually set the focus since calling preventDefault above 760 | // prevents the browser from setting it automatically. 761 | 762 | scope.domElement.focus ? scope.domElement.focus() : window.focus(); 763 | 764 | var mouseAction; 765 | 766 | switch ( event.button ) { 767 | 768 | case 0: 769 | 770 | mouseAction = scope.mouseButtons.LEFT; 771 | break; 772 | 773 | case 1: 774 | 775 | mouseAction = scope.mouseButtons.MIDDLE; 776 | break; 777 | 778 | case 2: 779 | 780 | mouseAction = scope.mouseButtons.RIGHT; 781 | break; 782 | 783 | default: 784 | 785 | mouseAction = - 1; 786 | 787 | } 788 | 789 | switch ( mouseAction ) { 790 | 791 | case THREE.MOUSE.DOLLY: 792 | 793 | if ( scope.enableZoom === false ) return; 794 | 795 | handleMouseDownDolly( event ); 796 | 797 | state = STATE.DOLLY; 798 | 799 | break; 800 | 801 | case THREE.MOUSE.ROTATE: 802 | 803 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 804 | 805 | if ( scope.enablePan === false ) return; 806 | 807 | handleMouseDownPan( event ); 808 | 809 | state = STATE.PAN; 810 | 811 | } else { 812 | 813 | if ( scope.enableRotate === false ) return; 814 | 815 | handleMouseDownRotate( event ); 816 | 817 | state = STATE.ROTATE; 818 | 819 | } 820 | 821 | break; 822 | 823 | case THREE.MOUSE.PAN: 824 | 825 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 826 | 827 | if ( scope.enableRotate === false ) return; 828 | 829 | handleMouseDownRotate( event ); 830 | 831 | state = STATE.ROTATE; 832 | 833 | } else { 834 | 835 | if ( scope.enablePan === false ) return; 836 | 837 | handleMouseDownPan( event ); 838 | 839 | state = STATE.PAN; 840 | 841 | } 842 | 843 | break; 844 | 845 | default: 846 | 847 | state = STATE.NONE; 848 | 849 | } 850 | 851 | if ( state !== STATE.NONE ) { 852 | 853 | document.addEventListener( 'mousemove', onMouseMove, false ); 854 | document.addEventListener( 'mouseup', onMouseUp, false ); 855 | 856 | scope.dispatchEvent( startEvent ); 857 | 858 | } 859 | 860 | } 861 | 862 | function onMouseMove( event ) { 863 | 864 | if ( scope.enabled === false ) return; 865 | 866 | event.preventDefault(); 867 | 868 | switch ( state ) { 869 | 870 | case STATE.ROTATE: 871 | 872 | if ( scope.enableRotate === false ) return; 873 | 874 | handleMouseMoveRotate( event ); 875 | 876 | break; 877 | 878 | case STATE.DOLLY: 879 | 880 | if ( scope.enableZoom === false ) return; 881 | 882 | handleMouseMoveDolly( event ); 883 | 884 | break; 885 | 886 | case STATE.PAN: 887 | 888 | if ( scope.enablePan === false ) return; 889 | 890 | handleMouseMovePan( event ); 891 | 892 | break; 893 | 894 | } 895 | 896 | } 897 | 898 | function onMouseUp( event ) { 899 | 900 | if ( scope.enabled === false ) return; 901 | 902 | handleMouseUp( event ); 903 | 904 | document.removeEventListener( 'mousemove', onMouseMove, false ); 905 | document.removeEventListener( 'mouseup', onMouseUp, false ); 906 | 907 | scope.dispatchEvent( endEvent ); 908 | 909 | state = STATE.NONE; 910 | 911 | } 912 | 913 | function onMouseWheel( event ) { 914 | 915 | if ( scope.enabled === false || scope.enableZoom === false || ( state !== STATE.NONE && state !== STATE.ROTATE ) ) return; 916 | 917 | event.preventDefault(); 918 | event.stopPropagation(); 919 | 920 | scope.dispatchEvent( startEvent ); 921 | 922 | handleMouseWheel( event ); 923 | 924 | scope.dispatchEvent( endEvent ); 925 | 926 | } 927 | 928 | function onKeyDown( event ) { 929 | 930 | if ( scope.enabled === false || scope.enableKeys === false || scope.enablePan === false ) return; 931 | 932 | handleKeyDown( event ); 933 | 934 | } 935 | 936 | function onTouchStart( event ) { 937 | 938 | if ( scope.enabled === false ) return; 939 | 940 | event.preventDefault(); 941 | 942 | switch ( event.touches.length ) { 943 | 944 | case 1: 945 | 946 | switch ( scope.touches.ONE ) { 947 | 948 | case THREE.TOUCH.ROTATE: 949 | 950 | if ( scope.enableRotate === false ) return; 951 | 952 | handleTouchStartRotate( event ); 953 | 954 | state = STATE.TOUCH_ROTATE; 955 | 956 | break; 957 | 958 | case THREE.TOUCH.PAN: 959 | 960 | if ( scope.enablePan === false ) return; 961 | 962 | handleTouchStartPan( event ); 963 | 964 | state = STATE.TOUCH_PAN; 965 | 966 | break; 967 | 968 | default: 969 | 970 | state = STATE.NONE; 971 | 972 | } 973 | 974 | break; 975 | 976 | case 2: 977 | 978 | switch ( scope.touches.TWO ) { 979 | 980 | case THREE.TOUCH.DOLLY_PAN: 981 | 982 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 983 | 984 | handleTouchStartDollyPan( event ); 985 | 986 | state = STATE.TOUCH_DOLLY_PAN; 987 | 988 | break; 989 | 990 | case THREE.TOUCH.DOLLY_ROTATE: 991 | 992 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 993 | 994 | handleTouchStartDollyRotate( event ); 995 | 996 | state = STATE.TOUCH_DOLLY_ROTATE; 997 | 998 | break; 999 | 1000 | default: 1001 | 1002 | state = STATE.NONE; 1003 | 1004 | } 1005 | 1006 | break; 1007 | 1008 | default: 1009 | 1010 | state = STATE.NONE; 1011 | 1012 | } 1013 | 1014 | if ( state !== STATE.NONE ) { 1015 | 1016 | scope.dispatchEvent( startEvent ); 1017 | 1018 | } 1019 | 1020 | } 1021 | 1022 | function onTouchMove( event ) { 1023 | 1024 | if ( scope.enabled === false ) return; 1025 | 1026 | event.preventDefault(); 1027 | event.stopPropagation(); 1028 | 1029 | switch ( state ) { 1030 | 1031 | case STATE.TOUCH_ROTATE: 1032 | 1033 | if ( scope.enableRotate === false ) return; 1034 | 1035 | handleTouchMoveRotate( event ); 1036 | 1037 | scope.update(); 1038 | 1039 | break; 1040 | 1041 | case STATE.TOUCH_PAN: 1042 | 1043 | if ( scope.enablePan === false ) return; 1044 | 1045 | handleTouchMovePan( event ); 1046 | 1047 | scope.update(); 1048 | 1049 | break; 1050 | 1051 | case STATE.TOUCH_DOLLY_PAN: 1052 | 1053 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 1054 | 1055 | handleTouchMoveDollyPan( event ); 1056 | 1057 | scope.update(); 1058 | 1059 | break; 1060 | 1061 | case STATE.TOUCH_DOLLY_ROTATE: 1062 | 1063 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 1064 | 1065 | handleTouchMoveDollyRotate( event ); 1066 | 1067 | scope.update(); 1068 | 1069 | break; 1070 | 1071 | default: 1072 | 1073 | state = STATE.NONE; 1074 | 1075 | } 1076 | 1077 | } 1078 | 1079 | function onTouchEnd( event ) { 1080 | 1081 | if ( scope.enabled === false ) return; 1082 | 1083 | handleTouchEnd( event ); 1084 | 1085 | scope.dispatchEvent( endEvent ); 1086 | 1087 | state = STATE.NONE; 1088 | 1089 | } 1090 | 1091 | function onContextMenu( event ) { 1092 | 1093 | if ( scope.enabled === false ) return; 1094 | 1095 | event.preventDefault(); 1096 | 1097 | } 1098 | 1099 | // 1100 | 1101 | scope.domElement.addEventListener( 'contextmenu', onContextMenu, false ); 1102 | 1103 | scope.domElement.addEventListener( 'mousedown', onMouseDown, false ); 1104 | scope.domElement.addEventListener( 'wheel', onMouseWheel, false ); 1105 | 1106 | scope.domElement.addEventListener( 'touchstart', onTouchStart, false ); 1107 | scope.domElement.addEventListener( 'touchend', onTouchEnd, false ); 1108 | scope.domElement.addEventListener( 'touchmove', onTouchMove, false ); 1109 | 1110 | scope.domElement.addEventListener( 'keydown', onKeyDown, false ); 1111 | 1112 | // make sure element can receive keys. 1113 | 1114 | if ( scope.domElement.tabIndex === - 1 ) { 1115 | 1116 | scope.domElement.tabIndex = 0; 1117 | 1118 | } 1119 | 1120 | // force an update at start 1121 | 1122 | this.update(); 1123 | 1124 | }; 1125 | 1126 | THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype ); 1127 | THREE.OrbitControls.prototype.constructor = THREE.OrbitControls; 1128 | 1129 | 1130 | // This set of controls performs orbiting, dollying (zooming), and panning. 1131 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 1132 | // This is very similar to OrbitControls, another set of touch behavior 1133 | // 1134 | // Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate 1135 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 1136 | // Pan - left mouse, or arrow keys / touch: one-finger move 1137 | 1138 | THREE.MapControls = function ( object, domElement ) { 1139 | 1140 | THREE.OrbitControls.call( this, object, domElement ); 1141 | 1142 | this.mouseButtons.LEFT = THREE.MOUSE.PAN; 1143 | this.mouseButtons.RIGHT = THREE.MOUSE.ROTATE; 1144 | 1145 | this.touches.ONE = THREE.TOUCH.PAN; 1146 | this.touches.TWO = THREE.TOUCH.DOLLY_ROTATE; 1147 | 1148 | }; 1149 | 1150 | THREE.MapControls.prototype = Object.create( THREE.EventDispatcher.prototype ); 1151 | THREE.MapControls.prototype.constructor = THREE.MapControls; 1152 | -------------------------------------------------------------------------------- /viewer.css: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | Copyright Abhinav Singh Chauhan (@xclkvj) 4 | Copying the contents of this file by any means is prohibited. 5 | */ 6 | 7 | #viewerCanvasWrapper, #viewerCanvasWrapper canvas { 8 | position: absolute; 9 | width: 100%; 10 | height: 100%; 11 | outline: 0; 12 | } 13 | 14 | #orientCubeWrapper canvas { 15 | outline: 0; 16 | } 17 | 18 | #modelBrowser { 19 | z-index: 15; 20 | } 21 | 22 | .wrapper { 23 | display: flex; 24 | flex-direction: column; 25 | height: 100vh; 26 | color: #666; 27 | overflow: hidden; 28 | -webkit-tap-highlight-color: transparent; 29 | } 30 | 31 | .viewer-wrapper { 32 | position: relative; 33 | background: #eee; 34 | flex: 1; 35 | } 36 | 37 | .viewer-loader { 38 | position: relative; 39 | width: 100%; 40 | height: 100%; 41 | z-index: 12; 42 | } 43 | 44 | .header, .footer { 45 | display: flex; 46 | padding: 0.55em; 47 | background: #fff; 48 | border: 1px solid #ddd; 49 | border-left: none; 50 | border-right: none; 51 | z-index: 10; 52 | } 53 | 54 | .header { 55 | justify-content: space-between; 56 | } 57 | 58 | .footer { 59 | justify-content: center; 60 | } 61 | 62 | .header-item, .footer-item { 63 | display: flex; 64 | border-radius: 1.55em; 65 | padding: 0.45em 0.85em; 66 | margin: 0 5px; 67 | color: #666; 68 | font-weight: medium; 69 | transition: all 0.2s; 70 | cursor: pointer; 71 | } 72 | 73 | .header-item:hover, .footer-item:hover { 74 | box-shadow: 0 0.11em 0.11em #bbb; 75 | color: #111; 76 | } 77 | 78 | .header-item:active, .footer-item:active, .item-selected, .item-selected:hover { 79 | box-shadow: 0 0.11em 0.11em #ccc inset; 80 | color: #03a9f4; 81 | } 82 | 83 | .header-item { 84 | align-items: center; 85 | } 86 | 87 | .header-icon { 88 | font-size: 1.25em; 89 | padding: 0 0.4em; 90 | } 91 | 92 | .footer-item { 93 | flex-direction: column; 94 | align-items: center; 95 | padding: 0.65em 1.55em; 96 | } 97 | 98 | .footer-icon { 99 | font-size: 2em; 100 | } 101 | 102 | .header-left, .header-right { 103 | display: flex; 104 | } 105 | 106 | .slider-container { 107 | position: absolute; 108 | bottom: 0; 109 | left: 0; 110 | background: #fff; 111 | border-top: 1px solid #ccc; 112 | width: 100%; 113 | text-align: center; 114 | } 115 | 116 | .slider { 117 | appearance: none; 118 | -webkit-appearance: none; 119 | height: 10px; 120 | border-radius: 4px; 121 | outline: 0; 122 | background: #d3d3d3; 123 | opacity: 0.7; 124 | transition: opacity 0.2s; 125 | -webkit-transition: 0.2s; 126 | margin: 20px 0; 127 | width: 100%; 128 | } 129 | 130 | .slider:hover { 131 | opacity: 1; 132 | } 133 | 134 | .slider::-webkit-slider-thumb { 135 | -webkit-appearance: none; 136 | appearance: none; 137 | width: 25px; 138 | height: 25px; 139 | background: #03a9f4; 140 | border-radius: 50%; 141 | cursor: pointer; 142 | } 143 | 144 | .slider::-moz-range-thumb { 145 | width: 25px; 146 | height: 25px; 147 | background: #03a9f4; 148 | border-radius: 50%; 149 | cursor: pointer; 150 | } 151 | 152 | .share-sidebar { 153 | position: absolute; 154 | right: 0; 155 | top: 0; 156 | background: #fff; 157 | border-left: 1px solid #ccc; 158 | height: 100%; 159 | padding: 20px; 160 | } 161 | 162 | .sidebar-title { 163 | font-weight: lighter; 164 | text-align: center; 165 | } 166 | 167 | .loader { 168 | position: absolute; 169 | left: 0; 170 | top: 0; 171 | background: #fff; 172 | width: 100%; 173 | height: 100%; 174 | z-index: 20; 175 | } 176 | 177 | .loader-spinner { 178 | position: absolute; 179 | left: 50%; 180 | top: 50%; 181 | margin-left: -60px; 182 | margin-top: -60px; 183 | width: 120px; 184 | height: 120px; 185 | border-radius: 50%; 186 | border: 10px solid #f3f3f3; 187 | border-top: 10px solid #3498db; 188 | animation: spin 2s linear infinite; 189 | } 190 | 191 | .loader-text { 192 | position: absolute; 193 | left: 50%; 194 | bottom: 10px; 195 | transform: translate(-50%, 0); 196 | } 197 | 198 | @keyframes spin { 199 | from { transform: rotate(0deg); } 200 | to { transform: rotate(360deg); } 201 | } 202 | 203 | #explodeSlider { 204 | width: 200px; 205 | } 206 | 207 | .right-container { 208 | position: absolute; 209 | right: 10px; 210 | top: 10px; 211 | display: flex; 212 | } 213 | 214 | .left-sidebar { 215 | position: absolute; 216 | left: 0; 217 | top: 0; 218 | height: 100%; 219 | background: #fff; 220 | border-right: 1px solid #bbb; 221 | min-width: 300px; 222 | display: flex; 223 | flex-direction: column; 224 | } 225 | 226 | .sidebar-header { 227 | border-bottom: 1px solid #bbb; 228 | padding: 15px 10px; 229 | font-size: 1.25em; 230 | display: flex; 231 | justify-content: space-between; 232 | align-items: center; 233 | } 234 | 235 | .sidebar-content { 236 | height: 100%; 237 | overflow-y: auto; 238 | } 239 | 240 | #backToHome { 241 | color: #aaa; 242 | opacity: 0.5; 243 | cursor: pointer; 244 | font-size: 1.5em; 245 | } 246 | 247 | #backToHome:hover { 248 | opacity: 1; 249 | } 250 | 251 | #orientCubeWrapper { 252 | width: 130px; 253 | height: 130px; 254 | opacity: 0.7; 255 | transition: opacity 0.3s; 256 | -webkit-transition: 0.3s; 257 | } 258 | 259 | #orientCubeWrapper:hover { 260 | opacity: 1; 261 | } 262 | 263 | .graph-item-wrapper { 264 | padding: 0.5em 0.75em; 265 | } 266 | 267 | .graph-item-wrapper:hover { 268 | background: rgba(0, 191, 255, 0.2) !important; 269 | } 270 | 271 | .graph-item-wrapper:nth-child(even) { 272 | background: #f5f8fb; 273 | } 274 | 275 | .graph-item-wrapper:nth-child(odd) { 276 | background: #fff; 277 | } 278 | 279 | .graph-item { 280 | display: flex; 281 | justify-content: space-between; 282 | } 283 | 284 | .graph-left, .graph-right { 285 | display: flex; 286 | align-items: center; 287 | } 288 | 289 | .graph-folder { 290 | margin-right: 10px; 291 | } 292 | 293 | .graph-name { 294 | overflow: hidden; 295 | } 296 | 297 | .graph-visible { 298 | margin-left: 10px; 299 | } 300 | 301 | .graph-visible, .graph-folder, .graph-name { 302 | cursor: pointer; 303 | } 304 | 305 | @media only screen and (max-width: 600px) { 306 | #modelBrowser, .share-sidebar { 307 | width: 100%; 308 | } 309 | 310 | .footer-title { 311 | display: none; 312 | } 313 | } 314 | 315 | @media only screen and (max-width: 500px) { 316 | .header-title { 317 | display: none; 318 | } 319 | .footer { 320 | justify-content: space-between; 321 | overflow-x: scroll; 322 | } 323 | } 324 | 325 | .explode-content { 326 | display: flex; 327 | flex-direction: column; 328 | align-items: center; 329 | justify-content: center; 330 | } 331 | 332 | .cb-container { 333 | position: relative; 334 | padding-left: 35px; 335 | margin-bottom: 12px; 336 | cursor: pointer; 337 | -webkit-user-select: none; 338 | -moz-user-select: none; 339 | -ms-user-select: none; 340 | user-select: none; 341 | display: flex; 342 | align-items: center; 343 | } 344 | 345 | .cb-container input { 346 | position: absolute; 347 | opacity: 0; 348 | cursor: pointer; 349 | height: 0; 350 | width: 0; 351 | margin: 10px; 352 | } 353 | 354 | .checkmark { 355 | position: absolute; 356 | top: 0; 357 | left: 0; 358 | height: 25px; 359 | width: 25px; 360 | border-radius: 50%; 361 | background-color: #eee; 362 | } 363 | 364 | .cb-container:hover input ~ .checkmark { 365 | background-color: #ccc; 366 | } 367 | 368 | .cb-container input:checked ~ .checkmark { 369 | background-color: #03a9f4; 370 | } 371 | 372 | .cb-checkmark:after { 373 | content: ""; 374 | position: absolute; 375 | display: none; 376 | } 377 | 378 | .cb-container input:checked ~ .checkmark:after { 379 | display: block; 380 | } 381 | 382 | .cb-container .checkmark:after { 383 | left: 9px; 384 | top: 5px; 385 | width: 5px; 386 | height: 10px; 387 | border: solid white; 388 | border-width: 0 3px 3px 0; 389 | -webkit-transform: rotate(45deg); 390 | -ms-transform: rotate(45deg); 391 | transform: rotate(45deg); 392 | } -------------------------------------------------------------------------------- /viewer.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | /* 4 | Copyright Abhinav Singh Chauhan (@xclkvj) 5 | Copying the contents of this file by any means is prohibited. 6 | */ 7 | 8 | const ViewerBG = '#eee'; 9 | const ViewerUI = { 10 | canvasWrapper: document.getElementById('viewerCanvasWrapper'), 11 | cubeWrapper: document.getElementById('orientCubeWrapper'), 12 | toggleZoom: document.getElementById('toggleZoom'), 13 | togglePan: document.getElementById('togglePan'), 14 | toggleOrbit: document.getElementById('toggleOrbit'), 15 | resetBtn: document.getElementById('resetBtn'), 16 | toggleModelBrowser: document.getElementById('toggleModelBrowser'), 17 | modelBrowser: document.getElementById('modelBrowser'), 18 | modelBrowserContent: document.getElementById('modelBrowserContent'), 19 | fileInput: document.getElementById('fileInput'), 20 | explodeSliderWrapper: document.getElementById('explodeSliderWrapper'), 21 | explodeSlider: document.getElementById('explodeSlider'), 22 | toggleExplode: document.getElementById('toggleExplode'), 23 | toggleShare: document.getElementById('toggleShare'), 24 | shareSidebar: document.getElementById('shareSidebar'), 25 | loader: document.getElementById('loader'), 26 | toggleMeasure: document.getElementById('toggleMeasure'), 27 | loaderInfo: document.getElementById('loaderInfo'), 28 | backToHome: document.getElementById('backToHome'), 29 | webglContainer: document.getElementById('webglContainer'), 30 | downloadScreen: document.getElementById('downloadScreen'), 31 | explodeFace: document.getElementById('explodeFace') 32 | }; 33 | 34 | function setItemSelected(ele, bool) { 35 | if (bool) { 36 | ele.classList.add('item-selected'); 37 | } else { 38 | ele.classList.remove('item-selected'); 39 | } 40 | } 41 | 42 | function toggle(ele) { 43 | if (ele.getBoundingClientRect().height > 0) { 44 | ele.style.display = 'none'; 45 | return false; 46 | } else { 47 | ele.style.display = 'block'; 48 | return true; 49 | } 50 | } 51 | 52 | function toggleThrough(ele, through, cb, selected=true) { 53 | through.onclick = () => { 54 | let bool = toggle(ele); 55 | selected && setItemSelected(through, bool); 56 | cb && cb(bool); 57 | } 58 | } 59 | 60 | function show(ele) { 61 | ele.style.display = 'block'; 62 | } 63 | 64 | function hide(ele) { 65 | ele.style.display = 'none'; 66 | } 67 | 68 | function Viewer() { 69 | 70 | ViewerUI.downloadScreen.onclick = function() { 71 | const canvas = renderer.domElement; 72 | renderAll(); 73 | const image = canvas.toDataURL("image/png"); 74 | const a = document.createElement("a"); 75 | a.href = image.replace(/^data:image\/[^;]/, 'data:application/octet-stream'); 76 | a.download = "image.png" 77 | a.click(); 78 | } 79 | 80 | ViewerUI.explodeFace.onclick = function() { 81 | explodeFace = this.checked; 82 | resetExplode(); 83 | explode(); 84 | } 85 | 86 | let cubeCameraDistance = 1.75; 87 | 88 | let cubeWrapper = ViewerUI.cubeWrapper; 89 | let cubeScene = new THREE.Scene(); 90 | let cubeCamera = new THREE.PerspectiveCamera(70, cubeWrapper.offsetWidth / cubeWrapper.offsetHeight, 0.1, 100); 91 | let cubeRenderer = new THREE.WebGLRenderer({ 92 | alpha: true, 93 | antialias: true, 94 | preserveDrawingBuffer: true 95 | }); 96 | 97 | cubeRenderer.setSize(cubeWrapper.offsetWidth, cubeWrapper.offsetHeight); 98 | cubeRenderer.setPixelRatio(window.deivicePixelRatio); 99 | 100 | cubeWrapper.appendChild(cubeRenderer.domElement); 101 | 102 | let materials = []; 103 | let texts = ['RIGHT', 'LEFT', 'TOP', 'BOTTOM', 'FRONT', 'BACK']; 104 | 105 | let textureLoader = new THREE.TextureLoader(); 106 | let canvas = document.createElement('canvas'); 107 | let ctx = canvas.getContext('2d'); 108 | 109 | let size = 64; 110 | canvas.width = size; 111 | canvas.height = size; 112 | 113 | ctx.font = 'bolder 12px "Open sans", Arial'; 114 | ctx.textBaseline = 'middle'; 115 | ctx.textAlign = 'center'; 116 | 117 | let mainColor = '#fff'; 118 | let otherColor = '#ccc'; 119 | 120 | let bg = ctx.createLinearGradient(0, 0, 0, size); 121 | bg.addColorStop(0, mainColor); 122 | bg.addColorStop(1, otherColor); 123 | 124 | for (let i = 0; i < 6; i++) { 125 | if (texts[i] == 'TOP') { 126 | ctx.fillStyle = mainColor; 127 | } else if (texts[i] == 'BOTTOM') { 128 | ctx.fillStyle = otherColor; 129 | } else { 130 | ctx.fillStyle = bg; 131 | } 132 | ctx.fillRect(0, 0, size, size); 133 | ctx.strokeStyle = '#aaa'; 134 | ctx.setLineDash([8, 8]); 135 | ctx.lineWidth = 4; 136 | ctx.strokeRect(0, 0, size, size); 137 | ctx.fillStyle = '#999'; 138 | ctx.fillText(texts[i], size / 2, size / 2); 139 | materials[i] = new THREE.MeshBasicMaterial({ 140 | map: textureLoader.load(canvas.toDataURL()) 141 | }); 142 | } 143 | 144 | let planes = []; 145 | 146 | let planeMaterial = new THREE.MeshBasicMaterial({ 147 | side: THREE.DoubleSide, 148 | color: 0x00c0ff, 149 | transparent: true, 150 | opacity: 0, 151 | depthTest: false 152 | }); 153 | let planeSize = 0.7; 154 | let planeGeometry = new THREE.PlaneGeometry(planeSize, planeSize); 155 | 156 | let a = 0.51; 157 | 158 | let plane1 = new THREE.Mesh(planeGeometry, planeMaterial.clone()); 159 | plane1.position.z = a; 160 | cubeScene.add(plane1); 161 | planes.push(plane1); 162 | 163 | let plane2 = new THREE.Mesh(planeGeometry, planeMaterial.clone()); 164 | plane2.position.z = -a; 165 | cubeScene.add(plane2); 166 | planes.push(plane2); 167 | 168 | let plane3 = new THREE.Mesh(planeGeometry, planeMaterial.clone()); 169 | plane3.rotation.y = Math.PI / 2; 170 | plane3.position.x = a; 171 | cubeScene.add(plane3); 172 | planes.push(plane3); 173 | 174 | let plane4 = new THREE.Mesh(planeGeometry, planeMaterial.clone()); 175 | plane4.rotation.y = Math.PI / 2; 176 | plane4.position.x = -a; 177 | cubeScene.add(plane4); 178 | planes.push(plane4); 179 | 180 | let plane5 = new THREE.Mesh(planeGeometry, planeMaterial.clone()); 181 | plane5.rotation.x = Math.PI / 2; 182 | plane5.position.y = a; 183 | cubeScene.add(plane5); 184 | planes.push(plane5); 185 | 186 | let plane6 = new THREE.Mesh(planeGeometry, planeMaterial.clone()); 187 | plane6.rotation.x = Math.PI / 2; 188 | plane6.position.y = -a; 189 | cubeScene.add(plane6); 190 | planes.push(plane6); 191 | 192 | let groundMaterial = new THREE.MeshBasicMaterial({ 193 | color: 0xaaaaaa 194 | }); 195 | let groundGeometry = new THREE.PlaneGeometry(1, 1); 196 | let groundPlane = new THREE.Mesh(groundGeometry, groundMaterial); 197 | groundPlane.rotation.x = -Math.PI / 2; 198 | groundPlane.position.y = -0.6; 199 | 200 | cubeScene.add(groundPlane); 201 | 202 | let cube = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), materials); 203 | cubeScene.add(cube); 204 | 205 | function updateCubeCamera() { 206 | cubeCamera.rotation.copy(camera.rotation); 207 | let dir = camera.position.clone().sub(controller.target).normalize(); 208 | cubeCamera.position.copy(dir.multiplyScalar(cubeCameraDistance)); 209 | } 210 | 211 | let activePlane = null; 212 | 213 | cubeRenderer.domElement.onmousemove = function(evt) { 214 | 215 | if (activePlane) { 216 | activePlane.material.opacity = 0; 217 | activePlane.material.needsUpdate = true; 218 | activePlane = null; 219 | } 220 | 221 | let x = evt.offsetX; 222 | let y = evt.offsetY; 223 | let size = cubeRenderer.getSize(new THREE.Vector2()); 224 | let mouse = new THREE.Vector2(x / size.width * 2 - 1, -y / size.height * 2 + 1); 225 | 226 | let raycaster = new THREE.Raycaster(); 227 | raycaster.setFromCamera(mouse, cubeCamera); 228 | let intersects = raycaster.intersectObjects(planes.concat(cube)); 229 | 230 | if (intersects.length > 0 && intersects[0].object != cube) { 231 | activePlane = intersects[0].object; 232 | activePlane.material.opacity = 0.2; 233 | activePlane.material.needsUpdate = true; 234 | } 235 | } 236 | 237 | let startTime = 0; 238 | let duration = 500; 239 | let oldPosition = new THREE.Vector3(); 240 | let newPosition = new THREE.Vector3(); 241 | let play = false; 242 | 243 | cubeRenderer.domElement.onclick = function(evt) { 244 | 245 | cubeRenderer.domElement.onmousemove(evt); 246 | 247 | if (!activePlane || hasMoved) { 248 | return false; 249 | } 250 | 251 | oldPosition.copy(camera.position); 252 | 253 | let distance = camera.position.clone().sub(controller.target).length(); 254 | newPosition.copy(controller.target); 255 | 256 | if (activePlane.position.x !== 0) { 257 | newPosition.x += activePlane.position.x < 0 ? -distance : distance; 258 | } else if (activePlane.position.y !== 0) { 259 | newPosition.y += activePlane.position.y < 0 ? -distance : distance; 260 | } else if (activePlane.position.z !== 0) { 261 | newPosition.z += activePlane.position.z < 0 ? -distance : distance; 262 | } 263 | 264 | //play = true; 265 | //startTime = Date.now(); 266 | camera.position.copy(newPosition); 267 | } 268 | 269 | cubeRenderer.domElement.ontouchmove = function(e) { 270 | let rect = e.target.getBoundingClientRect(); 271 | let x = e.targetTouches[0].pageX - rect.left; 272 | let y = e.targetTouches[0].pageY - rect.top; 273 | cubeRenderer.domElement.onmousemove({ 274 | offsetX: x, 275 | offsetY: y 276 | }); 277 | } 278 | 279 | cubeRenderer.domElement.ontouchstart = function(e) { 280 | let rect = e.target.getBoundingClientRect(); 281 | let x = e.targetTouches[0].pageX - rect.left; 282 | let y = e.targetTouches[0].pageY - rect.top; 283 | cubeRenderer.domElement.onclick({ 284 | offsetX: x, 285 | offsetY: y 286 | }); 287 | } 288 | 289 | ViewerUI.fileInput.addEventListener('input', function(evt) { 290 | let file = evt.target.files[0]; 291 | if (file) { 292 | show(ViewerUI.loader); 293 | ViewerUI.loaderInfo.innerHTML = 'Reading file...'; 294 | let reader = new FileReader(); 295 | reader.onload = function(e) { 296 | loadModel(e.target.result); 297 | } 298 | reader.onerror = function(err) { 299 | ViewerUI.loaderInfo.innerHTML = 'Error reading file! See console for more info.'; 300 | console.error(err); 301 | } 302 | reader.readAsDataURL(file); 303 | } 304 | }); 305 | 306 | hide(ViewerUI.loader); 307 | 308 | let hasMoved = false; 309 | 310 | function antiMoveOnDown(e) { 311 | hasMoved = false; 312 | } 313 | function antiMoveOnMove(e) { 314 | hasMoved = true; 315 | } 316 | 317 | window.addEventListener('mousedown', antiMoveOnDown, false); 318 | window.addEventListener('mousemove', antiMoveOnMove, false); 319 | window.addEventListener('touchstart', antiMoveOnDown, false); 320 | window.addEventListener('touchmove', antiMoveOnMove, true); 321 | 322 | let showExploded = false; 323 | let explodeFactor = 0; 324 | let explodeFace = !true; 325 | 326 | toggleThrough(ViewerUI.explodeSliderWrapper, ViewerUI.toggleExplode, (bool) => { 327 | if (!bool) { 328 | resetExplode(); 329 | } else { 330 | explodeFactor = ViewerUI.explodeSlider.value; 331 | explode(); 332 | } 333 | }); 334 | 335 | function resetExplode() { 336 | let temp = explodeFactor; 337 | let temp2 = explodeFace; 338 | explodeFace = true; 339 | explodeFactor = 0; 340 | explode(); 341 | explodeFace = false; 342 | explode(); 343 | explodeFactor = temp; 344 | explodeFace = temp2; 345 | } 346 | 347 | toggleThrough(ViewerUI.shareSidebar, ViewerUI.toggleShare); 348 | toggleThrough(ViewerUI.modelBrowser, ViewerUI.toggleModelBrowser); 349 | 350 | ViewerUI.explodeSlider.oninput = function() { 351 | explodeFactor = this.value; 352 | explode(); 353 | } 354 | 355 | function explode() { 356 | for (let i = 0; i < loadedMeshes.length; i++) { 357 | 358 | let node = loadedMeshes[i]; 359 | 360 | if (explodeFace) { 361 | let defaultPositionArray = node.defaultPositionArray; 362 | let positionArray = node.geometry.attributes.position.array; 363 | let normalArray = node.geometry.attributes.normal.array; 364 | let indexArray = node.geometry.index.array; 365 | 366 | for (let j = 0; j < indexArray.length; j++) { 367 | 368 | let index = indexArray[j] 369 | let position = new THREE.Vector3(defaultPositionArray[index * 3], defaultPositionArray[index * 3 + 1], defaultPositionArray[index * 3 + 2]); 370 | let normal = new THREE.Vector3(normalArray[index * 3], normalArray[index * 3 + 1], normalArray[index * 3 + 2]); 371 | 372 | position.add(normal.multiplyScalar(explodeFactor)); 373 | positionArray[index * 3] = position.x; 374 | positionArray[index * 3 + 1] = position.y; 375 | positionArray[index * 3 + 2] = position.z; 376 | 377 | } 378 | 379 | node.geometry.attributes.position.needsUpdate = true; 380 | node.geometry.computeBoundingBox(); 381 | node.geometry.computeBoundingSphere(); 382 | } else { 383 | node.position.copy(node.defaultPosition).add(node.defaultPosition.clone().normalize().multiplyScalar(explodeFactor)); 384 | } 385 | } 386 | } 387 | 388 | ViewerUI.toggleZoom.onclick = function() { 389 | setZoomMode(); 390 | setItemSelected(selectedModeElement, false); 391 | selectedModeElement = this; 392 | setItemSelected(this, true); 393 | } 394 | 395 | ViewerUI.togglePan.onclick = function() { 396 | setPanMode(); 397 | setItemSelected(selectedModeElement, false); 398 | selectedModeElement = this; 399 | setItemSelected(this, true); 400 | } 401 | 402 | ViewerUI.toggleOrbit.onclick = function() { 403 | setOrbitMode(); 404 | setItemSelected(selectedModeElement, false); 405 | selectedModeElement = this; 406 | setItemSelected(this, true); 407 | } 408 | 409 | ViewerUI.toggleMeasure.onclick = function() { 410 | isInMeasureMode = !isInMeasureMode; 411 | if (!isInMeasureMode) { 412 | lineScene.remove.apply(lineScene, lineScene.children); 413 | spriteScene.remove.apply(spriteScene, spriteScene.children); 414 | } 415 | setItemSelected(this, isInMeasureMode); 416 | } 417 | 418 | ViewerUI.resetBtn.onclick = ViewerUI.backToHome.onclick =function() { 419 | resetAll(); 420 | } 421 | 422 | function resetAll() { 423 | controller.reset(); 424 | lineScene.remove.apply(lineScene, lineScene.children); 425 | spriteScene.remove.apply(spriteScene, spriteScene.children); 426 | isInMeasureMode = false; 427 | setItemSelected(ViewerUI.toggleMeasure, false); 428 | ViewerUI.explodeSliderWrapper.style.display = 'none'; 429 | ViewerUI.explodeFace.checked = false; 430 | explodeFace = false; 431 | setItemSelected(ViewerUI.toggleExplode, false); 432 | resetExplode(); 433 | resetSelect(); 434 | } 435 | 436 | function updateSelectDom(child) { 437 | if (child.itemWrapper) { 438 | if (child.isSelected) { 439 | child.itemWrapper.querySelector('.graph-name').style.color = '#03a9f4'; 440 | } else { 441 | child.itemWrapper.querySelector('.graph-name').style.color = 'inherit'; 442 | } 443 | } 444 | } 445 | 446 | function setOrbitMode() { 447 | controller.enableZoom = true; 448 | controller.enablePan = true; 449 | controller.enableRotate = true; 450 | controller.mouseButtons = { 451 | LEFT: THREE.MOUSE.ROTATE, 452 | MIDDLE: THREE.MOUSE.DOLLY, 453 | RIGHT: THREE.MOUSE.PAN 454 | }; 455 | } 456 | 457 | function setPanMode() { 458 | controller.enableZoom = false; 459 | controller.enablePan = true; 460 | controller.enableRotate = false; 461 | controller.mouseButtons = { 462 | LEFT: THREE.MOUSE.PAN, 463 | MIDDLE: THREE.MOUSE.PAN, 464 | RIGHT: THREE.MOUSE.PAN 465 | }; 466 | } 467 | 468 | function setZoomMode() { 469 | controller.enableZoom = true; 470 | controller.enablePan = false; 471 | controller.enableRotate = false; 472 | controller.mouseButtons = { 473 | LEFT: THREE.MOUSE.DOLLY, 474 | MIDDLE: THREE.MOUSE.DOLLY, 475 | RIGHT: THREE.MOUSE.DOLLY 476 | }; 477 | } 478 | 479 | let wrapper = ViewerUI.canvasWrapper; 480 | let scene = new THREE.Scene(); 481 | let camera = new THREE.PerspectiveCamera(70, wrapper.offsetWidth / wrapper.offsetHeight, 0.1, 1000); 482 | 483 | let renderer = new THREE.WebGLRenderer({ 484 | antialias: true, 485 | alpha: false 486 | }); 487 | 488 | renderer.setClearColor(new THREE.Color(ViewerBG)); 489 | renderer.autoClear = false; 490 | renderer.setPixelRatio(window.deivicePixelRatio); 491 | 492 | let isInMeasureMode = false; 493 | let lineScene = new THREE.Scene(); 494 | let spriteScene = new THREE.Scene(); 495 | 496 | function makeCircleImage() { 497 | let canvas = document.createElement('canvas'); 498 | let ctx = canvas.getContext('2d'); 499 | let size = 32; 500 | canvas.width = size; 501 | canvas.height = size; 502 | 503 | let r = size * 0.8 / 2; 504 | let blur = size - r; 505 | 506 | ctx.shadowBlur = 5; 507 | ctx.shadowColor = '#555'; 508 | 509 | ctx.fillStyle = '#fff'; 510 | ctx.beginPath(); 511 | ctx.arc(size / 2, size / 2, r, 0, Math.PI * 2); 512 | ctx.closePath(); 513 | ctx.fill(); 514 | 515 | ctx.shadowBlur = 0; 516 | ctx.fillStyle = '#009bff'; 517 | ctx.beginPath(); 518 | ctx.arc(size / 2, size / 2, r * 0.5, 0, Math.PI * 2); 519 | ctx.closePath(); 520 | ctx.fill(); 521 | 522 | return canvas; 523 | } 524 | 525 | let circleTexture = new THREE.CanvasTexture(makeCircleImage()) 526 | let circleMaterial = new THREE.SpriteMaterial({ 527 | map: circleTexture, 528 | sizeAttenuation: false 529 | }); 530 | let circleSprite = new THREE.Sprite(circleMaterial); 531 | circleSprite.scale.setScalar(0.08); 532 | 533 | let lineMaterial = new THREE.LineBasicMaterial({ 534 | color: 0x009bff, 535 | linewidth: 10 536 | }); 537 | 538 | let activeLine = null; 539 | 540 | renderer.domElement.onclick = function(evt) { 541 | if (hasMoved) { 542 | return false; 543 | } 544 | 545 | evt = evt || window.event; 546 | 547 | let x = evt.offsetX; 548 | let y = evt.offsetY; 549 | let size = renderer.getSize(new THREE.Vector2()); 550 | let mouse = new THREE.Vector2(x / size.width * 2 - 1, -y / size.height * 2 + 1); 551 | 552 | let raycaster = new THREE.Raycaster(); 553 | raycaster.setFromCamera(mouse, camera); 554 | let intersects = raycaster.intersectObjects(loadedMeshes); 555 | 556 | if (!isInMeasureMode) { 557 | resetSelect(); 558 | } 559 | 560 | if (intersects.length > 0) { 561 | if (isInMeasureMode) { 562 | let point = intersects[0].point; 563 | if (!activeLine) { 564 | let sprite1 = circleSprite.clone(); 565 | let sprite2 = circleSprite.clone(); 566 | sprite1.position.copy(point.clone()); 567 | sprite2.position.copy(point.clone()); 568 | spriteScene.add(sprite1); 569 | spriteScene.add(sprite2); 570 | let lineGeometry = new THREE.Geometry(); 571 | lineGeometry.vertices.push(sprite1.position, sprite2.position); 572 | let line = new THREE.Line(lineGeometry, lineMaterial); 573 | line.sprite1 = sprite1; 574 | line.sprite2 = sprite2; 575 | lineScene.add(line); 576 | activeLine = line; 577 | } else { 578 | activeLine.geometry.vertices[1].copy(point); 579 | activeLine.geometry.verticesNeedUpdate = true; 580 | makeDistanceSprite(); 581 | activeLine = null; 582 | } 583 | } else { 584 | let mesh = intersects[0].object; 585 | mesh.isSelected = true; 586 | updateMeshInteractionMaterial(mesh); 587 | } 588 | } else { 589 | if (isInMeasureMode) { 590 | if (activeLine) { 591 | lineScene.remove(activeLine); 592 | spriteScene.remove(activeLine.sprite1); 593 | spriteScene.remove(activeLine.sprite2); 594 | activeLine = null; 595 | } 596 | } 597 | } 598 | } 599 | 600 | function resetSelect() { 601 | scene.traverse(child => { 602 | child.isSelected = false; 603 | if (child.isMesh && child.material) { 604 | updateMeshInteractionMaterial(child); 605 | } 606 | updateSelectDom(child); 607 | }); 608 | } 609 | 610 | 611 | renderer.domElement.onmousemove = function(evt) { 612 | evt = evt || window.event; 613 | 614 | if (!isInMeasureMode) { 615 | return; 616 | } 617 | 618 | let x = evt.offsetX; 619 | let y = evt.offsetY; 620 | let size = renderer.getSize(new THREE.Vector2()); 621 | let mouse = new THREE.Vector2(x / size.width * 2 - 1, -y / size.height * 2 + 1); 622 | 623 | let raycaster = new THREE.Raycaster(); 624 | raycaster.setFromCamera(mouse, camera); 625 | let intersects = raycaster.intersectObjects(loadedMeshes); 626 | 627 | if (isInMeasureMode && activeLine) { 628 | if (intersects.length > 0) { 629 | let point = intersects[0].point; 630 | activeLine.geometry.vertices[1].copy(point); 631 | activeLine.geometry.verticesNeedUpdate = true; 632 | } else { 633 | activeLine.geometry.vertices[1].copy(activeLine.geometry.vertices[0]); 634 | activeLine.geometry.verticesNeedUpdate = true; 635 | } 636 | 637 | } 638 | } 639 | 640 | /*renderer.domElement.ontouchmove = function(e) { 641 | let rect = e.target.getBoundingClientRect(); 642 | let x = e.targetTouches[0].pageX - rect.left; 643 | let y = e.targetTouches[0].pageY - rect.top; 644 | renderer.domElement.onmousemove({ 645 | offsetX: x, 646 | offsetY: y 647 | }); 648 | }*/ 649 | 650 | renderer.domElement.ontouchstart = function(e) { 651 | let rect = e.target.getBoundingClientRect(); 652 | let x = e.targetTouches[0].pageX - rect.left; 653 | let y = e.targetTouches[0].pageY - rect.top; 654 | renderer.domElement.onclick({ 655 | offsetX: x, 656 | offsetY: y 657 | }); 658 | } 659 | 660 | function makeDistanceSprite() { 661 | 662 | let canvas = document.createElement('canvas'); 663 | let ctx = canvas.getContext('2d'); 664 | let fontsize = 32; 665 | 666 | ctx.font = 'bolder ' + fontsize + 'px "Open Sans", Arial'; 667 | let v = activeLine.geometry.vertices; 668 | let length = v[0].clone().sub(v[1]).length().toFixed(1); 669 | let text = '~ ' + length; 670 | let size = ctx.measureText(text); 671 | let paddingLeft = 20; 672 | let paddingTop = 10; 673 | let margin = 10; 674 | canvas.width = size.width + paddingLeft * 2 + margin * 2; 675 | canvas.height = fontsize + paddingTop * 2 + margin * 2; 676 | 677 | ctx.shadowBlur = 10; 678 | ctx.shadowColor = '#555'; 679 | ctx.fillStyle = '#009bff'; 680 | roundRect(ctx, margin, margin, canvas.width - margin * 2, canvas.height - margin * 2, 10); 681 | 682 | ctx.shadowBlur = 0; 683 | ctx.fillStyle = '#fff'; 684 | ctx.textAlign = 'left'; 685 | ctx.textBaseline = 'top'; 686 | ctx.font = 'bolder ' + fontsize + 'px "Open Sans", Arial'; 687 | ctx.fillText(text, paddingLeft + margin, paddingTop + margin); 688 | 689 | let texture = new THREE.CanvasTexture(canvas); 690 | let sprite = new THREE.Sprite(new THREE.SpriteMaterial({ 691 | map: texture, 692 | sizeAttenuation: false 693 | })); 694 | 695 | let h = 0.7; 696 | sprite.scale.set(0.002 * canvas.width, 0.0025 * canvas.height).multiplyScalar(h); 697 | 698 | sprite.position.copy(v[0].clone().add(v[1]).multiplyScalar(0.5)); 699 | spriteScene.add(sprite); 700 | 701 | } 702 | 703 | function roundRect(ctx, x, y, w, h, r) { 704 | ctx.beginPath(); 705 | ctx.moveTo(x + r, y); 706 | ctx.lineTo(x + w - r, y); 707 | ctx.quadraticCurveTo(x + w, y, x + w, y + r); 708 | ctx.lineTo(x + w, y + h - r); 709 | ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); 710 | ctx.lineTo(x + r, y + h); 711 | ctx.quadraticCurveTo(x, y + h, x, y + h - r); 712 | ctx.lineTo(x, y + r); 713 | ctx.quadraticCurveTo(x, y, x + r, y); 714 | ctx.closePath(); 715 | ctx.fill(); 716 | } 717 | 718 | 719 | function updateMeshInteractionMaterial(mesh) { 720 | if (mesh.isHidden) { 721 | mesh.interactionMaterial.color = hiddenColor; 722 | mesh.interactionMaterial.opacity = hiddenAlpha; 723 | } else { 724 | mesh.interactionMaterial.opacity = 1; 725 | } 726 | if (mesh.isSelected) { 727 | mesh.interactionMaterial.color = selectColor; 728 | mesh.itemWrapper.querySelector('.graph-name').style.color = '#03a9f4'; 729 | } else { 730 | mesh.itemWrapper.querySelector('.graph-name').style.color = 'inherit'; 731 | } 732 | mesh.interactionMaterial.needsUpdate = true; 733 | if (!mesh.isSelected && !mesh.isHidden) { 734 | mesh.material = mesh.defaultMaterial; 735 | } else { 736 | mesh.material = mesh.interactionMaterial; 737 | } 738 | } 739 | 740 | function onResize() { 741 | let width = wrapper.offsetWidth; 742 | let height = wrapper.offsetHeight; 743 | renderer.setSize(width, height, false); 744 | camera.aspect = width / height; 745 | camera.updateProjectionMatrix(); 746 | } 747 | 748 | onResize(); 749 | 750 | wrapper.appendChild(renderer.domElement); 751 | window.addEventListener('resize', onResize, false); 752 | 753 | let gltfLoader = new THREE.GLTFLoader(); 754 | let loadedScene = null; 755 | let loadedMeshes = []; 756 | 757 | let d = 5; 758 | 759 | let selectColor = new THREE.Color('#42006b'); 760 | let hiddenColor = new THREE.Color('#555'); 761 | let hiddenAlpha = 0.3; 762 | 763 | let interactionMaterial = new THREE.MeshPhongMaterial({ 764 | transparent: true, 765 | color: selectColor, 766 | side: THREE.DoubleSide, 767 | precision: 'mediump' 768 | }); 769 | 770 | function loadModel(url) { 771 | 772 | resetAll(); 773 | if (loadedScene) { 774 | scene.remove(loadedScene); 775 | loadedScene = null; 776 | loadedMeshes.length = 0; 777 | } 778 | 779 | show(ViewerUI.loader); 780 | ViewerUI.modelBrowserContent.innerHTML = ''; 781 | ViewerUI.loaderInfo.innerHTML = 'Loading model...'; 782 | 783 | gltfLoader.load( 784 | url, 785 | function onLoad(gltf) { 786 | 787 | loadedScene = gltf.scene; 788 | scene.add(gltf.scene); 789 | 790 | gltf.scene = gltf.scene || gltf.scenes[0]; 791 | 792 | let object = gltf.scene; 793 | 794 | const box = new THREE.Box3().setFromObject(object); 795 | const size = box.getSize(new THREE.Vector3()).length(); 796 | const center = box.getCenter(new THREE.Vector3()); 797 | 798 | controller.reset(); 799 | 800 | object.position.x += (object.position.x - center.x); 801 | object.position.y += (object.position.y - center.y); 802 | object.position.z += (object.position.z - center.z); 803 | controller.maxDistance = size * 10; 804 | camera.near = size / 100; 805 | camera.far = size * 100; 806 | camera.updateProjectionMatrix(); 807 | 808 | camera.position.copy(center); 809 | camera.position.x += size / 2.0; 810 | camera.position.y += size / 5.0; 811 | camera.position.z += size / 2.0; 812 | 813 | directionalLight.position.setScalar(size); 814 | 815 | camera.lookAt(center); 816 | 817 | controller.saveState(); 818 | 819 | gltf.scene.traverse((node) => { 820 | if (node.isMesh && node.material) { 821 | node.geometry.computeBoundingBox(); 822 | node.material.side = THREE.DoubleSide; 823 | node.material.precision = 'mediump'; 824 | node.material.needsUpdate = true; 825 | node.interactionMaterial = interactionMaterial.clone(); 826 | node.defaultMaterial = node.material; 827 | node.defaultPositionArray = Array.from(node.geometry.attributes.position.array); 828 | node.defaultPosition = node.position.clone(); 829 | loadedMeshes.push(node); 830 | } 831 | }); 832 | 833 | let content = ViewerUI.modelBrowserContent; 834 | let counter = 0; 835 | let parentLevel = 0; 836 | 837 | function makeSceneGraph(obj) { 838 | 839 | if (obj.children.length === 0 && !obj.isMesh) { 840 | return; 841 | } 842 | 843 | let itemWrapper = document.createElement('div'); 844 | itemWrapper.classList.add('graph-item-wrapper'); 845 | 846 | let item = document.createElement('div'); 847 | item.classList.add('graph-item'); 848 | 849 | itemWrapper.appendChild(item); 850 | 851 | content.appendChild(itemWrapper); 852 | let n = 0; 853 | let obj2 = obj; 854 | while (obj2 != gltf.scene) { 855 | obj2 = obj2.parent; 856 | n++; 857 | } 858 | 859 | item.style.paddingLeft = n * 1.5 + 'em'; 860 | obj.itemWrapper = itemWrapper; 861 | 862 | let left = document.createElement('div'); 863 | left.classList.add('graph-left'); 864 | let right = document.createElement('div'); 865 | right.classList.add('graph-right'); 866 | item.appendChild(left); 867 | item.appendChild(right); 868 | 869 | if (obj.children.length > 0) { 870 | 871 | parentLevel++; 872 | let folder = document.createElement('div'); 873 | 874 | folder.style.marginRight = '10px'; 875 | folder.classList.add('graph-folder'); 876 | folder.innerHTML = ''; 877 | left.appendChild(folder); 878 | 879 | obj.isFolderOpen = true; 880 | obj.openFolder = function() { 881 | folder.innerHTML = obj.isFolderOpen ? '' : ''; 882 | obj.traverse(child => { 883 | if (obj === child) { 884 | return; 885 | } 886 | if (child.itemWrapper) { 887 | if (child.parent.isFolderOpen && obj.isFolderOpen) { 888 | child.itemWrapper.style.display = 'block'; 889 | } 890 | if (!obj.isFolderOpen) { 891 | child.itemWrapper.style.display = 'none'; 892 | } 893 | } 894 | }); 895 | } 896 | 897 | folder.onclick = () => { 898 | obj.isFolderOpen = !obj.isFolderOpen; 899 | obj.openFolder(); 900 | } 901 | 902 | for (let i = 0; i < obj.children.length; i++) { 903 | makeSceneGraph(obj.children[i]); 904 | } 905 | 906 | } 907 | 908 | let name = document.createElement('div'); 909 | name.classList.add('graph-name'); 910 | name.innerHTML = obj.name || 'None'; 911 | left.appendChild(name); 912 | 913 | name.onclick = function() { 914 | resetSelect(); 915 | obj.traverse(child => { 916 | child.isSelected = true; 917 | if (child.isMesh && child.material) { 918 | updateMeshInteractionMaterial(child); 919 | } 920 | updateSelectDom(child) 921 | }); 922 | } 923 | 924 | let visible = document.createElement('div'); 925 | visible.classList.add('graph-visible'); 926 | visible.innerHTML = ''; 927 | 928 | obj.showMesh = function() { 929 | visible.innerHTML = obj.isMeshVisible ? '' : ''; 930 | obj.traverse(child => { 931 | if (child.itemWrapper) { 932 | let eye = child.itemWrapper.querySelector('.graph-visible'); 933 | eye.innerHTML = obj.isMeshVisible ? '' : ''; 934 | eye.style.color = obj.isMeshVisible ? 'inherit' : 'rgba(0, 0, 0, 0.3)'; 935 | } 936 | if (child.isMesh && child.material) { 937 | child.isHidden = !obj.isMeshVisible; 938 | updateMeshInteractionMaterial(child); 939 | } 940 | }); 941 | } 942 | 943 | obj.isHidden = false; 944 | obj.isSelected = false; 945 | obj.isMeshVisible = true; 946 | visible.onclick = function() { 947 | obj.isMeshVisible = !obj.isMeshVisible; 948 | obj.showMesh(); 949 | } 950 | 951 | right.appendChild(visible) 952 | 953 | } 954 | 955 | makeSceneGraph(gltf.scene) 956 | 957 | hide(ViewerUI.loader); 958 | 959 | }, 960 | function onProgress(xhr) { 961 | ViewerUI.loaderInfo.innerHTML = Math.round(xhr.loaded / xhr.total * 100) + '% loaded'; 962 | }, 963 | function onError(err) { 964 | ViewerUI.loaderInfo.innerHTML = 'Error loading model! See console for more info.'; 965 | console.error('Error loading model!', err); 966 | } 967 | ); 968 | 969 | } 970 | 971 | 972 | 973 | let controller = new THREE.OrbitControls(camera, renderer.domElement); 974 | controller.enabled = true; 975 | controller.enableDamping = true; 976 | controller.dampingFactor = 0.5; 977 | controller.screenSpacePanning = true; 978 | 979 | let cubeController = new THREE.OrbitControls(camera, cubeRenderer.domElement); 980 | cubeController.enablePan = false; 981 | cubeController.enableZoom = false; 982 | cubeController.rotateSpeed = 0.125; 983 | 984 | let selectedModeElement = ViewerUI.toggleOrbit; 985 | setOrbitMode(); 986 | 987 | camera.position.z = d; 988 | camera.lookAt(scene.position); 989 | controller.update(); 990 | controller.saveState(); 991 | 992 | let ambientLight = new THREE.AmbientLight(); 993 | ambientLight.intensity = 1; 994 | scene.add(ambientLight); 995 | 996 | let directionalLight = new THREE.DirectionalLight(); 997 | directionalLight.position.set(200, 200, 200) 998 | directionalLight.intensity = 0.5; 999 | scene.add(directionalLight); 1000 | 1001 | /*let light1 = new THREE.PointLight(0xffffff); 1002 | light1.position.set(100, 100, 100); 1003 | scene.add(light1); 1004 | 1005 | let light2 = new THREE.PointLight(0xffffff); 1006 | light2.position.set(100, 100, -100); 1007 | scene.add(light2); 1008 | 1009 | let light3 = new THREE.PointLight(0xffffff); 1010 | light3.position.set(-100, 100, 100); 1011 | scene.add(light3); 1012 | 1013 | let light4 = new THREE.PointLight(0xffffff); 1014 | light4.position.set(-100, 100, -100); 1015 | scene.add(light4); 1016 | 1017 | light1.intensity = light2.intensity = light3.intensity = light4.intensity = 0.3;*/ 1018 | 1019 | let stop = false; 1020 | 1021 | function renderAll() { 1022 | 1023 | renderer.clear(); 1024 | renderer.render(scene, camera); 1025 | updateCubeCamera(); 1026 | cubeRenderer.render(cubeScene, cubeCamera); 1027 | 1028 | renderer.clearDepth(); 1029 | 1030 | if (isInMeasureMode) { 1031 | renderer.clearDepth(); 1032 | renderer.render(lineScene, camera); 1033 | renderer.clearDepth(); 1034 | renderer.render(spriteScene, camera); 1035 | } 1036 | } 1037 | 1038 | function animate(time) { 1039 | 1040 | if (stop) { 1041 | return; 1042 | } 1043 | 1044 | if (play) { 1045 | let now = Date.now(); 1046 | let x = Math.min(1, (now - startTime) / duration); 1047 | camera.position.copy(oldPosition).lerp(newPosition, x) 1048 | if (x === 1) { 1049 | play = false; 1050 | } 1051 | } 1052 | 1053 | requestAnimationFrame(animate); 1054 | controller.update(); 1055 | renderAll(); 1056 | } 1057 | 1058 | requestAnimationFrame(animate); 1059 | 1060 | return { 1061 | loadModel: loadModel 1062 | }; 1063 | 1064 | } 1065 | 1066 | function draggable(ele, toggleEle) { 1067 | 1068 | let startX = 0; 1069 | let startY = 0; 1070 | 1071 | function onMouseDown(evt) { 1072 | evt = evt || window.event; 1073 | startDrag(evt.clientX, evt.clientY); 1074 | window.addEventListener('mousemove', onMouseMove, true); 1075 | } 1076 | 1077 | function onMouseMove(evt) { 1078 | evt = evt || window.event; 1079 | let newX = evt.clientX; 1080 | let newY = evt.clientY; 1081 | moveDrag(newX, newY); 1082 | } 1083 | 1084 | function onMouseUp() { 1085 | window.removeEventListener('mousemove', onMouseMove, true); 1086 | } 1087 | 1088 | function startDrag(x, y) { 1089 | startX = x; 1090 | startY = y; 1091 | } 1092 | 1093 | function moveDrag(newX, newY) { 1094 | 1095 | let deltaX = newX - startX; 1096 | let deltaY = newY - startY; 1097 | 1098 | startX = newX; 1099 | startY = newY; 1100 | 1101 | let x = ele.offsetLeft + deltaX; 1102 | let y = ele.offsetTop + deltaY; 1103 | x < 0 && (x = 0); 1104 | y < 0 && (y = 0); 1105 | let w = ele.parentNode.offsetWidth - ele.offsetWidth; 1106 | let h = ele.parentNode.offsetHeight - ele.offsetHeight; 1107 | x > w && (x = w); 1108 | y > h && (y = h); 1109 | 1110 | ele.style.left = x + 'px'; 1111 | ele.style.top = y + 'px'; 1112 | 1113 | } 1114 | 1115 | toggleEle.addEventListener('mousedown', onMouseDown, true); 1116 | window.addEventListener('mouseup', onMouseUp, true); 1117 | 1118 | } 1119 | 1120 | --------------------------------------------------------------------------------