├── .gitignore ├── GLTFLoader.js ├── LICENSE ├── OrbitControls.js ├── README.md ├── RGBELoader.js ├── environment.hdr ├── index.html ├── server.py ├── suzanne.bin ├── suzanne.blend ├── suzanne.gltf └── three.module.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.blend1 2 | -------------------------------------------------------------------------------- /GLTFLoader.js: -------------------------------------------------------------------------------- 1 | import { 2 | AnimationClip, 3 | Bone, 4 | Box3, 5 | BufferAttribute, 6 | BufferGeometry, 7 | ClampToEdgeWrapping, 8 | Color, 9 | DirectionalLight, 10 | DoubleSide, 11 | FileLoader, 12 | FrontSide, 13 | Group, 14 | ImageBitmapLoader, 15 | InterleavedBuffer, 16 | InterleavedBufferAttribute, 17 | Interpolant, 18 | InterpolateDiscrete, 19 | InterpolateLinear, 20 | Line, 21 | LineBasicMaterial, 22 | LineLoop, 23 | LineSegments, 24 | LinearFilter, 25 | LinearMipmapLinearFilter, 26 | LinearMipmapNearestFilter, 27 | Loader, 28 | LoaderUtils, 29 | Material, 30 | MathUtils, 31 | Matrix4, 32 | Mesh, 33 | MeshBasicMaterial, 34 | MeshPhysicalMaterial, 35 | MeshStandardMaterial, 36 | MirroredRepeatWrapping, 37 | NearestFilter, 38 | NearestMipmapLinearFilter, 39 | NearestMipmapNearestFilter, 40 | NumberKeyframeTrack, 41 | Object3D, 42 | OrthographicCamera, 43 | PerspectiveCamera, 44 | PointLight, 45 | Points, 46 | PointsMaterial, 47 | PropertyBinding, 48 | Quaternion, 49 | QuaternionKeyframeTrack, 50 | RGBFormat, 51 | RepeatWrapping, 52 | Skeleton, 53 | SkinnedMesh, 54 | Sphere, 55 | SpotLight, 56 | TangentSpaceNormalMap, 57 | Texture, 58 | TextureLoader, 59 | TriangleFanDrawMode, 60 | TriangleStripDrawMode, 61 | Vector2, 62 | Vector3, 63 | VectorKeyframeTrack, 64 | sRGBEncoding 65 | } from './three.module.js'; 66 | 67 | class GLTFLoader extends Loader { 68 | 69 | constructor( manager ) { 70 | 71 | super( manager ); 72 | 73 | this.dracoLoader = null; 74 | this.ktx2Loader = null; 75 | this.meshoptDecoder = null; 76 | 77 | this.pluginCallbacks = []; 78 | 79 | this.register( function ( parser ) { 80 | 81 | return new GLTFMaterialsClearcoatExtension( parser ); 82 | 83 | } ); 84 | 85 | this.register( function ( parser ) { 86 | 87 | return new GLTFTextureBasisUExtension( parser ); 88 | 89 | } ); 90 | 91 | this.register( function ( parser ) { 92 | 93 | return new GLTFTextureWebPExtension( parser ); 94 | 95 | } ); 96 | 97 | this.register( function ( parser ) { 98 | 99 | return new GLTFMaterialsSheenExtension( parser ); 100 | 101 | } ); 102 | 103 | this.register( function ( parser ) { 104 | 105 | return new GLTFMaterialsTransmissionExtension( parser ); 106 | 107 | } ); 108 | 109 | this.register( function ( parser ) { 110 | 111 | return new GLTFMaterialsVolumeExtension( parser ); 112 | 113 | } ); 114 | 115 | this.register( function ( parser ) { 116 | 117 | return new GLTFMaterialsIorExtension( parser ); 118 | 119 | } ); 120 | 121 | this.register( function ( parser ) { 122 | 123 | return new GLTFMaterialsSpecularExtension( parser ); 124 | 125 | } ); 126 | 127 | this.register( function ( parser ) { 128 | 129 | return new GLTFLightsExtension( parser ); 130 | 131 | } ); 132 | 133 | this.register( function ( parser ) { 134 | 135 | return new GLTFMeshoptCompression( parser ); 136 | 137 | } ); 138 | 139 | } 140 | 141 | load( url, onLoad, onProgress, onError ) { 142 | 143 | const scope = this; 144 | 145 | let resourcePath; 146 | 147 | if ( this.resourcePath !== '' ) { 148 | 149 | resourcePath = this.resourcePath; 150 | 151 | } else if ( this.path !== '' ) { 152 | 153 | resourcePath = this.path; 154 | 155 | } else { 156 | 157 | resourcePath = LoaderUtils.extractUrlBase( url ); 158 | 159 | } 160 | 161 | // Tells the LoadingManager to track an extra item, which resolves after 162 | // the model is fully loaded. This means the count of items loaded will 163 | // be incorrect, but ensures manager.onLoad() does not fire early. 164 | this.manager.itemStart( url ); 165 | 166 | const _onError = function ( e ) { 167 | 168 | if ( onError ) { 169 | 170 | onError( e ); 171 | 172 | } else { 173 | 174 | console.error( e ); 175 | 176 | } 177 | 178 | scope.manager.itemError( url ); 179 | scope.manager.itemEnd( url ); 180 | 181 | }; 182 | 183 | const loader = new FileLoader( this.manager ); 184 | 185 | loader.setPath( this.path ); 186 | loader.setResponseType( 'arraybuffer' ); 187 | loader.setRequestHeader( this.requestHeader ); 188 | loader.setWithCredentials( this.withCredentials ); 189 | 190 | loader.load( url, function ( data ) { 191 | 192 | try { 193 | 194 | scope.parse( data, resourcePath, function ( gltf ) { 195 | 196 | onLoad( gltf ); 197 | 198 | scope.manager.itemEnd( url ); 199 | 200 | }, _onError ); 201 | 202 | } catch ( e ) { 203 | 204 | _onError( e ); 205 | 206 | } 207 | 208 | }, onProgress, _onError ); 209 | 210 | } 211 | 212 | setDRACOLoader( dracoLoader ) { 213 | 214 | this.dracoLoader = dracoLoader; 215 | return this; 216 | 217 | } 218 | 219 | setDDSLoader() { 220 | 221 | throw new Error( 222 | 223 | 'THREE.GLTFLoader: "MSFT_texture_dds" no longer supported. Please update to "KHR_texture_basisu".' 224 | 225 | ); 226 | 227 | } 228 | 229 | setKTX2Loader( ktx2Loader ) { 230 | 231 | this.ktx2Loader = ktx2Loader; 232 | return this; 233 | 234 | } 235 | 236 | setMeshoptDecoder( meshoptDecoder ) { 237 | 238 | this.meshoptDecoder = meshoptDecoder; 239 | return this; 240 | 241 | } 242 | 243 | register( callback ) { 244 | 245 | if ( this.pluginCallbacks.indexOf( callback ) === - 1 ) { 246 | 247 | this.pluginCallbacks.push( callback ); 248 | 249 | } 250 | 251 | return this; 252 | 253 | } 254 | 255 | unregister( callback ) { 256 | 257 | if ( this.pluginCallbacks.indexOf( callback ) !== - 1 ) { 258 | 259 | this.pluginCallbacks.splice( this.pluginCallbacks.indexOf( callback ), 1 ); 260 | 261 | } 262 | 263 | return this; 264 | 265 | } 266 | 267 | parse( data, path, onLoad, onError ) { 268 | 269 | let content; 270 | const extensions = {}; 271 | const plugins = {}; 272 | 273 | if ( typeof data === 'string' ) { 274 | 275 | content = data; 276 | 277 | } else { 278 | 279 | const magic = LoaderUtils.decodeText( new Uint8Array( data, 0, 4 ) ); 280 | 281 | if ( magic === BINARY_EXTENSION_HEADER_MAGIC ) { 282 | 283 | try { 284 | 285 | extensions[ EXTENSIONS.KHR_BINARY_GLTF ] = new GLTFBinaryExtension( data ); 286 | 287 | } catch ( error ) { 288 | 289 | if ( onError ) onError( error ); 290 | return; 291 | 292 | } 293 | 294 | content = extensions[ EXTENSIONS.KHR_BINARY_GLTF ].content; 295 | 296 | } else { 297 | 298 | content = LoaderUtils.decodeText( new Uint8Array( data ) ); 299 | 300 | } 301 | 302 | } 303 | 304 | const json = JSON.parse( content ); 305 | 306 | if ( json.asset === undefined || json.asset.version[ 0 ] < 2 ) { 307 | 308 | if ( onError ) onError( new Error( 'THREE.GLTFLoader: Unsupported asset. glTF versions >=2.0 are supported.' ) ); 309 | return; 310 | 311 | } 312 | 313 | const parser = new GLTFParser( json, { 314 | 315 | path: path || this.resourcePath || '', 316 | crossOrigin: this.crossOrigin, 317 | requestHeader: this.requestHeader, 318 | manager: this.manager, 319 | ktx2Loader: this.ktx2Loader, 320 | meshoptDecoder: this.meshoptDecoder 321 | 322 | } ); 323 | 324 | parser.fileLoader.setRequestHeader( this.requestHeader ); 325 | 326 | for ( let i = 0; i < this.pluginCallbacks.length; i ++ ) { 327 | 328 | const plugin = this.pluginCallbacks[ i ]( parser ); 329 | plugins[ plugin.name ] = plugin; 330 | 331 | // Workaround to avoid determining as unknown extension 332 | // in addUnknownExtensionsToUserData(). 333 | // Remove this workaround if we move all the existing 334 | // extension handlers to plugin system 335 | extensions[ plugin.name ] = true; 336 | 337 | } 338 | 339 | if ( json.extensionsUsed ) { 340 | 341 | for ( let i = 0; i < json.extensionsUsed.length; ++ i ) { 342 | 343 | const extensionName = json.extensionsUsed[ i ]; 344 | const extensionsRequired = json.extensionsRequired || []; 345 | 346 | switch ( extensionName ) { 347 | 348 | case EXTENSIONS.KHR_MATERIALS_UNLIT: 349 | extensions[ extensionName ] = new GLTFMaterialsUnlitExtension(); 350 | break; 351 | 352 | case EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: 353 | extensions[ extensionName ] = new GLTFMaterialsPbrSpecularGlossinessExtension(); 354 | break; 355 | 356 | case EXTENSIONS.KHR_DRACO_MESH_COMPRESSION: 357 | extensions[ extensionName ] = new GLTFDracoMeshCompressionExtension( json, this.dracoLoader ); 358 | break; 359 | 360 | case EXTENSIONS.KHR_TEXTURE_TRANSFORM: 361 | extensions[ extensionName ] = new GLTFTextureTransformExtension(); 362 | break; 363 | 364 | case EXTENSIONS.KHR_MESH_QUANTIZATION: 365 | extensions[ extensionName ] = new GLTFMeshQuantizationExtension(); 366 | break; 367 | 368 | default: 369 | 370 | if ( extensionsRequired.indexOf( extensionName ) >= 0 && plugins[ extensionName ] === undefined ) { 371 | 372 | console.warn( 'THREE.GLTFLoader: Unknown extension "' + extensionName + '".' ); 373 | 374 | } 375 | 376 | } 377 | 378 | } 379 | 380 | } 381 | 382 | parser.setExtensions( extensions ); 383 | parser.setPlugins( plugins ); 384 | parser.parse( onLoad, onError ); 385 | 386 | } 387 | 388 | } 389 | 390 | /* GLTFREGISTRY */ 391 | 392 | function GLTFRegistry() { 393 | 394 | let objects = {}; 395 | 396 | return { 397 | 398 | get: function ( key ) { 399 | 400 | return objects[ key ]; 401 | 402 | }, 403 | 404 | add: function ( key, object ) { 405 | 406 | objects[ key ] = object; 407 | 408 | }, 409 | 410 | remove: function ( key ) { 411 | 412 | delete objects[ key ]; 413 | 414 | }, 415 | 416 | removeAll: function () { 417 | 418 | objects = {}; 419 | 420 | } 421 | 422 | }; 423 | 424 | } 425 | 426 | /*********************************/ 427 | /********** EXTENSIONS ***********/ 428 | /*********************************/ 429 | 430 | const EXTENSIONS = { 431 | KHR_BINARY_GLTF: 'KHR_binary_glTF', 432 | KHR_DRACO_MESH_COMPRESSION: 'KHR_draco_mesh_compression', 433 | KHR_LIGHTS_PUNCTUAL: 'KHR_lights_punctual', 434 | KHR_MATERIALS_CLEARCOAT: 'KHR_materials_clearcoat', 435 | KHR_MATERIALS_IOR: 'KHR_materials_ior', 436 | KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: 'KHR_materials_pbrSpecularGlossiness', 437 | KHR_MATERIALS_SHEEN: 'KHR_materials_sheen', 438 | KHR_MATERIALS_SPECULAR: 'KHR_materials_specular', 439 | KHR_MATERIALS_TRANSMISSION: 'KHR_materials_transmission', 440 | KHR_MATERIALS_UNLIT: 'KHR_materials_unlit', 441 | KHR_MATERIALS_VOLUME: 'KHR_materials_volume', 442 | KHR_TEXTURE_BASISU: 'KHR_texture_basisu', 443 | KHR_TEXTURE_TRANSFORM: 'KHR_texture_transform', 444 | KHR_MESH_QUANTIZATION: 'KHR_mesh_quantization', 445 | EXT_TEXTURE_WEBP: 'EXT_texture_webp', 446 | EXT_MESHOPT_COMPRESSION: 'EXT_meshopt_compression' 447 | }; 448 | 449 | /** 450 | * Punctual Lights Extension 451 | * 452 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual 453 | */ 454 | class GLTFLightsExtension { 455 | 456 | constructor( parser ) { 457 | 458 | this.parser = parser; 459 | this.name = EXTENSIONS.KHR_LIGHTS_PUNCTUAL; 460 | 461 | // Object3D instance caches 462 | this.cache = { refs: {}, uses: {} }; 463 | 464 | } 465 | 466 | _markDefs() { 467 | 468 | const parser = this.parser; 469 | const nodeDefs = this.parser.json.nodes || []; 470 | 471 | for ( let nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex ++ ) { 472 | 473 | const nodeDef = nodeDefs[ nodeIndex ]; 474 | 475 | if ( nodeDef.extensions 476 | && nodeDef.extensions[ this.name ] 477 | && nodeDef.extensions[ this.name ].light !== undefined ) { 478 | 479 | parser._addNodeRef( this.cache, nodeDef.extensions[ this.name ].light ); 480 | 481 | } 482 | 483 | } 484 | 485 | } 486 | 487 | _loadLight( lightIndex ) { 488 | 489 | const parser = this.parser; 490 | const cacheKey = 'light:' + lightIndex; 491 | let dependency = parser.cache.get( cacheKey ); 492 | 493 | if ( dependency ) return dependency; 494 | 495 | const json = parser.json; 496 | const extensions = ( json.extensions && json.extensions[ this.name ] ) || {}; 497 | const lightDefs = extensions.lights || []; 498 | const lightDef = lightDefs[ lightIndex ]; 499 | let lightNode; 500 | 501 | const color = new Color( 0xffffff ); 502 | 503 | if ( lightDef.color !== undefined ) color.fromArray( lightDef.color ); 504 | 505 | const range = lightDef.range !== undefined ? lightDef.range : 0; 506 | 507 | switch ( lightDef.type ) { 508 | 509 | case 'directional': 510 | lightNode = new DirectionalLight( color ); 511 | lightNode.target.position.set( 0, 0, - 1 ); 512 | lightNode.add( lightNode.target ); 513 | break; 514 | 515 | case 'point': 516 | lightNode = new PointLight( color ); 517 | lightNode.distance = range; 518 | break; 519 | 520 | case 'spot': 521 | lightNode = new SpotLight( color ); 522 | lightNode.distance = range; 523 | // Handle spotlight properties. 524 | lightDef.spot = lightDef.spot || {}; 525 | lightDef.spot.innerConeAngle = lightDef.spot.innerConeAngle !== undefined ? lightDef.spot.innerConeAngle : 0; 526 | lightDef.spot.outerConeAngle = lightDef.spot.outerConeAngle !== undefined ? lightDef.spot.outerConeAngle : Math.PI / 4.0; 527 | lightNode.angle = lightDef.spot.outerConeAngle; 528 | lightNode.penumbra = 1.0 - lightDef.spot.innerConeAngle / lightDef.spot.outerConeAngle; 529 | lightNode.target.position.set( 0, 0, - 1 ); 530 | lightNode.add( lightNode.target ); 531 | break; 532 | 533 | default: 534 | throw new Error( 'THREE.GLTFLoader: Unexpected light type: ' + lightDef.type ); 535 | 536 | } 537 | 538 | // Some lights (e.g. spot) default to a position other than the origin. Reset the position 539 | // here, because node-level parsing will only override position if explicitly specified. 540 | lightNode.position.set( 0, 0, 0 ); 541 | 542 | lightNode.decay = 2; 543 | 544 | if ( lightDef.intensity !== undefined ) lightNode.intensity = lightDef.intensity; 545 | 546 | lightNode.name = parser.createUniqueName( lightDef.name || ( 'light_' + lightIndex ) ); 547 | 548 | dependency = Promise.resolve( lightNode ); 549 | 550 | parser.cache.add( cacheKey, dependency ); 551 | 552 | return dependency; 553 | 554 | } 555 | 556 | createNodeAttachment( nodeIndex ) { 557 | 558 | const self = this; 559 | const parser = this.parser; 560 | const json = parser.json; 561 | const nodeDef = json.nodes[ nodeIndex ]; 562 | const lightDef = ( nodeDef.extensions && nodeDef.extensions[ this.name ] ) || {}; 563 | const lightIndex = lightDef.light; 564 | 565 | if ( lightIndex === undefined ) return null; 566 | 567 | return this._loadLight( lightIndex ).then( function ( light ) { 568 | 569 | return parser._getNodeRef( self.cache, lightIndex, light ); 570 | 571 | } ); 572 | 573 | } 574 | 575 | } 576 | 577 | /** 578 | * Unlit Materials Extension 579 | * 580 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_unlit 581 | */ 582 | class GLTFMaterialsUnlitExtension { 583 | 584 | constructor() { 585 | 586 | this.name = EXTENSIONS.KHR_MATERIALS_UNLIT; 587 | 588 | } 589 | 590 | getMaterialType() { 591 | 592 | return MeshBasicMaterial; 593 | 594 | } 595 | 596 | extendParams( materialParams, materialDef, parser ) { 597 | 598 | const pending = []; 599 | 600 | materialParams.color = new Color( 1.0, 1.0, 1.0 ); 601 | materialParams.opacity = 1.0; 602 | 603 | const metallicRoughness = materialDef.pbrMetallicRoughness; 604 | 605 | if ( metallicRoughness ) { 606 | 607 | if ( Array.isArray( metallicRoughness.baseColorFactor ) ) { 608 | 609 | const array = metallicRoughness.baseColorFactor; 610 | 611 | materialParams.color.fromArray( array ); 612 | materialParams.opacity = array[ 3 ]; 613 | 614 | } 615 | 616 | if ( metallicRoughness.baseColorTexture !== undefined ) { 617 | 618 | pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture ) ); 619 | 620 | } 621 | 622 | } 623 | 624 | return Promise.all( pending ); 625 | 626 | } 627 | 628 | } 629 | 630 | /** 631 | * Clearcoat Materials Extension 632 | * 633 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_clearcoat 634 | */ 635 | class GLTFMaterialsClearcoatExtension { 636 | 637 | constructor( parser ) { 638 | 639 | this.parser = parser; 640 | this.name = EXTENSIONS.KHR_MATERIALS_CLEARCOAT; 641 | 642 | } 643 | 644 | getMaterialType( materialIndex ) { 645 | 646 | const parser = this.parser; 647 | const materialDef = parser.json.materials[ materialIndex ]; 648 | 649 | if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; 650 | 651 | return MeshPhysicalMaterial; 652 | 653 | } 654 | 655 | extendMaterialParams( materialIndex, materialParams ) { 656 | 657 | const parser = this.parser; 658 | const materialDef = parser.json.materials[ materialIndex ]; 659 | 660 | if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { 661 | 662 | return Promise.resolve(); 663 | 664 | } 665 | 666 | const pending = []; 667 | 668 | const extension = materialDef.extensions[ this.name ]; 669 | 670 | if ( extension.clearcoatFactor !== undefined ) { 671 | 672 | materialParams.clearcoat = extension.clearcoatFactor; 673 | 674 | } 675 | 676 | if ( extension.clearcoatTexture !== undefined ) { 677 | 678 | pending.push( parser.assignTexture( materialParams, 'clearcoatMap', extension.clearcoatTexture ) ); 679 | 680 | } 681 | 682 | if ( extension.clearcoatRoughnessFactor !== undefined ) { 683 | 684 | materialParams.clearcoatRoughness = extension.clearcoatRoughnessFactor; 685 | 686 | } 687 | 688 | if ( extension.clearcoatRoughnessTexture !== undefined ) { 689 | 690 | pending.push( parser.assignTexture( materialParams, 'clearcoatRoughnessMap', extension.clearcoatRoughnessTexture ) ); 691 | 692 | } 693 | 694 | if ( extension.clearcoatNormalTexture !== undefined ) { 695 | 696 | pending.push( parser.assignTexture( materialParams, 'clearcoatNormalMap', extension.clearcoatNormalTexture ) ); 697 | 698 | if ( extension.clearcoatNormalTexture.scale !== undefined ) { 699 | 700 | const scale = extension.clearcoatNormalTexture.scale; 701 | 702 | materialParams.clearcoatNormalScale = new Vector2( scale, scale ); 703 | 704 | } 705 | 706 | } 707 | 708 | return Promise.all( pending ); 709 | 710 | } 711 | 712 | } 713 | 714 | /** 715 | * Sheen Materials Extension 716 | * 717 | * Specification: https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_sheen 718 | */ 719 | class GLTFMaterialsSheenExtension { 720 | 721 | constructor( parser ) { 722 | 723 | this.parser = parser; 724 | this.name = EXTENSIONS.KHR_MATERIALS_SHEEN; 725 | 726 | } 727 | 728 | getMaterialType( materialIndex ) { 729 | 730 | const parser = this.parser; 731 | const materialDef = parser.json.materials[ materialIndex ]; 732 | 733 | if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; 734 | 735 | return MeshPhysicalMaterial; 736 | 737 | } 738 | 739 | extendMaterialParams( materialIndex, materialParams ) { 740 | 741 | const parser = this.parser; 742 | const materialDef = parser.json.materials[ materialIndex ]; 743 | 744 | if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { 745 | 746 | return Promise.resolve(); 747 | 748 | } 749 | 750 | const pending = []; 751 | 752 | materialParams.sheenColor = new Color( 0, 0, 0 ); 753 | materialParams.sheenRoughness = 0; 754 | materialParams.sheen = 1; 755 | 756 | const extension = materialDef.extensions[ this.name ]; 757 | 758 | if ( extension.sheenColorFactor !== undefined ) { 759 | 760 | materialParams.sheenColor.fromArray( extension.sheenColorFactor ); 761 | 762 | } 763 | 764 | if ( extension.sheenRoughnessFactor !== undefined ) { 765 | 766 | materialParams.sheenRoughness = extension.sheenRoughnessFactor; 767 | 768 | } 769 | 770 | if ( extension.sheenColorTexture !== undefined ) { 771 | 772 | pending.push( parser.assignTexture( materialParams, 'sheenColorMap', extension.sheenColorTexture ) ); 773 | 774 | } 775 | 776 | if ( extension.sheenRoughnessTexture !== undefined ) { 777 | 778 | pending.push( parser.assignTexture( materialParams, 'sheenRoughnessMap', extension.sheenRoughnessTexture ) ); 779 | 780 | } 781 | 782 | return Promise.all( pending ); 783 | 784 | } 785 | 786 | } 787 | 788 | /** 789 | * Transmission Materials Extension 790 | * 791 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_transmission 792 | * Draft: https://github.com/KhronosGroup/glTF/pull/1698 793 | */ 794 | class GLTFMaterialsTransmissionExtension { 795 | 796 | constructor( parser ) { 797 | 798 | this.parser = parser; 799 | this.name = EXTENSIONS.KHR_MATERIALS_TRANSMISSION; 800 | 801 | } 802 | 803 | getMaterialType( materialIndex ) { 804 | 805 | const parser = this.parser; 806 | const materialDef = parser.json.materials[ materialIndex ]; 807 | 808 | if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; 809 | 810 | return MeshPhysicalMaterial; 811 | 812 | } 813 | 814 | extendMaterialParams( materialIndex, materialParams ) { 815 | 816 | const parser = this.parser; 817 | const materialDef = parser.json.materials[ materialIndex ]; 818 | 819 | if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { 820 | 821 | return Promise.resolve(); 822 | 823 | } 824 | 825 | const pending = []; 826 | 827 | const extension = materialDef.extensions[ this.name ]; 828 | 829 | if ( extension.transmissionFactor !== undefined ) { 830 | 831 | materialParams.transmission = extension.transmissionFactor; 832 | 833 | } 834 | 835 | if ( extension.transmissionTexture !== undefined ) { 836 | 837 | pending.push( parser.assignTexture( materialParams, 'transmissionMap', extension.transmissionTexture ) ); 838 | 839 | } 840 | 841 | return Promise.all( pending ); 842 | 843 | } 844 | 845 | } 846 | 847 | /** 848 | * Materials Volume Extension 849 | * 850 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_volume 851 | */ 852 | class GLTFMaterialsVolumeExtension { 853 | 854 | constructor( parser ) { 855 | 856 | this.parser = parser; 857 | this.name = EXTENSIONS.KHR_MATERIALS_VOLUME; 858 | 859 | } 860 | 861 | getMaterialType( materialIndex ) { 862 | 863 | const parser = this.parser; 864 | const materialDef = parser.json.materials[ materialIndex ]; 865 | 866 | if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; 867 | 868 | return MeshPhysicalMaterial; 869 | 870 | } 871 | 872 | extendMaterialParams( materialIndex, materialParams ) { 873 | 874 | const parser = this.parser; 875 | const materialDef = parser.json.materials[ materialIndex ]; 876 | 877 | if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { 878 | 879 | return Promise.resolve(); 880 | 881 | } 882 | 883 | const pending = []; 884 | 885 | const extension = materialDef.extensions[ this.name ]; 886 | 887 | materialParams.thickness = extension.thicknessFactor !== undefined ? extension.thicknessFactor : 0; 888 | 889 | if ( extension.thicknessTexture !== undefined ) { 890 | 891 | pending.push( parser.assignTexture( materialParams, 'thicknessMap', extension.thicknessTexture ) ); 892 | 893 | } 894 | 895 | materialParams.attenuationDistance = extension.attenuationDistance || 0; 896 | 897 | const colorArray = extension.attenuationColor || [ 1, 1, 1 ]; 898 | materialParams.attenuationColor = new Color( colorArray[ 0 ], colorArray[ 1 ], colorArray[ 2 ] ); 899 | 900 | return Promise.all( pending ); 901 | 902 | } 903 | 904 | } 905 | 906 | /** 907 | * Materials ior Extension 908 | * 909 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_ior 910 | */ 911 | class GLTFMaterialsIorExtension { 912 | 913 | constructor( parser ) { 914 | 915 | this.parser = parser; 916 | this.name = EXTENSIONS.KHR_MATERIALS_IOR; 917 | 918 | } 919 | 920 | getMaterialType( materialIndex ) { 921 | 922 | const parser = this.parser; 923 | const materialDef = parser.json.materials[ materialIndex ]; 924 | 925 | if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; 926 | 927 | return MeshPhysicalMaterial; 928 | 929 | } 930 | 931 | extendMaterialParams( materialIndex, materialParams ) { 932 | 933 | const parser = this.parser; 934 | const materialDef = parser.json.materials[ materialIndex ]; 935 | 936 | if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { 937 | 938 | return Promise.resolve(); 939 | 940 | } 941 | 942 | const extension = materialDef.extensions[ this.name ]; 943 | 944 | materialParams.ior = extension.ior !== undefined ? extension.ior : 1.5; 945 | 946 | return Promise.resolve(); 947 | 948 | } 949 | 950 | } 951 | 952 | /** 953 | * Materials specular Extension 954 | * 955 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_specular 956 | */ 957 | class GLTFMaterialsSpecularExtension { 958 | 959 | constructor( parser ) { 960 | 961 | this.parser = parser; 962 | this.name = EXTENSIONS.KHR_MATERIALS_SPECULAR; 963 | 964 | } 965 | 966 | getMaterialType( materialIndex ) { 967 | 968 | const parser = this.parser; 969 | const materialDef = parser.json.materials[ materialIndex ]; 970 | 971 | if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; 972 | 973 | return MeshPhysicalMaterial; 974 | 975 | } 976 | 977 | extendMaterialParams( materialIndex, materialParams ) { 978 | 979 | const parser = this.parser; 980 | const materialDef = parser.json.materials[ materialIndex ]; 981 | 982 | if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { 983 | 984 | return Promise.resolve(); 985 | 986 | } 987 | 988 | const pending = []; 989 | 990 | const extension = materialDef.extensions[ this.name ]; 991 | 992 | materialParams.specularIntensity = extension.specularFactor !== undefined ? extension.specularFactor : 1.0; 993 | 994 | if ( extension.specularTexture !== undefined ) { 995 | 996 | pending.push( parser.assignTexture( materialParams, 'specularIntensityMap', extension.specularTexture ) ); 997 | 998 | } 999 | 1000 | const colorArray = extension.specularColorFactor || [ 1, 1, 1 ]; 1001 | materialParams.specularColor = new Color( colorArray[ 0 ], colorArray[ 1 ], colorArray[ 2 ] ); 1002 | 1003 | if ( extension.specularColorTexture !== undefined ) { 1004 | 1005 | pending.push( parser.assignTexture( materialParams, 'specularColorMap', extension.specularColorTexture ).then( function ( texture ) { 1006 | 1007 | texture.encoding = sRGBEncoding; 1008 | 1009 | } ) ); 1010 | 1011 | } 1012 | 1013 | return Promise.all( pending ); 1014 | 1015 | } 1016 | 1017 | } 1018 | 1019 | /** 1020 | * BasisU Texture Extension 1021 | * 1022 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_basisu 1023 | */ 1024 | class GLTFTextureBasisUExtension { 1025 | 1026 | constructor( parser ) { 1027 | 1028 | this.parser = parser; 1029 | this.name = EXTENSIONS.KHR_TEXTURE_BASISU; 1030 | 1031 | } 1032 | 1033 | loadTexture( textureIndex ) { 1034 | 1035 | const parser = this.parser; 1036 | const json = parser.json; 1037 | 1038 | const textureDef = json.textures[ textureIndex ]; 1039 | 1040 | if ( ! textureDef.extensions || ! textureDef.extensions[ this.name ] ) { 1041 | 1042 | return null; 1043 | 1044 | } 1045 | 1046 | const extension = textureDef.extensions[ this.name ]; 1047 | const source = json.images[ extension.source ]; 1048 | const loader = parser.options.ktx2Loader; 1049 | 1050 | if ( ! loader ) { 1051 | 1052 | if ( json.extensionsRequired && json.extensionsRequired.indexOf( this.name ) >= 0 ) { 1053 | 1054 | throw new Error( 'THREE.GLTFLoader: setKTX2Loader must be called before loading KTX2 textures' ); 1055 | 1056 | } else { 1057 | 1058 | // Assumes that the extension is optional and that a fallback texture is present 1059 | return null; 1060 | 1061 | } 1062 | 1063 | } 1064 | 1065 | return parser.loadTextureImage( textureIndex, source, loader ); 1066 | 1067 | } 1068 | 1069 | } 1070 | 1071 | /** 1072 | * WebP Texture Extension 1073 | * 1074 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_texture_webp 1075 | */ 1076 | class GLTFTextureWebPExtension { 1077 | 1078 | constructor( parser ) { 1079 | 1080 | this.parser = parser; 1081 | this.name = EXTENSIONS.EXT_TEXTURE_WEBP; 1082 | this.isSupported = null; 1083 | 1084 | } 1085 | 1086 | loadTexture( textureIndex ) { 1087 | 1088 | const name = this.name; 1089 | const parser = this.parser; 1090 | const json = parser.json; 1091 | 1092 | const textureDef = json.textures[ textureIndex ]; 1093 | 1094 | if ( ! textureDef.extensions || ! textureDef.extensions[ name ] ) { 1095 | 1096 | return null; 1097 | 1098 | } 1099 | 1100 | const extension = textureDef.extensions[ name ]; 1101 | const source = json.images[ extension.source ]; 1102 | 1103 | let loader = parser.textureLoader; 1104 | if ( source.uri ) { 1105 | 1106 | const handler = parser.options.manager.getHandler( source.uri ); 1107 | if ( handler !== null ) loader = handler; 1108 | 1109 | } 1110 | 1111 | return this.detectSupport().then( function ( isSupported ) { 1112 | 1113 | if ( isSupported ) return parser.loadTextureImage( textureIndex, source, loader ); 1114 | 1115 | if ( json.extensionsRequired && json.extensionsRequired.indexOf( name ) >= 0 ) { 1116 | 1117 | throw new Error( 'THREE.GLTFLoader: WebP required by asset but unsupported.' ); 1118 | 1119 | } 1120 | 1121 | // Fall back to PNG or JPEG. 1122 | return parser.loadTexture( textureIndex ); 1123 | 1124 | } ); 1125 | 1126 | } 1127 | 1128 | detectSupport() { 1129 | 1130 | if ( ! this.isSupported ) { 1131 | 1132 | this.isSupported = new Promise( function ( resolve ) { 1133 | 1134 | const image = new Image(); 1135 | 1136 | // Lossy test image. Support for lossy images doesn't guarantee support for all 1137 | // WebP images, unfortunately. 1138 | image.src = 'data:image/webp;base64,UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA'; 1139 | 1140 | image.onload = image.onerror = function () { 1141 | 1142 | resolve( image.height === 1 ); 1143 | 1144 | }; 1145 | 1146 | } ); 1147 | 1148 | } 1149 | 1150 | return this.isSupported; 1151 | 1152 | } 1153 | 1154 | } 1155 | 1156 | /** 1157 | * meshopt BufferView Compression Extension 1158 | * 1159 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_meshopt_compression 1160 | */ 1161 | class GLTFMeshoptCompression { 1162 | 1163 | constructor( parser ) { 1164 | 1165 | this.name = EXTENSIONS.EXT_MESHOPT_COMPRESSION; 1166 | this.parser = parser; 1167 | 1168 | } 1169 | 1170 | loadBufferView( index ) { 1171 | 1172 | const json = this.parser.json; 1173 | const bufferView = json.bufferViews[ index ]; 1174 | 1175 | if ( bufferView.extensions && bufferView.extensions[ this.name ] ) { 1176 | 1177 | const extensionDef = bufferView.extensions[ this.name ]; 1178 | 1179 | const buffer = this.parser.getDependency( 'buffer', extensionDef.buffer ); 1180 | const decoder = this.parser.options.meshoptDecoder; 1181 | 1182 | if ( ! decoder || ! decoder.supported ) { 1183 | 1184 | if ( json.extensionsRequired && json.extensionsRequired.indexOf( this.name ) >= 0 ) { 1185 | 1186 | throw new Error( 'THREE.GLTFLoader: setMeshoptDecoder must be called before loading compressed files' ); 1187 | 1188 | } else { 1189 | 1190 | // Assumes that the extension is optional and that fallback buffer data is present 1191 | return null; 1192 | 1193 | } 1194 | 1195 | } 1196 | 1197 | return Promise.all( [ buffer, decoder.ready ] ).then( function ( res ) { 1198 | 1199 | const byteOffset = extensionDef.byteOffset || 0; 1200 | const byteLength = extensionDef.byteLength || 0; 1201 | 1202 | const count = extensionDef.count; 1203 | const stride = extensionDef.byteStride; 1204 | 1205 | const result = new ArrayBuffer( count * stride ); 1206 | const source = new Uint8Array( res[ 0 ], byteOffset, byteLength ); 1207 | 1208 | decoder.decodeGltfBuffer( new Uint8Array( result ), count, stride, source, extensionDef.mode, extensionDef.filter ); 1209 | return result; 1210 | 1211 | } ); 1212 | 1213 | } else { 1214 | 1215 | return null; 1216 | 1217 | } 1218 | 1219 | } 1220 | 1221 | } 1222 | 1223 | /* BINARY EXTENSION */ 1224 | const BINARY_EXTENSION_HEADER_MAGIC = 'glTF'; 1225 | const BINARY_EXTENSION_HEADER_LENGTH = 12; 1226 | const BINARY_EXTENSION_CHUNK_TYPES = { JSON: 0x4E4F534A, BIN: 0x004E4942 }; 1227 | 1228 | class GLTFBinaryExtension { 1229 | 1230 | constructor( data ) { 1231 | 1232 | this.name = EXTENSIONS.KHR_BINARY_GLTF; 1233 | this.content = null; 1234 | this.body = null; 1235 | 1236 | const headerView = new DataView( data, 0, BINARY_EXTENSION_HEADER_LENGTH ); 1237 | 1238 | this.header = { 1239 | magic: LoaderUtils.decodeText( new Uint8Array( data.slice( 0, 4 ) ) ), 1240 | version: headerView.getUint32( 4, true ), 1241 | length: headerView.getUint32( 8, true ) 1242 | }; 1243 | 1244 | if ( this.header.magic !== BINARY_EXTENSION_HEADER_MAGIC ) { 1245 | 1246 | throw new Error( 'THREE.GLTFLoader: Unsupported glTF-Binary header.' ); 1247 | 1248 | } else if ( this.header.version < 2.0 ) { 1249 | 1250 | throw new Error( 'THREE.GLTFLoader: Legacy binary file detected.' ); 1251 | 1252 | } 1253 | 1254 | const chunkContentsLength = this.header.length - BINARY_EXTENSION_HEADER_LENGTH; 1255 | const chunkView = new DataView( data, BINARY_EXTENSION_HEADER_LENGTH ); 1256 | let chunkIndex = 0; 1257 | 1258 | while ( chunkIndex < chunkContentsLength ) { 1259 | 1260 | const chunkLength = chunkView.getUint32( chunkIndex, true ); 1261 | chunkIndex += 4; 1262 | 1263 | const chunkType = chunkView.getUint32( chunkIndex, true ); 1264 | chunkIndex += 4; 1265 | 1266 | if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.JSON ) { 1267 | 1268 | const contentArray = new Uint8Array( data, BINARY_EXTENSION_HEADER_LENGTH + chunkIndex, chunkLength ); 1269 | this.content = LoaderUtils.decodeText( contentArray ); 1270 | 1271 | } else if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.BIN ) { 1272 | 1273 | const byteOffset = BINARY_EXTENSION_HEADER_LENGTH + chunkIndex; 1274 | this.body = data.slice( byteOffset, byteOffset + chunkLength ); 1275 | 1276 | } 1277 | 1278 | // Clients must ignore chunks with unknown types. 1279 | 1280 | chunkIndex += chunkLength; 1281 | 1282 | } 1283 | 1284 | if ( this.content === null ) { 1285 | 1286 | throw new Error( 'THREE.GLTFLoader: JSON content not found.' ); 1287 | 1288 | } 1289 | 1290 | } 1291 | 1292 | } 1293 | 1294 | /** 1295 | * DRACO Mesh Compression Extension 1296 | * 1297 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_draco_mesh_compression 1298 | */ 1299 | class GLTFDracoMeshCompressionExtension { 1300 | 1301 | constructor( json, dracoLoader ) { 1302 | 1303 | if ( ! dracoLoader ) { 1304 | 1305 | throw new Error( 'THREE.GLTFLoader: No DRACOLoader instance provided.' ); 1306 | 1307 | } 1308 | 1309 | this.name = EXTENSIONS.KHR_DRACO_MESH_COMPRESSION; 1310 | this.json = json; 1311 | this.dracoLoader = dracoLoader; 1312 | this.dracoLoader.preload(); 1313 | 1314 | } 1315 | 1316 | decodePrimitive( primitive, parser ) { 1317 | 1318 | const json = this.json; 1319 | const dracoLoader = this.dracoLoader; 1320 | const bufferViewIndex = primitive.extensions[ this.name ].bufferView; 1321 | const gltfAttributeMap = primitive.extensions[ this.name ].attributes; 1322 | const threeAttributeMap = {}; 1323 | const attributeNormalizedMap = {}; 1324 | const attributeTypeMap = {}; 1325 | 1326 | for ( const attributeName in gltfAttributeMap ) { 1327 | 1328 | const threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase(); 1329 | 1330 | threeAttributeMap[ threeAttributeName ] = gltfAttributeMap[ attributeName ]; 1331 | 1332 | } 1333 | 1334 | for ( const attributeName in primitive.attributes ) { 1335 | 1336 | const threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase(); 1337 | 1338 | if ( gltfAttributeMap[ attributeName ] !== undefined ) { 1339 | 1340 | const accessorDef = json.accessors[ primitive.attributes[ attributeName ] ]; 1341 | const componentType = WEBGL_COMPONENT_TYPES[ accessorDef.componentType ]; 1342 | 1343 | attributeTypeMap[ threeAttributeName ] = componentType; 1344 | attributeNormalizedMap[ threeAttributeName ] = accessorDef.normalized === true; 1345 | 1346 | } 1347 | 1348 | } 1349 | 1350 | return parser.getDependency( 'bufferView', bufferViewIndex ).then( function ( bufferView ) { 1351 | 1352 | return new Promise( function ( resolve ) { 1353 | 1354 | dracoLoader.decodeDracoFile( bufferView, function ( geometry ) { 1355 | 1356 | for ( const attributeName in geometry.attributes ) { 1357 | 1358 | const attribute = geometry.attributes[ attributeName ]; 1359 | const normalized = attributeNormalizedMap[ attributeName ]; 1360 | 1361 | if ( normalized !== undefined ) attribute.normalized = normalized; 1362 | 1363 | } 1364 | 1365 | resolve( geometry ); 1366 | 1367 | }, threeAttributeMap, attributeTypeMap ); 1368 | 1369 | } ); 1370 | 1371 | } ); 1372 | 1373 | } 1374 | 1375 | } 1376 | 1377 | /** 1378 | * Texture Transform Extension 1379 | * 1380 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_transform 1381 | */ 1382 | class GLTFTextureTransformExtension { 1383 | 1384 | constructor() { 1385 | 1386 | this.name = EXTENSIONS.KHR_TEXTURE_TRANSFORM; 1387 | 1388 | } 1389 | 1390 | extendTexture( texture, transform ) { 1391 | 1392 | if ( transform.texCoord !== undefined ) { 1393 | 1394 | console.warn( 'THREE.GLTFLoader: Custom UV sets in "' + this.name + '" extension not yet supported.' ); 1395 | 1396 | } 1397 | 1398 | if ( transform.offset === undefined && transform.rotation === undefined && transform.scale === undefined ) { 1399 | 1400 | // See https://github.com/mrdoob/three.js/issues/21819. 1401 | return texture; 1402 | 1403 | } 1404 | 1405 | texture = texture.clone(); 1406 | 1407 | if ( transform.offset !== undefined ) { 1408 | 1409 | texture.offset.fromArray( transform.offset ); 1410 | 1411 | } 1412 | 1413 | if ( transform.rotation !== undefined ) { 1414 | 1415 | texture.rotation = transform.rotation; 1416 | 1417 | } 1418 | 1419 | if ( transform.scale !== undefined ) { 1420 | 1421 | texture.repeat.fromArray( transform.scale ); 1422 | 1423 | } 1424 | 1425 | texture.needsUpdate = true; 1426 | 1427 | return texture; 1428 | 1429 | } 1430 | 1431 | } 1432 | 1433 | /** 1434 | * Specular-Glossiness Extension 1435 | * 1436 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness 1437 | */ 1438 | 1439 | /** 1440 | * A sub class of StandardMaterial with some of the functionality 1441 | * changed via the `onBeforeCompile` callback 1442 | * @pailhead 1443 | */ 1444 | class GLTFMeshStandardSGMaterial extends MeshStandardMaterial { 1445 | 1446 | constructor( params ) { 1447 | 1448 | super(); 1449 | 1450 | this.isGLTFSpecularGlossinessMaterial = true; 1451 | 1452 | //various chunks that need replacing 1453 | const specularMapParsFragmentChunk = [ 1454 | '#ifdef USE_SPECULARMAP', 1455 | ' uniform sampler2D specularMap;', 1456 | '#endif' 1457 | ].join( '\n' ); 1458 | 1459 | const glossinessMapParsFragmentChunk = [ 1460 | '#ifdef USE_GLOSSINESSMAP', 1461 | ' uniform sampler2D glossinessMap;', 1462 | '#endif' 1463 | ].join( '\n' ); 1464 | 1465 | const specularMapFragmentChunk = [ 1466 | 'vec3 specularFactor = specular;', 1467 | '#ifdef USE_SPECULARMAP', 1468 | ' vec4 texelSpecular = texture2D( specularMap, vUv );', 1469 | ' texelSpecular = sRGBToLinear( texelSpecular );', 1470 | ' // reads channel RGB, compatible with a glTF Specular-Glossiness (RGBA) texture', 1471 | ' specularFactor *= texelSpecular.rgb;', 1472 | '#endif' 1473 | ].join( '\n' ); 1474 | 1475 | const glossinessMapFragmentChunk = [ 1476 | 'float glossinessFactor = glossiness;', 1477 | '#ifdef USE_GLOSSINESSMAP', 1478 | ' vec4 texelGlossiness = texture2D( glossinessMap, vUv );', 1479 | ' // reads channel A, compatible with a glTF Specular-Glossiness (RGBA) texture', 1480 | ' glossinessFactor *= texelGlossiness.a;', 1481 | '#endif' 1482 | ].join( '\n' ); 1483 | 1484 | const lightPhysicalFragmentChunk = [ 1485 | 'PhysicalMaterial material;', 1486 | 'material.diffuseColor = diffuseColor.rgb * ( 1. - max( specularFactor.r, max( specularFactor.g, specularFactor.b ) ) );', 1487 | 'vec3 dxy = max( abs( dFdx( geometryNormal ) ), abs( dFdy( geometryNormal ) ) );', 1488 | 'float geometryRoughness = max( max( dxy.x, dxy.y ), dxy.z );', 1489 | 'material.roughness = max( 1.0 - glossinessFactor, 0.0525 ); // 0.0525 corresponds to the base mip of a 256 cubemap.', 1490 | 'material.roughness += geometryRoughness;', 1491 | 'material.roughness = min( material.roughness, 1.0 );', 1492 | 'material.specularColor = specularFactor;', 1493 | ].join( '\n' ); 1494 | 1495 | const uniforms = { 1496 | specular: { value: new Color().setHex( 0xffffff ) }, 1497 | glossiness: { value: 1 }, 1498 | specularMap: { value: null }, 1499 | glossinessMap: { value: null } 1500 | }; 1501 | 1502 | this._extraUniforms = uniforms; 1503 | 1504 | this.onBeforeCompile = function ( shader ) { 1505 | 1506 | for ( const uniformName in uniforms ) { 1507 | 1508 | shader.uniforms[ uniformName ] = uniforms[ uniformName ]; 1509 | 1510 | } 1511 | 1512 | shader.fragmentShader = shader.fragmentShader 1513 | .replace( 'uniform float roughness;', 'uniform vec3 specular;' ) 1514 | .replace( 'uniform float metalness;', 'uniform float glossiness;' ) 1515 | .replace( '#include ', specularMapParsFragmentChunk ) 1516 | .replace( '#include ', glossinessMapParsFragmentChunk ) 1517 | .replace( '#include ', specularMapFragmentChunk ) 1518 | .replace( '#include ', glossinessMapFragmentChunk ) 1519 | .replace( '#include ', lightPhysicalFragmentChunk ); 1520 | 1521 | }; 1522 | 1523 | Object.defineProperties( this, { 1524 | 1525 | specular: { 1526 | get: function () { 1527 | 1528 | return uniforms.specular.value; 1529 | 1530 | }, 1531 | set: function ( v ) { 1532 | 1533 | uniforms.specular.value = v; 1534 | 1535 | } 1536 | }, 1537 | 1538 | specularMap: { 1539 | get: function () { 1540 | 1541 | return uniforms.specularMap.value; 1542 | 1543 | }, 1544 | set: function ( v ) { 1545 | 1546 | uniforms.specularMap.value = v; 1547 | 1548 | if ( v ) { 1549 | 1550 | this.defines.USE_SPECULARMAP = ''; // USE_UV is set by the renderer for specular maps 1551 | 1552 | } else { 1553 | 1554 | delete this.defines.USE_SPECULARMAP; 1555 | 1556 | } 1557 | 1558 | } 1559 | }, 1560 | 1561 | glossiness: { 1562 | get: function () { 1563 | 1564 | return uniforms.glossiness.value; 1565 | 1566 | }, 1567 | set: function ( v ) { 1568 | 1569 | uniforms.glossiness.value = v; 1570 | 1571 | } 1572 | }, 1573 | 1574 | glossinessMap: { 1575 | get: function () { 1576 | 1577 | return uniforms.glossinessMap.value; 1578 | 1579 | }, 1580 | set: function ( v ) { 1581 | 1582 | uniforms.glossinessMap.value = v; 1583 | 1584 | if ( v ) { 1585 | 1586 | this.defines.USE_GLOSSINESSMAP = ''; 1587 | this.defines.USE_UV = ''; 1588 | 1589 | } else { 1590 | 1591 | delete this.defines.USE_GLOSSINESSMAP; 1592 | delete this.defines.USE_UV; 1593 | 1594 | } 1595 | 1596 | } 1597 | } 1598 | 1599 | } ); 1600 | 1601 | delete this.metalness; 1602 | delete this.roughness; 1603 | delete this.metalnessMap; 1604 | delete this.roughnessMap; 1605 | 1606 | this.setValues( params ); 1607 | 1608 | } 1609 | 1610 | copy( source ) { 1611 | 1612 | super.copy( source ); 1613 | 1614 | this.specularMap = source.specularMap; 1615 | this.specular.copy( source.specular ); 1616 | this.glossinessMap = source.glossinessMap; 1617 | this.glossiness = source.glossiness; 1618 | delete this.metalness; 1619 | delete this.roughness; 1620 | delete this.metalnessMap; 1621 | delete this.roughnessMap; 1622 | return this; 1623 | 1624 | } 1625 | 1626 | } 1627 | 1628 | 1629 | class GLTFMaterialsPbrSpecularGlossinessExtension { 1630 | 1631 | constructor() { 1632 | 1633 | this.name = EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS; 1634 | 1635 | this.specularGlossinessParams = [ 1636 | 'color', 1637 | 'map', 1638 | 'lightMap', 1639 | 'lightMapIntensity', 1640 | 'aoMap', 1641 | 'aoMapIntensity', 1642 | 'emissive', 1643 | 'emissiveIntensity', 1644 | 'emissiveMap', 1645 | 'bumpMap', 1646 | 'bumpScale', 1647 | 'normalMap', 1648 | 'normalMapType', 1649 | 'displacementMap', 1650 | 'displacementScale', 1651 | 'displacementBias', 1652 | 'specularMap', 1653 | 'specular', 1654 | 'glossinessMap', 1655 | 'glossiness', 1656 | 'alphaMap', 1657 | 'envMap', 1658 | 'envMapIntensity', 1659 | 'refractionRatio', 1660 | ]; 1661 | 1662 | } 1663 | 1664 | getMaterialType() { 1665 | 1666 | return GLTFMeshStandardSGMaterial; 1667 | 1668 | } 1669 | 1670 | extendParams( materialParams, materialDef, parser ) { 1671 | 1672 | const pbrSpecularGlossiness = materialDef.extensions[ this.name ]; 1673 | 1674 | materialParams.color = new Color( 1.0, 1.0, 1.0 ); 1675 | materialParams.opacity = 1.0; 1676 | 1677 | const pending = []; 1678 | 1679 | if ( Array.isArray( pbrSpecularGlossiness.diffuseFactor ) ) { 1680 | 1681 | const array = pbrSpecularGlossiness.diffuseFactor; 1682 | 1683 | materialParams.color.fromArray( array ); 1684 | materialParams.opacity = array[ 3 ]; 1685 | 1686 | } 1687 | 1688 | if ( pbrSpecularGlossiness.diffuseTexture !== undefined ) { 1689 | 1690 | pending.push( parser.assignTexture( materialParams, 'map', pbrSpecularGlossiness.diffuseTexture ) ); 1691 | 1692 | } 1693 | 1694 | materialParams.emissive = new Color( 0.0, 0.0, 0.0 ); 1695 | materialParams.glossiness = pbrSpecularGlossiness.glossinessFactor !== undefined ? pbrSpecularGlossiness.glossinessFactor : 1.0; 1696 | materialParams.specular = new Color( 1.0, 1.0, 1.0 ); 1697 | 1698 | if ( Array.isArray( pbrSpecularGlossiness.specularFactor ) ) { 1699 | 1700 | materialParams.specular.fromArray( pbrSpecularGlossiness.specularFactor ); 1701 | 1702 | } 1703 | 1704 | if ( pbrSpecularGlossiness.specularGlossinessTexture !== undefined ) { 1705 | 1706 | const specGlossMapDef = pbrSpecularGlossiness.specularGlossinessTexture; 1707 | pending.push( parser.assignTexture( materialParams, 'glossinessMap', specGlossMapDef ) ); 1708 | pending.push( parser.assignTexture( materialParams, 'specularMap', specGlossMapDef ) ); 1709 | 1710 | } 1711 | 1712 | return Promise.all( pending ); 1713 | 1714 | } 1715 | 1716 | createMaterial( materialParams ) { 1717 | 1718 | const material = new GLTFMeshStandardSGMaterial( materialParams ); 1719 | material.fog = true; 1720 | 1721 | material.color = materialParams.color; 1722 | 1723 | material.map = materialParams.map === undefined ? null : materialParams.map; 1724 | 1725 | material.lightMap = null; 1726 | material.lightMapIntensity = 1.0; 1727 | 1728 | material.aoMap = materialParams.aoMap === undefined ? null : materialParams.aoMap; 1729 | material.aoMapIntensity = 1.0; 1730 | 1731 | material.emissive = materialParams.emissive; 1732 | material.emissiveIntensity = 1.0; 1733 | material.emissiveMap = materialParams.emissiveMap === undefined ? null : materialParams.emissiveMap; 1734 | 1735 | material.bumpMap = materialParams.bumpMap === undefined ? null : materialParams.bumpMap; 1736 | material.bumpScale = 1; 1737 | 1738 | material.normalMap = materialParams.normalMap === undefined ? null : materialParams.normalMap; 1739 | material.normalMapType = TangentSpaceNormalMap; 1740 | 1741 | if ( materialParams.normalScale ) material.normalScale = materialParams.normalScale; 1742 | 1743 | material.displacementMap = null; 1744 | material.displacementScale = 1; 1745 | material.displacementBias = 0; 1746 | 1747 | material.specularMap = materialParams.specularMap === undefined ? null : materialParams.specularMap; 1748 | material.specular = materialParams.specular; 1749 | 1750 | material.glossinessMap = materialParams.glossinessMap === undefined ? null : materialParams.glossinessMap; 1751 | material.glossiness = materialParams.glossiness; 1752 | 1753 | material.alphaMap = null; 1754 | 1755 | material.envMap = materialParams.envMap === undefined ? null : materialParams.envMap; 1756 | material.envMapIntensity = 1.0; 1757 | 1758 | material.refractionRatio = 0.98; 1759 | 1760 | return material; 1761 | 1762 | } 1763 | 1764 | } 1765 | 1766 | /** 1767 | * Mesh Quantization Extension 1768 | * 1769 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_mesh_quantization 1770 | */ 1771 | class GLTFMeshQuantizationExtension { 1772 | 1773 | constructor() { 1774 | 1775 | this.name = EXTENSIONS.KHR_MESH_QUANTIZATION; 1776 | 1777 | } 1778 | 1779 | } 1780 | 1781 | /*********************************/ 1782 | /********** INTERPOLATION ********/ 1783 | /*********************************/ 1784 | 1785 | // Spline Interpolation 1786 | // Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#appendix-c-spline-interpolation 1787 | class GLTFCubicSplineInterpolant extends Interpolant { 1788 | 1789 | constructor( parameterPositions, sampleValues, sampleSize, resultBuffer ) { 1790 | 1791 | super( parameterPositions, sampleValues, sampleSize, resultBuffer ); 1792 | 1793 | } 1794 | 1795 | copySampleValue_( index ) { 1796 | 1797 | // Copies a sample value to the result buffer. See description of glTF 1798 | // CUBICSPLINE values layout in interpolate_() function below. 1799 | 1800 | const result = this.resultBuffer, 1801 | values = this.sampleValues, 1802 | valueSize = this.valueSize, 1803 | offset = index * valueSize * 3 + valueSize; 1804 | 1805 | for ( let i = 0; i !== valueSize; i ++ ) { 1806 | 1807 | result[ i ] = values[ offset + i ]; 1808 | 1809 | } 1810 | 1811 | return result; 1812 | 1813 | } 1814 | 1815 | } 1816 | 1817 | GLTFCubicSplineInterpolant.prototype.beforeStart_ = GLTFCubicSplineInterpolant.prototype.copySampleValue_; 1818 | 1819 | GLTFCubicSplineInterpolant.prototype.afterEnd_ = GLTFCubicSplineInterpolant.prototype.copySampleValue_; 1820 | 1821 | GLTFCubicSplineInterpolant.prototype.interpolate_ = function ( i1, t0, t, t1 ) { 1822 | 1823 | const result = this.resultBuffer; 1824 | const values = this.sampleValues; 1825 | const stride = this.valueSize; 1826 | 1827 | const stride2 = stride * 2; 1828 | const stride3 = stride * 3; 1829 | 1830 | const td = t1 - t0; 1831 | 1832 | const p = ( t - t0 ) / td; 1833 | const pp = p * p; 1834 | const ppp = pp * p; 1835 | 1836 | const offset1 = i1 * stride3; 1837 | const offset0 = offset1 - stride3; 1838 | 1839 | const s2 = - 2 * ppp + 3 * pp; 1840 | const s3 = ppp - pp; 1841 | const s0 = 1 - s2; 1842 | const s1 = s3 - pp + p; 1843 | 1844 | // Layout of keyframe output values for CUBICSPLINE animations: 1845 | // [ inTangent_1, splineVertex_1, outTangent_1, inTangent_2, splineVertex_2, ... ] 1846 | for ( let i = 0; i !== stride; i ++ ) { 1847 | 1848 | const p0 = values[ offset0 + i + stride ]; // splineVertex_k 1849 | const m0 = values[ offset0 + i + stride2 ] * td; // outTangent_k * (t_k+1 - t_k) 1850 | const p1 = values[ offset1 + i + stride ]; // splineVertex_k+1 1851 | const m1 = values[ offset1 + i ] * td; // inTangent_k+1 * (t_k+1 - t_k) 1852 | 1853 | result[ i ] = s0 * p0 + s1 * m0 + s2 * p1 + s3 * m1; 1854 | 1855 | } 1856 | 1857 | return result; 1858 | 1859 | }; 1860 | 1861 | const _q = new Quaternion(); 1862 | 1863 | class GLTFCubicSplineQuaternionInterpolant extends GLTFCubicSplineInterpolant { 1864 | 1865 | interpolate_( i1, t0, t, t1 ) { 1866 | 1867 | const result = super.interpolate_( i1, t0, t, t1 ); 1868 | 1869 | _q.fromArray( result ).normalize().toArray( result ); 1870 | 1871 | return result; 1872 | 1873 | } 1874 | 1875 | } 1876 | 1877 | 1878 | /*********************************/ 1879 | /********** INTERNALS ************/ 1880 | /*********************************/ 1881 | 1882 | /* CONSTANTS */ 1883 | 1884 | const WEBGL_CONSTANTS = { 1885 | FLOAT: 5126, 1886 | //FLOAT_MAT2: 35674, 1887 | FLOAT_MAT3: 35675, 1888 | FLOAT_MAT4: 35676, 1889 | FLOAT_VEC2: 35664, 1890 | FLOAT_VEC3: 35665, 1891 | FLOAT_VEC4: 35666, 1892 | LINEAR: 9729, 1893 | REPEAT: 10497, 1894 | SAMPLER_2D: 35678, 1895 | POINTS: 0, 1896 | LINES: 1, 1897 | LINE_LOOP: 2, 1898 | LINE_STRIP: 3, 1899 | TRIANGLES: 4, 1900 | TRIANGLE_STRIP: 5, 1901 | TRIANGLE_FAN: 6, 1902 | UNSIGNED_BYTE: 5121, 1903 | UNSIGNED_SHORT: 5123 1904 | }; 1905 | 1906 | const WEBGL_COMPONENT_TYPES = { 1907 | 5120: Int8Array, 1908 | 5121: Uint8Array, 1909 | 5122: Int16Array, 1910 | 5123: Uint16Array, 1911 | 5125: Uint32Array, 1912 | 5126: Float32Array 1913 | }; 1914 | 1915 | const WEBGL_FILTERS = { 1916 | 9728: NearestFilter, 1917 | 9729: LinearFilter, 1918 | 9984: NearestMipmapNearestFilter, 1919 | 9985: LinearMipmapNearestFilter, 1920 | 9986: NearestMipmapLinearFilter, 1921 | 9987: LinearMipmapLinearFilter 1922 | }; 1923 | 1924 | const WEBGL_WRAPPINGS = { 1925 | 33071: ClampToEdgeWrapping, 1926 | 33648: MirroredRepeatWrapping, 1927 | 10497: RepeatWrapping 1928 | }; 1929 | 1930 | const WEBGL_TYPE_SIZES = { 1931 | 'SCALAR': 1, 1932 | 'VEC2': 2, 1933 | 'VEC3': 3, 1934 | 'VEC4': 4, 1935 | 'MAT2': 4, 1936 | 'MAT3': 9, 1937 | 'MAT4': 16 1938 | }; 1939 | 1940 | const ATTRIBUTES = { 1941 | POSITION: 'position', 1942 | NORMAL: 'normal', 1943 | TANGENT: 'tangent', 1944 | TEXCOORD_0: 'uv', 1945 | TEXCOORD_1: 'uv2', 1946 | COLOR_0: 'color', 1947 | WEIGHTS_0: 'skinWeight', 1948 | JOINTS_0: 'skinIndex', 1949 | }; 1950 | 1951 | const PATH_PROPERTIES = { 1952 | scale: 'scale', 1953 | translation: 'position', 1954 | rotation: 'quaternion', 1955 | weights: 'morphTargetInfluences' 1956 | }; 1957 | 1958 | const INTERPOLATION = { 1959 | CUBICSPLINE: undefined, // We use a custom interpolant (GLTFCubicSplineInterpolation) for CUBICSPLINE tracks. Each 1960 | // keyframe track will be initialized with a default interpolation type, then modified. 1961 | LINEAR: InterpolateLinear, 1962 | STEP: InterpolateDiscrete 1963 | }; 1964 | 1965 | const ALPHA_MODES = { 1966 | OPAQUE: 'OPAQUE', 1967 | MASK: 'MASK', 1968 | BLEND: 'BLEND' 1969 | }; 1970 | 1971 | /** 1972 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#default-material 1973 | */ 1974 | function createDefaultMaterial( cache ) { 1975 | 1976 | if ( cache[ 'DefaultMaterial' ] === undefined ) { 1977 | 1978 | cache[ 'DefaultMaterial' ] = new MeshStandardMaterial( { 1979 | color: 0xFFFFFF, 1980 | emissive: 0x000000, 1981 | metalness: 1, 1982 | roughness: 1, 1983 | transparent: false, 1984 | depthTest: true, 1985 | side: FrontSide 1986 | } ); 1987 | 1988 | } 1989 | 1990 | return cache[ 'DefaultMaterial' ]; 1991 | 1992 | } 1993 | 1994 | function addUnknownExtensionsToUserData( knownExtensions, object, objectDef ) { 1995 | 1996 | // Add unknown glTF extensions to an object's userData. 1997 | 1998 | for ( const name in objectDef.extensions ) { 1999 | 2000 | if ( knownExtensions[ name ] === undefined ) { 2001 | 2002 | object.userData.gltfExtensions = object.userData.gltfExtensions || {}; 2003 | object.userData.gltfExtensions[ name ] = objectDef.extensions[ name ]; 2004 | 2005 | } 2006 | 2007 | } 2008 | 2009 | } 2010 | 2011 | /** 2012 | * @param {Object3D|Material|BufferGeometry} object 2013 | * @param {GLTF.definition} gltfDef 2014 | */ 2015 | function assignExtrasToUserData( object, gltfDef ) { 2016 | 2017 | if ( gltfDef.extras !== undefined ) { 2018 | 2019 | if ( typeof gltfDef.extras === 'object' ) { 2020 | 2021 | Object.assign( object.userData, gltfDef.extras ); 2022 | 2023 | } else { 2024 | 2025 | console.warn( 'THREE.GLTFLoader: Ignoring primitive type .extras, ' + gltfDef.extras ); 2026 | 2027 | } 2028 | 2029 | } 2030 | 2031 | } 2032 | 2033 | /** 2034 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#morph-targets 2035 | * 2036 | * @param {BufferGeometry} geometry 2037 | * @param {Array} targets 2038 | * @param {GLTFParser} parser 2039 | * @return {Promise} 2040 | */ 2041 | function addMorphTargets( geometry, targets, parser ) { 2042 | 2043 | let hasMorphPosition = false; 2044 | let hasMorphNormal = false; 2045 | 2046 | for ( let i = 0, il = targets.length; i < il; i ++ ) { 2047 | 2048 | const target = targets[ i ]; 2049 | 2050 | if ( target.POSITION !== undefined ) hasMorphPosition = true; 2051 | if ( target.NORMAL !== undefined ) hasMorphNormal = true; 2052 | 2053 | if ( hasMorphPosition && hasMorphNormal ) break; 2054 | 2055 | } 2056 | 2057 | if ( ! hasMorphPosition && ! hasMorphNormal ) return Promise.resolve( geometry ); 2058 | 2059 | const pendingPositionAccessors = []; 2060 | const pendingNormalAccessors = []; 2061 | 2062 | for ( let i = 0, il = targets.length; i < il; i ++ ) { 2063 | 2064 | const target = targets[ i ]; 2065 | 2066 | if ( hasMorphPosition ) { 2067 | 2068 | const pendingAccessor = target.POSITION !== undefined 2069 | ? parser.getDependency( 'accessor', target.POSITION ) 2070 | : geometry.attributes.position; 2071 | 2072 | pendingPositionAccessors.push( pendingAccessor ); 2073 | 2074 | } 2075 | 2076 | if ( hasMorphNormal ) { 2077 | 2078 | const pendingAccessor = target.NORMAL !== undefined 2079 | ? parser.getDependency( 'accessor', target.NORMAL ) 2080 | : geometry.attributes.normal; 2081 | 2082 | pendingNormalAccessors.push( pendingAccessor ); 2083 | 2084 | } 2085 | 2086 | } 2087 | 2088 | return Promise.all( [ 2089 | Promise.all( pendingPositionAccessors ), 2090 | Promise.all( pendingNormalAccessors ) 2091 | ] ).then( function ( accessors ) { 2092 | 2093 | const morphPositions = accessors[ 0 ]; 2094 | const morphNormals = accessors[ 1 ]; 2095 | 2096 | if ( hasMorphPosition ) geometry.morphAttributes.position = morphPositions; 2097 | if ( hasMorphNormal ) geometry.morphAttributes.normal = morphNormals; 2098 | geometry.morphTargetsRelative = true; 2099 | 2100 | return geometry; 2101 | 2102 | } ); 2103 | 2104 | } 2105 | 2106 | /** 2107 | * @param {Mesh} mesh 2108 | * @param {GLTF.Mesh} meshDef 2109 | */ 2110 | function updateMorphTargets( mesh, meshDef ) { 2111 | 2112 | mesh.updateMorphTargets(); 2113 | 2114 | if ( meshDef.weights !== undefined ) { 2115 | 2116 | for ( let i = 0, il = meshDef.weights.length; i < il; i ++ ) { 2117 | 2118 | mesh.morphTargetInfluences[ i ] = meshDef.weights[ i ]; 2119 | 2120 | } 2121 | 2122 | } 2123 | 2124 | // .extras has user-defined data, so check that .extras.targetNames is an array. 2125 | if ( meshDef.extras && Array.isArray( meshDef.extras.targetNames ) ) { 2126 | 2127 | const targetNames = meshDef.extras.targetNames; 2128 | 2129 | if ( mesh.morphTargetInfluences.length === targetNames.length ) { 2130 | 2131 | mesh.morphTargetDictionary = {}; 2132 | 2133 | for ( let i = 0, il = targetNames.length; i < il; i ++ ) { 2134 | 2135 | mesh.morphTargetDictionary[ targetNames[ i ] ] = i; 2136 | 2137 | } 2138 | 2139 | } else { 2140 | 2141 | console.warn( 'THREE.GLTFLoader: Invalid extras.targetNames length. Ignoring names.' ); 2142 | 2143 | } 2144 | 2145 | } 2146 | 2147 | } 2148 | 2149 | function createPrimitiveKey( primitiveDef ) { 2150 | 2151 | const dracoExtension = primitiveDef.extensions && primitiveDef.extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ]; 2152 | let geometryKey; 2153 | 2154 | if ( dracoExtension ) { 2155 | 2156 | geometryKey = 'draco:' + dracoExtension.bufferView 2157 | + ':' + dracoExtension.indices 2158 | + ':' + createAttributesKey( dracoExtension.attributes ); 2159 | 2160 | } else { 2161 | 2162 | geometryKey = primitiveDef.indices + ':' + createAttributesKey( primitiveDef.attributes ) + ':' + primitiveDef.mode; 2163 | 2164 | } 2165 | 2166 | return geometryKey; 2167 | 2168 | } 2169 | 2170 | function createAttributesKey( attributes ) { 2171 | 2172 | let attributesKey = ''; 2173 | 2174 | const keys = Object.keys( attributes ).sort(); 2175 | 2176 | for ( let i = 0, il = keys.length; i < il; i ++ ) { 2177 | 2178 | attributesKey += keys[ i ] + ':' + attributes[ keys[ i ] ] + ';'; 2179 | 2180 | } 2181 | 2182 | return attributesKey; 2183 | 2184 | } 2185 | 2186 | function getNormalizedComponentScale( constructor ) { 2187 | 2188 | // Reference: 2189 | // https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_mesh_quantization#encoding-quantized-data 2190 | 2191 | switch ( constructor ) { 2192 | 2193 | case Int8Array: 2194 | return 1 / 127; 2195 | 2196 | case Uint8Array: 2197 | return 1 / 255; 2198 | 2199 | case Int16Array: 2200 | return 1 / 32767; 2201 | 2202 | case Uint16Array: 2203 | return 1 / 65535; 2204 | 2205 | default: 2206 | throw new Error( 'THREE.GLTFLoader: Unsupported normalized accessor component type.' ); 2207 | 2208 | } 2209 | 2210 | } 2211 | 2212 | /* GLTF PARSER */ 2213 | 2214 | class GLTFParser { 2215 | 2216 | constructor( json = {}, options = {} ) { 2217 | 2218 | this.json = json; 2219 | this.extensions = {}; 2220 | this.plugins = {}; 2221 | this.options = options; 2222 | 2223 | // loader object cache 2224 | this.cache = new GLTFRegistry(); 2225 | 2226 | // associations between Three.js objects and glTF elements 2227 | this.associations = new Map(); 2228 | 2229 | // BufferGeometry caching 2230 | this.primitiveCache = {}; 2231 | 2232 | // Object3D instance caches 2233 | this.meshCache = { refs: {}, uses: {} }; 2234 | this.cameraCache = { refs: {}, uses: {} }; 2235 | this.lightCache = { refs: {}, uses: {} }; 2236 | 2237 | this.textureCache = {}; 2238 | 2239 | // Track node names, to ensure no duplicates 2240 | this.nodeNamesUsed = {}; 2241 | 2242 | // Use an ImageBitmapLoader if imageBitmaps are supported. Moves much of the 2243 | // expensive work of uploading a texture to the GPU off the main thread. 2244 | if ( typeof createImageBitmap !== 'undefined' && /Firefox/.test( navigator.userAgent ) === false ) { 2245 | 2246 | this.textureLoader = new ImageBitmapLoader( this.options.manager ); 2247 | 2248 | } else { 2249 | 2250 | this.textureLoader = new TextureLoader( this.options.manager ); 2251 | 2252 | } 2253 | 2254 | this.textureLoader.setCrossOrigin( this.options.crossOrigin ); 2255 | this.textureLoader.setRequestHeader( this.options.requestHeader ); 2256 | 2257 | this.fileLoader = new FileLoader( this.options.manager ); 2258 | this.fileLoader.setResponseType( 'arraybuffer' ); 2259 | 2260 | if ( this.options.crossOrigin === 'use-credentials' ) { 2261 | 2262 | this.fileLoader.setWithCredentials( true ); 2263 | 2264 | } 2265 | 2266 | } 2267 | 2268 | setExtensions( extensions ) { 2269 | 2270 | this.extensions = extensions; 2271 | 2272 | } 2273 | 2274 | setPlugins( plugins ) { 2275 | 2276 | this.plugins = plugins; 2277 | 2278 | } 2279 | 2280 | parse( onLoad, onError ) { 2281 | 2282 | const parser = this; 2283 | const json = this.json; 2284 | const extensions = this.extensions; 2285 | 2286 | // Clear the loader cache 2287 | this.cache.removeAll(); 2288 | 2289 | // Mark the special nodes/meshes in json for efficient parse 2290 | this._invokeAll( function ( ext ) { 2291 | 2292 | return ext._markDefs && ext._markDefs(); 2293 | 2294 | } ); 2295 | 2296 | Promise.all( this._invokeAll( function ( ext ) { 2297 | 2298 | return ext.beforeRoot && ext.beforeRoot(); 2299 | 2300 | } ) ).then( function () { 2301 | 2302 | return Promise.all( [ 2303 | 2304 | parser.getDependencies( 'scene' ), 2305 | parser.getDependencies( 'animation' ), 2306 | parser.getDependencies( 'camera' ), 2307 | 2308 | ] ); 2309 | 2310 | } ).then( function ( dependencies ) { 2311 | 2312 | const result = { 2313 | scene: dependencies[ 0 ][ json.scene || 0 ], 2314 | scenes: dependencies[ 0 ], 2315 | animations: dependencies[ 1 ], 2316 | cameras: dependencies[ 2 ], 2317 | asset: json.asset, 2318 | parser: parser, 2319 | userData: {} 2320 | }; 2321 | 2322 | addUnknownExtensionsToUserData( extensions, result, json ); 2323 | 2324 | assignExtrasToUserData( result, json ); 2325 | 2326 | Promise.all( parser._invokeAll( function ( ext ) { 2327 | 2328 | return ext.afterRoot && ext.afterRoot( result ); 2329 | 2330 | } ) ).then( function () { 2331 | 2332 | onLoad( result ); 2333 | 2334 | } ); 2335 | 2336 | } ).catch( onError ); 2337 | 2338 | } 2339 | 2340 | /** 2341 | * Marks the special nodes/meshes in json for efficient parse. 2342 | */ 2343 | _markDefs() { 2344 | 2345 | const nodeDefs = this.json.nodes || []; 2346 | const skinDefs = this.json.skins || []; 2347 | const meshDefs = this.json.meshes || []; 2348 | 2349 | // Nothing in the node definition indicates whether it is a Bone or an 2350 | // Object3D. Use the skins' joint references to mark bones. 2351 | for ( let skinIndex = 0, skinLength = skinDefs.length; skinIndex < skinLength; skinIndex ++ ) { 2352 | 2353 | const joints = skinDefs[ skinIndex ].joints; 2354 | 2355 | for ( let i = 0, il = joints.length; i < il; i ++ ) { 2356 | 2357 | nodeDefs[ joints[ i ] ].isBone = true; 2358 | 2359 | } 2360 | 2361 | } 2362 | 2363 | // Iterate over all nodes, marking references to shared resources, 2364 | // as well as skeleton joints. 2365 | for ( let nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex ++ ) { 2366 | 2367 | const nodeDef = nodeDefs[ nodeIndex ]; 2368 | 2369 | if ( nodeDef.mesh !== undefined ) { 2370 | 2371 | this._addNodeRef( this.meshCache, nodeDef.mesh ); 2372 | 2373 | // Nothing in the mesh definition indicates whether it is 2374 | // a SkinnedMesh or Mesh. Use the node's mesh reference 2375 | // to mark SkinnedMesh if node has skin. 2376 | if ( nodeDef.skin !== undefined ) { 2377 | 2378 | meshDefs[ nodeDef.mesh ].isSkinnedMesh = true; 2379 | 2380 | } 2381 | 2382 | } 2383 | 2384 | if ( nodeDef.camera !== undefined ) { 2385 | 2386 | this._addNodeRef( this.cameraCache, nodeDef.camera ); 2387 | 2388 | } 2389 | 2390 | } 2391 | 2392 | } 2393 | 2394 | /** 2395 | * Counts references to shared node / Object3D resources. These resources 2396 | * can be reused, or "instantiated", at multiple nodes in the scene 2397 | * hierarchy. Mesh, Camera, and Light instances are instantiated and must 2398 | * be marked. Non-scenegraph resources (like Materials, Geometries, and 2399 | * Textures) can be reused directly and are not marked here. 2400 | * 2401 | * Example: CesiumMilkTruck sample model reuses "Wheel" meshes. 2402 | */ 2403 | _addNodeRef( cache, index ) { 2404 | 2405 | if ( index === undefined ) return; 2406 | 2407 | if ( cache.refs[ index ] === undefined ) { 2408 | 2409 | cache.refs[ index ] = cache.uses[ index ] = 0; 2410 | 2411 | } 2412 | 2413 | cache.refs[ index ] ++; 2414 | 2415 | } 2416 | 2417 | /** Returns a reference to a shared resource, cloning it if necessary. */ 2418 | _getNodeRef( cache, index, object ) { 2419 | 2420 | if ( cache.refs[ index ] <= 1 ) return object; 2421 | 2422 | const ref = object.clone(); 2423 | 2424 | // Propagates mappings to the cloned object, prevents mappings on the 2425 | // original object from being lost. 2426 | const updateMappings = ( original, clone ) => { 2427 | 2428 | const mappings = this.associations.get( original ); 2429 | if ( mappings != null ) { 2430 | 2431 | this.associations.set( clone, mappings ); 2432 | 2433 | } 2434 | 2435 | for ( const [ i, child ] of original.children.entries() ) { 2436 | 2437 | updateMappings( child, clone.children[ i ] ); 2438 | 2439 | } 2440 | 2441 | }; 2442 | 2443 | updateMappings( object, ref ); 2444 | 2445 | ref.name += '_instance_' + ( cache.uses[ index ] ++ ); 2446 | 2447 | return ref; 2448 | 2449 | } 2450 | 2451 | _invokeOne( func ) { 2452 | 2453 | const extensions = Object.values( this.plugins ); 2454 | extensions.push( this ); 2455 | 2456 | for ( let i = 0; i < extensions.length; i ++ ) { 2457 | 2458 | const result = func( extensions[ i ] ); 2459 | 2460 | if ( result ) return result; 2461 | 2462 | } 2463 | 2464 | return null; 2465 | 2466 | } 2467 | 2468 | _invokeAll( func ) { 2469 | 2470 | const extensions = Object.values( this.plugins ); 2471 | extensions.unshift( this ); 2472 | 2473 | const pending = []; 2474 | 2475 | for ( let i = 0; i < extensions.length; i ++ ) { 2476 | 2477 | const result = func( extensions[ i ] ); 2478 | 2479 | if ( result ) pending.push( result ); 2480 | 2481 | } 2482 | 2483 | return pending; 2484 | 2485 | } 2486 | 2487 | /** 2488 | * Requests the specified dependency asynchronously, with caching. 2489 | * @param {string} type 2490 | * @param {number} index 2491 | * @return {Promise} 2492 | */ 2493 | getDependency( type, index ) { 2494 | 2495 | const cacheKey = type + ':' + index; 2496 | let dependency = this.cache.get( cacheKey ); 2497 | 2498 | if ( ! dependency ) { 2499 | 2500 | switch ( type ) { 2501 | 2502 | case 'scene': 2503 | dependency = this.loadScene( index ); 2504 | break; 2505 | 2506 | case 'node': 2507 | dependency = this.loadNode( index ); 2508 | break; 2509 | 2510 | case 'mesh': 2511 | dependency = this._invokeOne( function ( ext ) { 2512 | 2513 | return ext.loadMesh && ext.loadMesh( index ); 2514 | 2515 | } ); 2516 | break; 2517 | 2518 | case 'accessor': 2519 | dependency = this.loadAccessor( index ); 2520 | break; 2521 | 2522 | case 'bufferView': 2523 | dependency = this._invokeOne( function ( ext ) { 2524 | 2525 | return ext.loadBufferView && ext.loadBufferView( index ); 2526 | 2527 | } ); 2528 | break; 2529 | 2530 | case 'buffer': 2531 | dependency = this.loadBuffer( index ); 2532 | break; 2533 | 2534 | case 'material': 2535 | dependency = this._invokeOne( function ( ext ) { 2536 | 2537 | return ext.loadMaterial && ext.loadMaterial( index ); 2538 | 2539 | } ); 2540 | break; 2541 | 2542 | case 'texture': 2543 | dependency = this._invokeOne( function ( ext ) { 2544 | 2545 | return ext.loadTexture && ext.loadTexture( index ); 2546 | 2547 | } ); 2548 | break; 2549 | 2550 | case 'skin': 2551 | dependency = this.loadSkin( index ); 2552 | break; 2553 | 2554 | case 'animation': 2555 | dependency = this.loadAnimation( index ); 2556 | break; 2557 | 2558 | case 'camera': 2559 | dependency = this.loadCamera( index ); 2560 | break; 2561 | 2562 | default: 2563 | throw new Error( 'Unknown type: ' + type ); 2564 | 2565 | } 2566 | 2567 | this.cache.add( cacheKey, dependency ); 2568 | 2569 | } 2570 | 2571 | return dependency; 2572 | 2573 | } 2574 | 2575 | /** 2576 | * Requests all dependencies of the specified type asynchronously, with caching. 2577 | * @param {string} type 2578 | * @return {Promise>} 2579 | */ 2580 | getDependencies( type ) { 2581 | 2582 | let dependencies = this.cache.get( type ); 2583 | 2584 | if ( ! dependencies ) { 2585 | 2586 | const parser = this; 2587 | const defs = this.json[ type + ( type === 'mesh' ? 'es' : 's' ) ] || []; 2588 | 2589 | dependencies = Promise.all( defs.map( function ( def, index ) { 2590 | 2591 | return parser.getDependency( type, index ); 2592 | 2593 | } ) ); 2594 | 2595 | this.cache.add( type, dependencies ); 2596 | 2597 | } 2598 | 2599 | return dependencies; 2600 | 2601 | } 2602 | 2603 | /** 2604 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views 2605 | * @param {number} bufferIndex 2606 | * @return {Promise} 2607 | */ 2608 | loadBuffer( bufferIndex ) { 2609 | 2610 | const bufferDef = this.json.buffers[ bufferIndex ]; 2611 | const loader = this.fileLoader; 2612 | 2613 | if ( bufferDef.type && bufferDef.type !== 'arraybuffer' ) { 2614 | 2615 | throw new Error( 'THREE.GLTFLoader: ' + bufferDef.type + ' buffer type is not supported.' ); 2616 | 2617 | } 2618 | 2619 | // If present, GLB container is required to be the first buffer. 2620 | if ( bufferDef.uri === undefined && bufferIndex === 0 ) { 2621 | 2622 | return Promise.resolve( this.extensions[ EXTENSIONS.KHR_BINARY_GLTF ].body ); 2623 | 2624 | } 2625 | 2626 | const options = this.options; 2627 | 2628 | return new Promise( function ( resolve, reject ) { 2629 | 2630 | loader.load( LoaderUtils.resolveURL( bufferDef.uri, options.path ), resolve, undefined, function () { 2631 | 2632 | reject( new Error( 'THREE.GLTFLoader: Failed to load buffer "' + bufferDef.uri + '".' ) ); 2633 | 2634 | } ); 2635 | 2636 | } ); 2637 | 2638 | } 2639 | 2640 | /** 2641 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views 2642 | * @param {number} bufferViewIndex 2643 | * @return {Promise} 2644 | */ 2645 | loadBufferView( bufferViewIndex ) { 2646 | 2647 | const bufferViewDef = this.json.bufferViews[ bufferViewIndex ]; 2648 | 2649 | return this.getDependency( 'buffer', bufferViewDef.buffer ).then( function ( buffer ) { 2650 | 2651 | const byteLength = bufferViewDef.byteLength || 0; 2652 | const byteOffset = bufferViewDef.byteOffset || 0; 2653 | return buffer.slice( byteOffset, byteOffset + byteLength ); 2654 | 2655 | } ); 2656 | 2657 | } 2658 | 2659 | /** 2660 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#accessors 2661 | * @param {number} accessorIndex 2662 | * @return {Promise} 2663 | */ 2664 | loadAccessor( accessorIndex ) { 2665 | 2666 | const parser = this; 2667 | const json = this.json; 2668 | 2669 | const accessorDef = this.json.accessors[ accessorIndex ]; 2670 | 2671 | if ( accessorDef.bufferView === undefined && accessorDef.sparse === undefined ) { 2672 | 2673 | // Ignore empty accessors, which may be used to declare runtime 2674 | // information about attributes coming from another source (e.g. Draco 2675 | // compression extension). 2676 | return Promise.resolve( null ); 2677 | 2678 | } 2679 | 2680 | const pendingBufferViews = []; 2681 | 2682 | if ( accessorDef.bufferView !== undefined ) { 2683 | 2684 | pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.bufferView ) ); 2685 | 2686 | } else { 2687 | 2688 | pendingBufferViews.push( null ); 2689 | 2690 | } 2691 | 2692 | if ( accessorDef.sparse !== undefined ) { 2693 | 2694 | pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.sparse.indices.bufferView ) ); 2695 | pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.sparse.values.bufferView ) ); 2696 | 2697 | } 2698 | 2699 | return Promise.all( pendingBufferViews ).then( function ( bufferViews ) { 2700 | 2701 | const bufferView = bufferViews[ 0 ]; 2702 | 2703 | const itemSize = WEBGL_TYPE_SIZES[ accessorDef.type ]; 2704 | const TypedArray = WEBGL_COMPONENT_TYPES[ accessorDef.componentType ]; 2705 | 2706 | // For VEC3: itemSize is 3, elementBytes is 4, itemBytes is 12. 2707 | const elementBytes = TypedArray.BYTES_PER_ELEMENT; 2708 | const itemBytes = elementBytes * itemSize; 2709 | const byteOffset = accessorDef.byteOffset || 0; 2710 | const byteStride = accessorDef.bufferView !== undefined ? json.bufferViews[ accessorDef.bufferView ].byteStride : undefined; 2711 | const normalized = accessorDef.normalized === true; 2712 | let array, bufferAttribute; 2713 | 2714 | // The buffer is not interleaved if the stride is the item size in bytes. 2715 | if ( byteStride && byteStride !== itemBytes ) { 2716 | 2717 | // Each "slice" of the buffer, as defined by 'count' elements of 'byteStride' bytes, gets its own InterleavedBuffer 2718 | // This makes sure that IBA.count reflects accessor.count properly 2719 | const ibSlice = Math.floor( byteOffset / byteStride ); 2720 | const ibCacheKey = 'InterleavedBuffer:' + accessorDef.bufferView + ':' + accessorDef.componentType + ':' + ibSlice + ':' + accessorDef.count; 2721 | let ib = parser.cache.get( ibCacheKey ); 2722 | 2723 | if ( ! ib ) { 2724 | 2725 | array = new TypedArray( bufferView, ibSlice * byteStride, accessorDef.count * byteStride / elementBytes ); 2726 | 2727 | // Integer parameters to IB/IBA are in array elements, not bytes. 2728 | ib = new InterleavedBuffer( array, byteStride / elementBytes ); 2729 | 2730 | parser.cache.add( ibCacheKey, ib ); 2731 | 2732 | } 2733 | 2734 | bufferAttribute = new InterleavedBufferAttribute( ib, itemSize, ( byteOffset % byteStride ) / elementBytes, normalized ); 2735 | 2736 | } else { 2737 | 2738 | if ( bufferView === null ) { 2739 | 2740 | array = new TypedArray( accessorDef.count * itemSize ); 2741 | 2742 | } else { 2743 | 2744 | array = new TypedArray( bufferView, byteOffset, accessorDef.count * itemSize ); 2745 | 2746 | } 2747 | 2748 | bufferAttribute = new BufferAttribute( array, itemSize, normalized ); 2749 | 2750 | } 2751 | 2752 | // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#sparse-accessors 2753 | if ( accessorDef.sparse !== undefined ) { 2754 | 2755 | const itemSizeIndices = WEBGL_TYPE_SIZES.SCALAR; 2756 | const TypedArrayIndices = WEBGL_COMPONENT_TYPES[ accessorDef.sparse.indices.componentType ]; 2757 | 2758 | const byteOffsetIndices = accessorDef.sparse.indices.byteOffset || 0; 2759 | const byteOffsetValues = accessorDef.sparse.values.byteOffset || 0; 2760 | 2761 | const sparseIndices = new TypedArrayIndices( bufferViews[ 1 ], byteOffsetIndices, accessorDef.sparse.count * itemSizeIndices ); 2762 | const sparseValues = new TypedArray( bufferViews[ 2 ], byteOffsetValues, accessorDef.sparse.count * itemSize ); 2763 | 2764 | if ( bufferView !== null ) { 2765 | 2766 | // Avoid modifying the original ArrayBuffer, if the bufferView wasn't initialized with zeroes. 2767 | bufferAttribute = new BufferAttribute( bufferAttribute.array.slice(), bufferAttribute.itemSize, bufferAttribute.normalized ); 2768 | 2769 | } 2770 | 2771 | for ( let i = 0, il = sparseIndices.length; i < il; i ++ ) { 2772 | 2773 | const index = sparseIndices[ i ]; 2774 | 2775 | bufferAttribute.setX( index, sparseValues[ i * itemSize ] ); 2776 | if ( itemSize >= 2 ) bufferAttribute.setY( index, sparseValues[ i * itemSize + 1 ] ); 2777 | if ( itemSize >= 3 ) bufferAttribute.setZ( index, sparseValues[ i * itemSize + 2 ] ); 2778 | if ( itemSize >= 4 ) bufferAttribute.setW( index, sparseValues[ i * itemSize + 3 ] ); 2779 | if ( itemSize >= 5 ) throw new Error( 'THREE.GLTFLoader: Unsupported itemSize in sparse BufferAttribute.' ); 2780 | 2781 | } 2782 | 2783 | } 2784 | 2785 | return bufferAttribute; 2786 | 2787 | } ); 2788 | 2789 | } 2790 | 2791 | /** 2792 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#textures 2793 | * @param {number} textureIndex 2794 | * @return {Promise} 2795 | */ 2796 | loadTexture( textureIndex ) { 2797 | 2798 | const json = this.json; 2799 | const options = this.options; 2800 | const textureDef = json.textures[ textureIndex ]; 2801 | const source = json.images[ textureDef.source ]; 2802 | 2803 | let loader = this.textureLoader; 2804 | 2805 | if ( source.uri ) { 2806 | 2807 | const handler = options.manager.getHandler( source.uri ); 2808 | if ( handler !== null ) loader = handler; 2809 | 2810 | } 2811 | 2812 | return this.loadTextureImage( textureIndex, source, loader ); 2813 | 2814 | } 2815 | 2816 | loadTextureImage( textureIndex, source, loader ) { 2817 | 2818 | const parser = this; 2819 | const json = this.json; 2820 | const options = this.options; 2821 | 2822 | const textureDef = json.textures[ textureIndex ]; 2823 | 2824 | const cacheKey = ( source.uri || source.bufferView ) + ':' + textureDef.sampler; 2825 | 2826 | if ( this.textureCache[ cacheKey ] ) { 2827 | 2828 | // See https://github.com/mrdoob/three.js/issues/21559. 2829 | return this.textureCache[ cacheKey ]; 2830 | 2831 | } 2832 | 2833 | const URL = self.URL || self.webkitURL; 2834 | 2835 | let sourceURI = source.uri || ''; 2836 | let isObjectURL = false; 2837 | 2838 | if ( source.bufferView !== undefined ) { 2839 | 2840 | // Load binary image data from bufferView, if provided. 2841 | 2842 | sourceURI = parser.getDependency( 'bufferView', source.bufferView ).then( function ( bufferView ) { 2843 | 2844 | isObjectURL = true; 2845 | const blob = new Blob( [ bufferView ], { type: source.mimeType } ); 2846 | sourceURI = URL.createObjectURL( blob ); 2847 | return sourceURI; 2848 | 2849 | } ); 2850 | 2851 | } else if ( source.uri === undefined ) { 2852 | 2853 | throw new Error( 'THREE.GLTFLoader: Image ' + textureIndex + ' is missing URI and bufferView' ); 2854 | 2855 | } 2856 | 2857 | const promise = Promise.resolve( sourceURI ).then( function ( sourceURI ) { 2858 | 2859 | return new Promise( function ( resolve, reject ) { 2860 | 2861 | let onLoad = resolve; 2862 | 2863 | if ( loader.isImageBitmapLoader === true ) { 2864 | 2865 | onLoad = function ( imageBitmap ) { 2866 | 2867 | const texture = new Texture( imageBitmap ); 2868 | texture.needsUpdate = true; 2869 | 2870 | resolve( texture ); 2871 | 2872 | }; 2873 | 2874 | } 2875 | 2876 | loader.load( LoaderUtils.resolveURL( sourceURI, options.path ), onLoad, undefined, reject ); 2877 | 2878 | } ); 2879 | 2880 | } ).then( function ( texture ) { 2881 | 2882 | // Clean up resources and configure Texture. 2883 | 2884 | if ( isObjectURL === true ) { 2885 | 2886 | URL.revokeObjectURL( sourceURI ); 2887 | 2888 | } 2889 | 2890 | texture.flipY = false; 2891 | 2892 | if ( textureDef.name ) texture.name = textureDef.name; 2893 | 2894 | const samplers = json.samplers || {}; 2895 | const sampler = samplers[ textureDef.sampler ] || {}; 2896 | 2897 | texture.magFilter = WEBGL_FILTERS[ sampler.magFilter ] || LinearFilter; 2898 | texture.minFilter = WEBGL_FILTERS[ sampler.minFilter ] || LinearMipmapLinearFilter; 2899 | texture.wrapS = WEBGL_WRAPPINGS[ sampler.wrapS ] || RepeatWrapping; 2900 | texture.wrapT = WEBGL_WRAPPINGS[ sampler.wrapT ] || RepeatWrapping; 2901 | 2902 | parser.associations.set( texture, { textures: textureIndex } ); 2903 | 2904 | return texture; 2905 | 2906 | } ).catch( function () { 2907 | 2908 | console.error( 'THREE.GLTFLoader: Couldn\'t load texture', sourceURI ); 2909 | return null; 2910 | 2911 | } ); 2912 | 2913 | this.textureCache[ cacheKey ] = promise; 2914 | 2915 | return promise; 2916 | 2917 | } 2918 | 2919 | /** 2920 | * Asynchronously assigns a texture to the given material parameters. 2921 | * @param {Object} materialParams 2922 | * @param {string} mapName 2923 | * @param {Object} mapDef 2924 | * @return {Promise} 2925 | */ 2926 | assignTexture( materialParams, mapName, mapDef ) { 2927 | 2928 | const parser = this; 2929 | 2930 | return this.getDependency( 'texture', mapDef.index ).then( function ( texture ) { 2931 | 2932 | // Materials sample aoMap from UV set 1 and other maps from UV set 0 - this can't be configured 2933 | // However, we will copy UV set 0 to UV set 1 on demand for aoMap 2934 | if ( mapDef.texCoord !== undefined && mapDef.texCoord != 0 && ! ( mapName === 'aoMap' && mapDef.texCoord == 1 ) ) { 2935 | 2936 | console.warn( 'THREE.GLTFLoader: Custom UV set ' + mapDef.texCoord + ' for texture ' + mapName + ' not yet supported.' ); 2937 | 2938 | } 2939 | 2940 | if ( parser.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ] ) { 2941 | 2942 | const transform = mapDef.extensions !== undefined ? mapDef.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ] : undefined; 2943 | 2944 | if ( transform ) { 2945 | 2946 | const gltfReference = parser.associations.get( texture ); 2947 | texture = parser.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ].extendTexture( texture, transform ); 2948 | parser.associations.set( texture, gltfReference ); 2949 | 2950 | } 2951 | 2952 | } 2953 | 2954 | materialParams[ mapName ] = texture; 2955 | 2956 | return texture; 2957 | 2958 | } ); 2959 | 2960 | } 2961 | 2962 | /** 2963 | * Assigns final material to a Mesh, Line, or Points instance. The instance 2964 | * already has a material (generated from the glTF material options alone) 2965 | * but reuse of the same glTF material may require multiple threejs materials 2966 | * to accommodate different primitive types, defines, etc. New materials will 2967 | * be created if necessary, and reused from a cache. 2968 | * @param {Object3D} mesh Mesh, Line, or Points instance. 2969 | */ 2970 | assignFinalMaterial( mesh ) { 2971 | 2972 | const geometry = mesh.geometry; 2973 | let material = mesh.material; 2974 | 2975 | const useDerivativeTangents = geometry.attributes.tangent === undefined; 2976 | const useVertexColors = geometry.attributes.color !== undefined; 2977 | const useFlatShading = geometry.attributes.normal === undefined; 2978 | 2979 | if ( mesh.isPoints ) { 2980 | 2981 | const cacheKey = 'PointsMaterial:' + material.uuid; 2982 | 2983 | let pointsMaterial = this.cache.get( cacheKey ); 2984 | 2985 | if ( ! pointsMaterial ) { 2986 | 2987 | pointsMaterial = new PointsMaterial(); 2988 | Material.prototype.copy.call( pointsMaterial, material ); 2989 | pointsMaterial.color.copy( material.color ); 2990 | pointsMaterial.map = material.map; 2991 | pointsMaterial.sizeAttenuation = false; // glTF spec says points should be 1px 2992 | 2993 | this.cache.add( cacheKey, pointsMaterial ); 2994 | 2995 | } 2996 | 2997 | material = pointsMaterial; 2998 | 2999 | } else if ( mesh.isLine ) { 3000 | 3001 | const cacheKey = 'LineBasicMaterial:' + material.uuid; 3002 | 3003 | let lineMaterial = this.cache.get( cacheKey ); 3004 | 3005 | if ( ! lineMaterial ) { 3006 | 3007 | lineMaterial = new LineBasicMaterial(); 3008 | Material.prototype.copy.call( lineMaterial, material ); 3009 | lineMaterial.color.copy( material.color ); 3010 | 3011 | this.cache.add( cacheKey, lineMaterial ); 3012 | 3013 | } 3014 | 3015 | material = lineMaterial; 3016 | 3017 | } 3018 | 3019 | // Clone the material if it will be modified 3020 | if ( useDerivativeTangents || useVertexColors || useFlatShading ) { 3021 | 3022 | let cacheKey = 'ClonedMaterial:' + material.uuid + ':'; 3023 | 3024 | if ( material.isGLTFSpecularGlossinessMaterial ) cacheKey += 'specular-glossiness:'; 3025 | if ( useDerivativeTangents ) cacheKey += 'derivative-tangents:'; 3026 | if ( useVertexColors ) cacheKey += 'vertex-colors:'; 3027 | if ( useFlatShading ) cacheKey += 'flat-shading:'; 3028 | 3029 | let cachedMaterial = this.cache.get( cacheKey ); 3030 | 3031 | if ( ! cachedMaterial ) { 3032 | 3033 | cachedMaterial = material.clone(); 3034 | 3035 | if ( useVertexColors ) cachedMaterial.vertexColors = true; 3036 | if ( useFlatShading ) cachedMaterial.flatShading = true; 3037 | 3038 | if ( useDerivativeTangents ) { 3039 | 3040 | // https://github.com/mrdoob/three.js/issues/11438#issuecomment-507003995 3041 | if ( cachedMaterial.normalScale ) cachedMaterial.normalScale.y *= - 1; 3042 | if ( cachedMaterial.clearcoatNormalScale ) cachedMaterial.clearcoatNormalScale.y *= - 1; 3043 | 3044 | } 3045 | 3046 | this.cache.add( cacheKey, cachedMaterial ); 3047 | 3048 | this.associations.set( cachedMaterial, this.associations.get( material ) ); 3049 | 3050 | } 3051 | 3052 | material = cachedMaterial; 3053 | 3054 | } 3055 | 3056 | // workarounds for mesh and geometry 3057 | 3058 | if ( material.aoMap && geometry.attributes.uv2 === undefined && geometry.attributes.uv !== undefined ) { 3059 | 3060 | geometry.setAttribute( 'uv2', geometry.attributes.uv ); 3061 | 3062 | } 3063 | 3064 | mesh.material = material; 3065 | 3066 | } 3067 | 3068 | getMaterialType( /* materialIndex */ ) { 3069 | 3070 | return MeshStandardMaterial; 3071 | 3072 | } 3073 | 3074 | /** 3075 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#materials 3076 | * @param {number} materialIndex 3077 | * @return {Promise} 3078 | */ 3079 | loadMaterial( materialIndex ) { 3080 | 3081 | const parser = this; 3082 | const json = this.json; 3083 | const extensions = this.extensions; 3084 | const materialDef = json.materials[ materialIndex ]; 3085 | 3086 | let materialType; 3087 | const materialParams = {}; 3088 | const materialExtensions = materialDef.extensions || {}; 3089 | 3090 | const pending = []; 3091 | 3092 | if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ] ) { 3093 | 3094 | const sgExtension = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ]; 3095 | materialType = sgExtension.getMaterialType(); 3096 | pending.push( sgExtension.extendParams( materialParams, materialDef, parser ) ); 3097 | 3098 | } else if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ] ) { 3099 | 3100 | const kmuExtension = extensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ]; 3101 | materialType = kmuExtension.getMaterialType(); 3102 | pending.push( kmuExtension.extendParams( materialParams, materialDef, parser ) ); 3103 | 3104 | } else { 3105 | 3106 | // Specification: 3107 | // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-material 3108 | 3109 | const metallicRoughness = materialDef.pbrMetallicRoughness || {}; 3110 | 3111 | materialParams.color = new Color( 1.0, 1.0, 1.0 ); 3112 | materialParams.opacity = 1.0; 3113 | 3114 | if ( Array.isArray( metallicRoughness.baseColorFactor ) ) { 3115 | 3116 | const array = metallicRoughness.baseColorFactor; 3117 | 3118 | materialParams.color.fromArray( array ); 3119 | materialParams.opacity = array[ 3 ]; 3120 | 3121 | } 3122 | 3123 | if ( metallicRoughness.baseColorTexture !== undefined ) { 3124 | 3125 | pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture ) ); 3126 | 3127 | } 3128 | 3129 | materialParams.metalness = metallicRoughness.metallicFactor !== undefined ? metallicRoughness.metallicFactor : 1.0; 3130 | materialParams.roughness = metallicRoughness.roughnessFactor !== undefined ? metallicRoughness.roughnessFactor : 1.0; 3131 | 3132 | if ( metallicRoughness.metallicRoughnessTexture !== undefined ) { 3133 | 3134 | pending.push( parser.assignTexture( materialParams, 'metalnessMap', metallicRoughness.metallicRoughnessTexture ) ); 3135 | pending.push( parser.assignTexture( materialParams, 'roughnessMap', metallicRoughness.metallicRoughnessTexture ) ); 3136 | 3137 | } 3138 | 3139 | materialType = this._invokeOne( function ( ext ) { 3140 | 3141 | return ext.getMaterialType && ext.getMaterialType( materialIndex ); 3142 | 3143 | } ); 3144 | 3145 | pending.push( Promise.all( this._invokeAll( function ( ext ) { 3146 | 3147 | return ext.extendMaterialParams && ext.extendMaterialParams( materialIndex, materialParams ); 3148 | 3149 | } ) ) ); 3150 | 3151 | } 3152 | 3153 | if ( materialDef.doubleSided === true ) { 3154 | 3155 | materialParams.side = DoubleSide; 3156 | 3157 | } 3158 | 3159 | const alphaMode = materialDef.alphaMode || ALPHA_MODES.OPAQUE; 3160 | 3161 | if ( alphaMode === ALPHA_MODES.BLEND ) { 3162 | 3163 | materialParams.transparent = true; 3164 | 3165 | // See: https://github.com/mrdoob/three.js/issues/17706 3166 | materialParams.depthWrite = false; 3167 | 3168 | } else { 3169 | 3170 | materialParams.format = RGBFormat; 3171 | materialParams.transparent = false; 3172 | 3173 | if ( alphaMode === ALPHA_MODES.MASK ) { 3174 | 3175 | materialParams.alphaTest = materialDef.alphaCutoff !== undefined ? materialDef.alphaCutoff : 0.5; 3176 | 3177 | } 3178 | 3179 | } 3180 | 3181 | if ( materialDef.normalTexture !== undefined && materialType !== MeshBasicMaterial ) { 3182 | 3183 | pending.push( parser.assignTexture( materialParams, 'normalMap', materialDef.normalTexture ) ); 3184 | 3185 | materialParams.normalScale = new Vector2( 1, 1 ); 3186 | 3187 | if ( materialDef.normalTexture.scale !== undefined ) { 3188 | 3189 | const scale = materialDef.normalTexture.scale; 3190 | 3191 | materialParams.normalScale.set( scale, scale ); 3192 | 3193 | } 3194 | 3195 | } 3196 | 3197 | if ( materialDef.occlusionTexture !== undefined && materialType !== MeshBasicMaterial ) { 3198 | 3199 | pending.push( parser.assignTexture( materialParams, 'aoMap', materialDef.occlusionTexture ) ); 3200 | 3201 | if ( materialDef.occlusionTexture.strength !== undefined ) { 3202 | 3203 | materialParams.aoMapIntensity = materialDef.occlusionTexture.strength; 3204 | 3205 | } 3206 | 3207 | } 3208 | 3209 | if ( materialDef.emissiveFactor !== undefined && materialType !== MeshBasicMaterial ) { 3210 | 3211 | materialParams.emissive = new Color().fromArray( materialDef.emissiveFactor ); 3212 | 3213 | } 3214 | 3215 | if ( materialDef.emissiveTexture !== undefined && materialType !== MeshBasicMaterial ) { 3216 | 3217 | pending.push( parser.assignTexture( materialParams, 'emissiveMap', materialDef.emissiveTexture ) ); 3218 | 3219 | } 3220 | 3221 | return Promise.all( pending ).then( function () { 3222 | 3223 | let material; 3224 | 3225 | if ( materialType === GLTFMeshStandardSGMaterial ) { 3226 | 3227 | material = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].createMaterial( materialParams ); 3228 | 3229 | } else { 3230 | 3231 | material = new materialType( materialParams ); 3232 | 3233 | } 3234 | 3235 | if ( materialDef.name ) material.name = materialDef.name; 3236 | 3237 | // baseColorTexture, emissiveTexture, and specularGlossinessTexture use sRGB encoding. 3238 | if ( material.map ) material.map.encoding = sRGBEncoding; 3239 | if ( material.emissiveMap ) material.emissiveMap.encoding = sRGBEncoding; 3240 | 3241 | assignExtrasToUserData( material, materialDef ); 3242 | 3243 | parser.associations.set( material, { materials: materialIndex } ); 3244 | 3245 | if ( materialDef.extensions ) addUnknownExtensionsToUserData( extensions, material, materialDef ); 3246 | 3247 | return material; 3248 | 3249 | } ); 3250 | 3251 | } 3252 | 3253 | /** When Object3D instances are targeted by animation, they need unique names. */ 3254 | createUniqueName( originalName ) { 3255 | 3256 | const sanitizedName = PropertyBinding.sanitizeNodeName( originalName || '' ); 3257 | 3258 | let name = sanitizedName; 3259 | 3260 | for ( let i = 1; this.nodeNamesUsed[ name ]; ++ i ) { 3261 | 3262 | name = sanitizedName + '_' + i; 3263 | 3264 | } 3265 | 3266 | this.nodeNamesUsed[ name ] = true; 3267 | 3268 | return name; 3269 | 3270 | } 3271 | 3272 | /** 3273 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#geometry 3274 | * 3275 | * Creates BufferGeometries from primitives. 3276 | * 3277 | * @param {Array} primitives 3278 | * @return {Promise>} 3279 | */ 3280 | loadGeometries( primitives ) { 3281 | 3282 | const parser = this; 3283 | const extensions = this.extensions; 3284 | const cache = this.primitiveCache; 3285 | 3286 | function createDracoPrimitive( primitive ) { 3287 | 3288 | return extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ] 3289 | .decodePrimitive( primitive, parser ) 3290 | .then( function ( geometry ) { 3291 | 3292 | return addPrimitiveAttributes( geometry, primitive, parser ); 3293 | 3294 | } ); 3295 | 3296 | } 3297 | 3298 | const pending = []; 3299 | 3300 | for ( let i = 0, il = primitives.length; i < il; i ++ ) { 3301 | 3302 | const primitive = primitives[ i ]; 3303 | const cacheKey = createPrimitiveKey( primitive ); 3304 | 3305 | // See if we've already created this geometry 3306 | const cached = cache[ cacheKey ]; 3307 | 3308 | if ( cached ) { 3309 | 3310 | // Use the cached geometry if it exists 3311 | pending.push( cached.promise ); 3312 | 3313 | } else { 3314 | 3315 | let geometryPromise; 3316 | 3317 | if ( primitive.extensions && primitive.extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ] ) { 3318 | 3319 | // Use DRACO geometry if available 3320 | geometryPromise = createDracoPrimitive( primitive ); 3321 | 3322 | } else { 3323 | 3324 | // Otherwise create a new geometry 3325 | geometryPromise = addPrimitiveAttributes( new BufferGeometry(), primitive, parser ); 3326 | 3327 | } 3328 | 3329 | // Cache this geometry 3330 | cache[ cacheKey ] = { primitive: primitive, promise: geometryPromise }; 3331 | 3332 | pending.push( geometryPromise ); 3333 | 3334 | } 3335 | 3336 | } 3337 | 3338 | return Promise.all( pending ); 3339 | 3340 | } 3341 | 3342 | /** 3343 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#meshes 3344 | * @param {number} meshIndex 3345 | * @return {Promise} 3346 | */ 3347 | loadMesh( meshIndex ) { 3348 | 3349 | const parser = this; 3350 | const json = this.json; 3351 | const extensions = this.extensions; 3352 | 3353 | const meshDef = json.meshes[ meshIndex ]; 3354 | const primitives = meshDef.primitives; 3355 | 3356 | const pending = []; 3357 | 3358 | for ( let i = 0, il = primitives.length; i < il; i ++ ) { 3359 | 3360 | const material = primitives[ i ].material === undefined 3361 | ? createDefaultMaterial( this.cache ) 3362 | : this.getDependency( 'material', primitives[ i ].material ); 3363 | 3364 | pending.push( material ); 3365 | 3366 | } 3367 | 3368 | pending.push( parser.loadGeometries( primitives ) ); 3369 | 3370 | return Promise.all( pending ).then( function ( results ) { 3371 | 3372 | const materials = results.slice( 0, results.length - 1 ); 3373 | const geometries = results[ results.length - 1 ]; 3374 | 3375 | const meshes = []; 3376 | 3377 | for ( let i = 0, il = geometries.length; i < il; i ++ ) { 3378 | 3379 | const geometry = geometries[ i ]; 3380 | const primitive = primitives[ i ]; 3381 | 3382 | // 1. create Mesh 3383 | 3384 | let mesh; 3385 | 3386 | const material = materials[ i ]; 3387 | 3388 | if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLES || 3389 | primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP || 3390 | primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN || 3391 | primitive.mode === undefined ) { 3392 | 3393 | // .isSkinnedMesh isn't in glTF spec. See ._markDefs() 3394 | mesh = meshDef.isSkinnedMesh === true 3395 | ? new SkinnedMesh( geometry, material ) 3396 | : new Mesh( geometry, material ); 3397 | 3398 | if ( mesh.isSkinnedMesh === true && ! mesh.geometry.attributes.skinWeight.normalized ) { 3399 | 3400 | // we normalize floating point skin weight array to fix malformed assets (see #15319) 3401 | // it's important to skip this for non-float32 data since normalizeSkinWeights assumes non-normalized inputs 3402 | mesh.normalizeSkinWeights(); 3403 | 3404 | } 3405 | 3406 | if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ) { 3407 | 3408 | mesh.geometry = toTrianglesDrawMode( mesh.geometry, TriangleStripDrawMode ); 3409 | 3410 | } else if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN ) { 3411 | 3412 | mesh.geometry = toTrianglesDrawMode( mesh.geometry, TriangleFanDrawMode ); 3413 | 3414 | } 3415 | 3416 | } else if ( primitive.mode === WEBGL_CONSTANTS.LINES ) { 3417 | 3418 | mesh = new LineSegments( geometry, material ); 3419 | 3420 | } else if ( primitive.mode === WEBGL_CONSTANTS.LINE_STRIP ) { 3421 | 3422 | mesh = new Line( geometry, material ); 3423 | 3424 | } else if ( primitive.mode === WEBGL_CONSTANTS.LINE_LOOP ) { 3425 | 3426 | mesh = new LineLoop( geometry, material ); 3427 | 3428 | } else if ( primitive.mode === WEBGL_CONSTANTS.POINTS ) { 3429 | 3430 | mesh = new Points( geometry, material ); 3431 | 3432 | } else { 3433 | 3434 | throw new Error( 'THREE.GLTFLoader: Primitive mode unsupported: ' + primitive.mode ); 3435 | 3436 | } 3437 | 3438 | if ( Object.keys( mesh.geometry.morphAttributes ).length > 0 ) { 3439 | 3440 | updateMorphTargets( mesh, meshDef ); 3441 | 3442 | } 3443 | 3444 | mesh.name = parser.createUniqueName( meshDef.name || ( 'mesh_' + meshIndex ) ); 3445 | 3446 | assignExtrasToUserData( mesh, meshDef ); 3447 | 3448 | if ( primitive.extensions ) addUnknownExtensionsToUserData( extensions, mesh, primitive ); 3449 | 3450 | parser.assignFinalMaterial( mesh ); 3451 | 3452 | meshes.push( mesh ); 3453 | 3454 | } 3455 | 3456 | for ( let i = 0, il = meshes.length; i < il; i ++ ) { 3457 | 3458 | parser.associations.set( meshes[ i ], { 3459 | meshes: meshIndex, 3460 | primitives: i 3461 | } ); 3462 | 3463 | } 3464 | 3465 | if ( meshes.length === 1 ) { 3466 | 3467 | return meshes[ 0 ]; 3468 | 3469 | } 3470 | 3471 | const group = new Group(); 3472 | 3473 | parser.associations.set( group, { meshes: meshIndex } ); 3474 | 3475 | for ( let i = 0, il = meshes.length; i < il; i ++ ) { 3476 | 3477 | group.add( meshes[ i ] ); 3478 | 3479 | } 3480 | 3481 | return group; 3482 | 3483 | } ); 3484 | 3485 | } 3486 | 3487 | /** 3488 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#cameras 3489 | * @param {number} cameraIndex 3490 | * @return {Promise} 3491 | */ 3492 | loadCamera( cameraIndex ) { 3493 | 3494 | let camera; 3495 | const cameraDef = this.json.cameras[ cameraIndex ]; 3496 | const params = cameraDef[ cameraDef.type ]; 3497 | 3498 | if ( ! params ) { 3499 | 3500 | console.warn( 'THREE.GLTFLoader: Missing camera parameters.' ); 3501 | return; 3502 | 3503 | } 3504 | 3505 | if ( cameraDef.type === 'perspective' ) { 3506 | 3507 | camera = new PerspectiveCamera( MathUtils.radToDeg( params.yfov ), params.aspectRatio || 1, params.znear || 1, params.zfar || 2e6 ); 3508 | 3509 | } else if ( cameraDef.type === 'orthographic' ) { 3510 | 3511 | camera = new OrthographicCamera( - params.xmag, params.xmag, params.ymag, - params.ymag, params.znear, params.zfar ); 3512 | 3513 | } 3514 | 3515 | if ( cameraDef.name ) camera.name = this.createUniqueName( cameraDef.name ); 3516 | 3517 | assignExtrasToUserData( camera, cameraDef ); 3518 | 3519 | return Promise.resolve( camera ); 3520 | 3521 | } 3522 | 3523 | /** 3524 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#skins 3525 | * @param {number} skinIndex 3526 | * @return {Promise} 3527 | */ 3528 | loadSkin( skinIndex ) { 3529 | 3530 | const skinDef = this.json.skins[ skinIndex ]; 3531 | 3532 | const skinEntry = { joints: skinDef.joints }; 3533 | 3534 | if ( skinDef.inverseBindMatrices === undefined ) { 3535 | 3536 | return Promise.resolve( skinEntry ); 3537 | 3538 | } 3539 | 3540 | return this.getDependency( 'accessor', skinDef.inverseBindMatrices ).then( function ( accessor ) { 3541 | 3542 | skinEntry.inverseBindMatrices = accessor; 3543 | 3544 | return skinEntry; 3545 | 3546 | } ); 3547 | 3548 | } 3549 | 3550 | /** 3551 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#animations 3552 | * @param {number} animationIndex 3553 | * @return {Promise} 3554 | */ 3555 | loadAnimation( animationIndex ) { 3556 | 3557 | const json = this.json; 3558 | 3559 | const animationDef = json.animations[ animationIndex ]; 3560 | 3561 | const pendingNodes = []; 3562 | const pendingInputAccessors = []; 3563 | const pendingOutputAccessors = []; 3564 | const pendingSamplers = []; 3565 | const pendingTargets = []; 3566 | 3567 | for ( let i = 0, il = animationDef.channels.length; i < il; i ++ ) { 3568 | 3569 | const channel = animationDef.channels[ i ]; 3570 | const sampler = animationDef.samplers[ channel.sampler ]; 3571 | const target = channel.target; 3572 | const name = target.node !== undefined ? target.node : target.id; // NOTE: target.id is deprecated. 3573 | const input = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.input ] : sampler.input; 3574 | const output = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.output ] : sampler.output; 3575 | 3576 | pendingNodes.push( this.getDependency( 'node', name ) ); 3577 | pendingInputAccessors.push( this.getDependency( 'accessor', input ) ); 3578 | pendingOutputAccessors.push( this.getDependency( 'accessor', output ) ); 3579 | pendingSamplers.push( sampler ); 3580 | pendingTargets.push( target ); 3581 | 3582 | } 3583 | 3584 | return Promise.all( [ 3585 | 3586 | Promise.all( pendingNodes ), 3587 | Promise.all( pendingInputAccessors ), 3588 | Promise.all( pendingOutputAccessors ), 3589 | Promise.all( pendingSamplers ), 3590 | Promise.all( pendingTargets ) 3591 | 3592 | ] ).then( function ( dependencies ) { 3593 | 3594 | const nodes = dependencies[ 0 ]; 3595 | const inputAccessors = dependencies[ 1 ]; 3596 | const outputAccessors = dependencies[ 2 ]; 3597 | const samplers = dependencies[ 3 ]; 3598 | const targets = dependencies[ 4 ]; 3599 | 3600 | const tracks = []; 3601 | 3602 | for ( let i = 0, il = nodes.length; i < il; i ++ ) { 3603 | 3604 | const node = nodes[ i ]; 3605 | const inputAccessor = inputAccessors[ i ]; 3606 | const outputAccessor = outputAccessors[ i ]; 3607 | const sampler = samplers[ i ]; 3608 | const target = targets[ i ]; 3609 | 3610 | if ( node === undefined ) continue; 3611 | 3612 | node.updateMatrix(); 3613 | node.matrixAutoUpdate = true; 3614 | 3615 | let TypedKeyframeTrack; 3616 | 3617 | switch ( PATH_PROPERTIES[ target.path ] ) { 3618 | 3619 | case PATH_PROPERTIES.weights: 3620 | 3621 | TypedKeyframeTrack = NumberKeyframeTrack; 3622 | break; 3623 | 3624 | case PATH_PROPERTIES.rotation: 3625 | 3626 | TypedKeyframeTrack = QuaternionKeyframeTrack; 3627 | break; 3628 | 3629 | case PATH_PROPERTIES.position: 3630 | case PATH_PROPERTIES.scale: 3631 | default: 3632 | 3633 | TypedKeyframeTrack = VectorKeyframeTrack; 3634 | break; 3635 | 3636 | } 3637 | 3638 | const targetName = node.name ? node.name : node.uuid; 3639 | 3640 | const interpolation = sampler.interpolation !== undefined ? INTERPOLATION[ sampler.interpolation ] : InterpolateLinear; 3641 | 3642 | const targetNames = []; 3643 | 3644 | if ( PATH_PROPERTIES[ target.path ] === PATH_PROPERTIES.weights ) { 3645 | 3646 | // Node may be a Group (glTF mesh with several primitives) or a Mesh. 3647 | node.traverse( function ( object ) { 3648 | 3649 | if ( object.isMesh === true && object.morphTargetInfluences ) { 3650 | 3651 | targetNames.push( object.name ? object.name : object.uuid ); 3652 | 3653 | } 3654 | 3655 | } ); 3656 | 3657 | } else { 3658 | 3659 | targetNames.push( targetName ); 3660 | 3661 | } 3662 | 3663 | let outputArray = outputAccessor.array; 3664 | 3665 | if ( outputAccessor.normalized ) { 3666 | 3667 | const scale = getNormalizedComponentScale( outputArray.constructor ); 3668 | const scaled = new Float32Array( outputArray.length ); 3669 | 3670 | for ( let j = 0, jl = outputArray.length; j < jl; j ++ ) { 3671 | 3672 | scaled[ j ] = outputArray[ j ] * scale; 3673 | 3674 | } 3675 | 3676 | outputArray = scaled; 3677 | 3678 | } 3679 | 3680 | for ( let j = 0, jl = targetNames.length; j < jl; j ++ ) { 3681 | 3682 | const track = new TypedKeyframeTrack( 3683 | targetNames[ j ] + '.' + PATH_PROPERTIES[ target.path ], 3684 | inputAccessor.array, 3685 | outputArray, 3686 | interpolation 3687 | ); 3688 | 3689 | // Override interpolation with custom factory method. 3690 | if ( sampler.interpolation === 'CUBICSPLINE' ) { 3691 | 3692 | track.createInterpolant = function InterpolantFactoryMethodGLTFCubicSpline( result ) { 3693 | 3694 | // A CUBICSPLINE keyframe in glTF has three output values for each input value, 3695 | // representing inTangent, splineVertex, and outTangent. As a result, track.getValueSize() 3696 | // must be divided by three to get the interpolant's sampleSize argument. 3697 | 3698 | const interpolantType = ( this instanceof QuaternionKeyframeTrack ) ? GLTFCubicSplineQuaternionInterpolant : GLTFCubicSplineInterpolant; 3699 | 3700 | return new interpolantType( this.times, this.values, this.getValueSize() / 3, result ); 3701 | 3702 | }; 3703 | 3704 | // Mark as CUBICSPLINE. `track.getInterpolation()` doesn't support custom interpolants. 3705 | track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline = true; 3706 | 3707 | } 3708 | 3709 | tracks.push( track ); 3710 | 3711 | } 3712 | 3713 | } 3714 | 3715 | const name = animationDef.name ? animationDef.name : 'animation_' + animationIndex; 3716 | 3717 | return new AnimationClip( name, undefined, tracks ); 3718 | 3719 | } ); 3720 | 3721 | } 3722 | 3723 | createNodeMesh( nodeIndex ) { 3724 | 3725 | const json = this.json; 3726 | const parser = this; 3727 | const nodeDef = json.nodes[ nodeIndex ]; 3728 | 3729 | if ( nodeDef.mesh === undefined ) return null; 3730 | 3731 | return parser.getDependency( 'mesh', nodeDef.mesh ).then( function ( mesh ) { 3732 | 3733 | const node = parser._getNodeRef( parser.meshCache, nodeDef.mesh, mesh ); 3734 | 3735 | // if weights are provided on the node, override weights on the mesh. 3736 | if ( nodeDef.weights !== undefined ) { 3737 | 3738 | node.traverse( function ( o ) { 3739 | 3740 | if ( ! o.isMesh ) return; 3741 | 3742 | for ( let i = 0, il = nodeDef.weights.length; i < il; i ++ ) { 3743 | 3744 | o.morphTargetInfluences[ i ] = nodeDef.weights[ i ]; 3745 | 3746 | } 3747 | 3748 | } ); 3749 | 3750 | } 3751 | 3752 | return node; 3753 | 3754 | } ); 3755 | 3756 | } 3757 | 3758 | /** 3759 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodes-and-hierarchy 3760 | * @param {number} nodeIndex 3761 | * @return {Promise} 3762 | */ 3763 | loadNode( nodeIndex ) { 3764 | 3765 | const json = this.json; 3766 | const extensions = this.extensions; 3767 | const parser = this; 3768 | 3769 | const nodeDef = json.nodes[ nodeIndex ]; 3770 | 3771 | // reserve node's name before its dependencies, so the root has the intended name. 3772 | const nodeName = nodeDef.name ? parser.createUniqueName( nodeDef.name ) : ''; 3773 | 3774 | return ( function () { 3775 | 3776 | const pending = []; 3777 | 3778 | const meshPromise = parser._invokeOne( function ( ext ) { 3779 | 3780 | return ext.createNodeMesh && ext.createNodeMesh( nodeIndex ); 3781 | 3782 | } ); 3783 | 3784 | if ( meshPromise ) { 3785 | 3786 | pending.push( meshPromise ); 3787 | 3788 | } 3789 | 3790 | if ( nodeDef.camera !== undefined ) { 3791 | 3792 | pending.push( parser.getDependency( 'camera', nodeDef.camera ).then( function ( camera ) { 3793 | 3794 | return parser._getNodeRef( parser.cameraCache, nodeDef.camera, camera ); 3795 | 3796 | } ) ); 3797 | 3798 | } 3799 | 3800 | parser._invokeAll( function ( ext ) { 3801 | 3802 | return ext.createNodeAttachment && ext.createNodeAttachment( nodeIndex ); 3803 | 3804 | } ).forEach( function ( promise ) { 3805 | 3806 | pending.push( promise ); 3807 | 3808 | } ); 3809 | 3810 | return Promise.all( pending ); 3811 | 3812 | }() ).then( function ( objects ) { 3813 | 3814 | let node; 3815 | 3816 | // .isBone isn't in glTF spec. See ._markDefs 3817 | if ( nodeDef.isBone === true ) { 3818 | 3819 | node = new Bone(); 3820 | 3821 | } else if ( objects.length > 1 ) { 3822 | 3823 | node = new Group(); 3824 | 3825 | } else if ( objects.length === 1 ) { 3826 | 3827 | node = objects[ 0 ]; 3828 | 3829 | } else { 3830 | 3831 | node = new Object3D(); 3832 | 3833 | } 3834 | 3835 | if ( node !== objects[ 0 ] ) { 3836 | 3837 | for ( let i = 0, il = objects.length; i < il; i ++ ) { 3838 | 3839 | node.add( objects[ i ] ); 3840 | 3841 | } 3842 | 3843 | } 3844 | 3845 | if ( nodeDef.name ) { 3846 | 3847 | node.userData.name = nodeDef.name; 3848 | node.name = nodeName; 3849 | 3850 | } 3851 | 3852 | assignExtrasToUserData( node, nodeDef ); 3853 | 3854 | if ( nodeDef.extensions ) addUnknownExtensionsToUserData( extensions, node, nodeDef ); 3855 | 3856 | if ( nodeDef.matrix !== undefined ) { 3857 | 3858 | const matrix = new Matrix4(); 3859 | matrix.fromArray( nodeDef.matrix ); 3860 | node.applyMatrix4( matrix ); 3861 | 3862 | } else { 3863 | 3864 | if ( nodeDef.translation !== undefined ) { 3865 | 3866 | node.position.fromArray( nodeDef.translation ); 3867 | 3868 | } 3869 | 3870 | if ( nodeDef.rotation !== undefined ) { 3871 | 3872 | node.quaternion.fromArray( nodeDef.rotation ); 3873 | 3874 | } 3875 | 3876 | if ( nodeDef.scale !== undefined ) { 3877 | 3878 | node.scale.fromArray( nodeDef.scale ); 3879 | 3880 | } 3881 | 3882 | } 3883 | 3884 | if ( ! parser.associations.has( node ) ) { 3885 | 3886 | parser.associations.set( node, {} ); 3887 | 3888 | } 3889 | 3890 | parser.associations.get( node ).nodes = nodeIndex; 3891 | 3892 | return node; 3893 | 3894 | } ); 3895 | 3896 | } 3897 | 3898 | /** 3899 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#scenes 3900 | * @param {number} sceneIndex 3901 | * @return {Promise} 3902 | */ 3903 | loadScene( sceneIndex ) { 3904 | 3905 | const json = this.json; 3906 | const extensions = this.extensions; 3907 | const sceneDef = this.json.scenes[ sceneIndex ]; 3908 | const parser = this; 3909 | 3910 | // Loader returns Group, not Scene. 3911 | // See: https://github.com/mrdoob/three.js/issues/18342#issuecomment-578981172 3912 | const scene = new Group(); 3913 | if ( sceneDef.name ) scene.name = parser.createUniqueName( sceneDef.name ); 3914 | 3915 | assignExtrasToUserData( scene, sceneDef ); 3916 | 3917 | if ( sceneDef.extensions ) addUnknownExtensionsToUserData( extensions, scene, sceneDef ); 3918 | 3919 | const nodeIds = sceneDef.nodes || []; 3920 | 3921 | const pending = []; 3922 | 3923 | for ( let i = 0, il = nodeIds.length; i < il; i ++ ) { 3924 | 3925 | pending.push( buildNodeHierarchy( nodeIds[ i ], scene, json, parser ) ); 3926 | 3927 | } 3928 | 3929 | return Promise.all( pending ).then( function () { 3930 | 3931 | // Removes dangling associations, associations that reference a node that 3932 | // didn't make it into the scene. 3933 | const reduceAssociations = ( node ) => { 3934 | 3935 | const reducedAssociations = new Map(); 3936 | 3937 | for ( const [ key, value ] of parser.associations ) { 3938 | 3939 | if ( key instanceof Material || key instanceof Texture ) { 3940 | 3941 | reducedAssociations.set( key, value ); 3942 | 3943 | } 3944 | 3945 | } 3946 | 3947 | node.traverse( ( node ) => { 3948 | 3949 | const mappings = parser.associations.get( node ); 3950 | 3951 | if ( mappings != null ) { 3952 | 3953 | reducedAssociations.set( node, mappings ); 3954 | 3955 | } 3956 | 3957 | } ); 3958 | 3959 | return reducedAssociations; 3960 | 3961 | }; 3962 | 3963 | parser.associations = reduceAssociations( scene ); 3964 | 3965 | return scene; 3966 | 3967 | } ); 3968 | 3969 | } 3970 | 3971 | } 3972 | 3973 | function buildNodeHierarchy( nodeId, parentObject, json, parser ) { 3974 | 3975 | const nodeDef = json.nodes[ nodeId ]; 3976 | 3977 | return parser.getDependency( 'node', nodeId ).then( function ( node ) { 3978 | 3979 | if ( nodeDef.skin === undefined ) return node; 3980 | 3981 | // build skeleton here as well 3982 | 3983 | let skinEntry; 3984 | 3985 | return parser.getDependency( 'skin', nodeDef.skin ).then( function ( skin ) { 3986 | 3987 | skinEntry = skin; 3988 | 3989 | const pendingJoints = []; 3990 | 3991 | for ( let i = 0, il = skinEntry.joints.length; i < il; i ++ ) { 3992 | 3993 | pendingJoints.push( parser.getDependency( 'node', skinEntry.joints[ i ] ) ); 3994 | 3995 | } 3996 | 3997 | return Promise.all( pendingJoints ); 3998 | 3999 | } ).then( function ( jointNodes ) { 4000 | 4001 | node.traverse( function ( mesh ) { 4002 | 4003 | if ( ! mesh.isMesh ) return; 4004 | 4005 | const bones = []; 4006 | const boneInverses = []; 4007 | 4008 | for ( let j = 0, jl = jointNodes.length; j < jl; j ++ ) { 4009 | 4010 | const jointNode = jointNodes[ j ]; 4011 | 4012 | if ( jointNode ) { 4013 | 4014 | bones.push( jointNode ); 4015 | 4016 | const mat = new Matrix4(); 4017 | 4018 | if ( skinEntry.inverseBindMatrices !== undefined ) { 4019 | 4020 | mat.fromArray( skinEntry.inverseBindMatrices.array, j * 16 ); 4021 | 4022 | } 4023 | 4024 | boneInverses.push( mat ); 4025 | 4026 | } else { 4027 | 4028 | console.warn( 'THREE.GLTFLoader: Joint "%s" could not be found.', skinEntry.joints[ j ] ); 4029 | 4030 | } 4031 | 4032 | } 4033 | 4034 | mesh.bind( new Skeleton( bones, boneInverses ), mesh.matrixWorld ); 4035 | 4036 | } ); 4037 | 4038 | return node; 4039 | 4040 | } ); 4041 | 4042 | } ).then( function ( node ) { 4043 | 4044 | // build node hierachy 4045 | 4046 | parentObject.add( node ); 4047 | 4048 | const pending = []; 4049 | 4050 | if ( nodeDef.children ) { 4051 | 4052 | const children = nodeDef.children; 4053 | 4054 | for ( let i = 0, il = children.length; i < il; i ++ ) { 4055 | 4056 | const child = children[ i ]; 4057 | pending.push( buildNodeHierarchy( child, node, json, parser ) ); 4058 | 4059 | } 4060 | 4061 | } 4062 | 4063 | return Promise.all( pending ); 4064 | 4065 | } ); 4066 | 4067 | } 4068 | 4069 | /** 4070 | * @param {BufferGeometry} geometry 4071 | * @param {GLTF.Primitive} primitiveDef 4072 | * @param {GLTFParser} parser 4073 | */ 4074 | function computeBounds( geometry, primitiveDef, parser ) { 4075 | 4076 | const attributes = primitiveDef.attributes; 4077 | 4078 | const box = new Box3(); 4079 | 4080 | if ( attributes.POSITION !== undefined ) { 4081 | 4082 | const accessor = parser.json.accessors[ attributes.POSITION ]; 4083 | 4084 | const min = accessor.min; 4085 | const max = accessor.max; 4086 | 4087 | // glTF requires 'min' and 'max', but VRM (which extends glTF) currently ignores that requirement. 4088 | 4089 | if ( min !== undefined && max !== undefined ) { 4090 | 4091 | box.set( 4092 | new Vector3( min[ 0 ], min[ 1 ], min[ 2 ] ), 4093 | new Vector3( max[ 0 ], max[ 1 ], max[ 2 ] ) 4094 | ); 4095 | 4096 | if ( accessor.normalized ) { 4097 | 4098 | const boxScale = getNormalizedComponentScale( WEBGL_COMPONENT_TYPES[ accessor.componentType ] ); 4099 | box.min.multiplyScalar( boxScale ); 4100 | box.max.multiplyScalar( boxScale ); 4101 | 4102 | } 4103 | 4104 | } else { 4105 | 4106 | console.warn( 'THREE.GLTFLoader: Missing min/max properties for accessor POSITION.' ); 4107 | 4108 | return; 4109 | 4110 | } 4111 | 4112 | } else { 4113 | 4114 | return; 4115 | 4116 | } 4117 | 4118 | const targets = primitiveDef.targets; 4119 | 4120 | if ( targets !== undefined ) { 4121 | 4122 | const maxDisplacement = new Vector3(); 4123 | const vector = new Vector3(); 4124 | 4125 | for ( let i = 0, il = targets.length; i < il; i ++ ) { 4126 | 4127 | const target = targets[ i ]; 4128 | 4129 | if ( target.POSITION !== undefined ) { 4130 | 4131 | const accessor = parser.json.accessors[ target.POSITION ]; 4132 | const min = accessor.min; 4133 | const max = accessor.max; 4134 | 4135 | // glTF requires 'min' and 'max', but VRM (which extends glTF) currently ignores that requirement. 4136 | 4137 | if ( min !== undefined && max !== undefined ) { 4138 | 4139 | // we need to get max of absolute components because target weight is [-1,1] 4140 | vector.setX( Math.max( Math.abs( min[ 0 ] ), Math.abs( max[ 0 ] ) ) ); 4141 | vector.setY( Math.max( Math.abs( min[ 1 ] ), Math.abs( max[ 1 ] ) ) ); 4142 | vector.setZ( Math.max( Math.abs( min[ 2 ] ), Math.abs( max[ 2 ] ) ) ); 4143 | 4144 | 4145 | if ( accessor.normalized ) { 4146 | 4147 | const boxScale = getNormalizedComponentScale( WEBGL_COMPONENT_TYPES[ accessor.componentType ] ); 4148 | vector.multiplyScalar( boxScale ); 4149 | 4150 | } 4151 | 4152 | // Note: this assumes that the sum of all weights is at most 1. This isn't quite correct - it's more conservative 4153 | // to assume that each target can have a max weight of 1. However, for some use cases - notably, when morph targets 4154 | // are used to implement key-frame animations and as such only two are active at a time - this results in very large 4155 | // boxes. So for now we make a box that's sometimes a touch too small but is hopefully mostly of reasonable size. 4156 | maxDisplacement.max( vector ); 4157 | 4158 | } else { 4159 | 4160 | console.warn( 'THREE.GLTFLoader: Missing min/max properties for accessor POSITION.' ); 4161 | 4162 | } 4163 | 4164 | } 4165 | 4166 | } 4167 | 4168 | // As per comment above this box isn't conservative, but has a reasonable size for a very large number of morph targets. 4169 | box.expandByVector( maxDisplacement ); 4170 | 4171 | } 4172 | 4173 | geometry.boundingBox = box; 4174 | 4175 | const sphere = new Sphere(); 4176 | 4177 | box.getCenter( sphere.center ); 4178 | sphere.radius = box.min.distanceTo( box.max ) / 2; 4179 | 4180 | geometry.boundingSphere = sphere; 4181 | 4182 | } 4183 | 4184 | /** 4185 | * @param {BufferGeometry} geometry 4186 | * @param {GLTF.Primitive} primitiveDef 4187 | * @param {GLTFParser} parser 4188 | * @return {Promise} 4189 | */ 4190 | function addPrimitiveAttributes( geometry, primitiveDef, parser ) { 4191 | 4192 | const attributes = primitiveDef.attributes; 4193 | 4194 | const pending = []; 4195 | 4196 | function assignAttributeAccessor( accessorIndex, attributeName ) { 4197 | 4198 | return parser.getDependency( 'accessor', accessorIndex ) 4199 | .then( function ( accessor ) { 4200 | 4201 | geometry.setAttribute( attributeName, accessor ); 4202 | 4203 | } ); 4204 | 4205 | } 4206 | 4207 | for ( const gltfAttributeName in attributes ) { 4208 | 4209 | const threeAttributeName = ATTRIBUTES[ gltfAttributeName ] || gltfAttributeName.toLowerCase(); 4210 | 4211 | // Skip attributes already provided by e.g. Draco extension. 4212 | if ( threeAttributeName in geometry.attributes ) continue; 4213 | 4214 | pending.push( assignAttributeAccessor( attributes[ gltfAttributeName ], threeAttributeName ) ); 4215 | 4216 | } 4217 | 4218 | if ( primitiveDef.indices !== undefined && ! geometry.index ) { 4219 | 4220 | const accessor = parser.getDependency( 'accessor', primitiveDef.indices ).then( function ( accessor ) { 4221 | 4222 | geometry.setIndex( accessor ); 4223 | 4224 | } ); 4225 | 4226 | pending.push( accessor ); 4227 | 4228 | } 4229 | 4230 | assignExtrasToUserData( geometry, primitiveDef ); 4231 | 4232 | computeBounds( geometry, primitiveDef, parser ); 4233 | 4234 | return Promise.all( pending ).then( function () { 4235 | 4236 | return primitiveDef.targets !== undefined 4237 | ? addMorphTargets( geometry, primitiveDef.targets, parser ) 4238 | : geometry; 4239 | 4240 | } ); 4241 | 4242 | } 4243 | 4244 | /** 4245 | * @param {BufferGeometry} geometry 4246 | * @param {Number} drawMode 4247 | * @return {BufferGeometry} 4248 | */ 4249 | function toTrianglesDrawMode( geometry, drawMode ) { 4250 | 4251 | let index = geometry.getIndex(); 4252 | 4253 | // generate index if not present 4254 | 4255 | if ( index === null ) { 4256 | 4257 | const indices = []; 4258 | 4259 | const position = geometry.getAttribute( 'position' ); 4260 | 4261 | if ( position !== undefined ) { 4262 | 4263 | for ( let i = 0; i < position.count; i ++ ) { 4264 | 4265 | indices.push( i ); 4266 | 4267 | } 4268 | 4269 | geometry.setIndex( indices ); 4270 | index = geometry.getIndex(); 4271 | 4272 | } else { 4273 | 4274 | console.error( 'THREE.GLTFLoader.toTrianglesDrawMode(): Undefined position attribute. Processing not possible.' ); 4275 | return geometry; 4276 | 4277 | } 4278 | 4279 | } 4280 | 4281 | // 4282 | 4283 | const numberOfTriangles = index.count - 2; 4284 | const newIndices = []; 4285 | 4286 | if ( drawMode === TriangleFanDrawMode ) { 4287 | 4288 | // gl.TRIANGLE_FAN 4289 | 4290 | for ( let i = 1; i <= numberOfTriangles; i ++ ) { 4291 | 4292 | newIndices.push( index.getX( 0 ) ); 4293 | newIndices.push( index.getX( i ) ); 4294 | newIndices.push( index.getX( i + 1 ) ); 4295 | 4296 | } 4297 | 4298 | } else { 4299 | 4300 | // gl.TRIANGLE_STRIP 4301 | 4302 | for ( let i = 0; i < numberOfTriangles; i ++ ) { 4303 | 4304 | if ( i % 2 === 0 ) { 4305 | 4306 | newIndices.push( index.getX( i ) ); 4307 | newIndices.push( index.getX( i + 1 ) ); 4308 | newIndices.push( index.getX( i + 2 ) ); 4309 | 4310 | 4311 | } else { 4312 | 4313 | newIndices.push( index.getX( i + 2 ) ); 4314 | newIndices.push( index.getX( i + 1 ) ); 4315 | newIndices.push( index.getX( i ) ); 4316 | 4317 | } 4318 | 4319 | } 4320 | 4321 | } 4322 | 4323 | if ( ( newIndices.length / 3 ) !== numberOfTriangles ) { 4324 | 4325 | console.error( 'THREE.GLTFLoader.toTrianglesDrawMode(): Unable to generate correct amount of triangles.' ); 4326 | 4327 | } 4328 | 4329 | // build final geometry 4330 | 4331 | const newGeometry = geometry.clone(); 4332 | newGeometry.setIndex( newIndices ); 4333 | 4334 | return newGeometry; 4335 | 4336 | } 4337 | 4338 | export { GLTFLoader }; 4339 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Soft8Soft LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /OrbitControls.js: -------------------------------------------------------------------------------- 1 | import { 2 | EventDispatcher, 3 | MOUSE, 4 | Quaternion, 5 | Spherical, 6 | TOUCH, 7 | Vector2, 8 | Vector3 9 | } from './three.module.js'; 10 | 11 | // This set of controls performs orbiting, dollying (zooming), and panning. 12 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 13 | // 14 | // Orbit - left mouse / touch: one-finger move 15 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 16 | // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move 17 | 18 | const _changeEvent = { type: 'change' }; 19 | const _startEvent = { type: 'start' }; 20 | const _endEvent = { type: 'end' }; 21 | 22 | class OrbitControls extends EventDispatcher { 23 | 24 | constructor( object, domElement ) { 25 | 26 | super(); 27 | 28 | if ( domElement === undefined ) console.warn( 'THREE.OrbitControls: The second parameter "domElement" is now mandatory.' ); 29 | if ( domElement === document ) console.error( 'THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.' ); 30 | 31 | this.object = object; 32 | this.domElement = domElement; 33 | this.domElement.style.touchAction = 'none'; // disable touch scroll 34 | 35 | // Set to false to disable this control 36 | this.enabled = true; 37 | 38 | // "target" sets the location of focus, where the object orbits around 39 | this.target = new Vector3(); 40 | 41 | // How far you can dolly in and out ( PerspectiveCamera only ) 42 | this.minDistance = 0; 43 | this.maxDistance = Infinity; 44 | 45 | // How far you can zoom in and out ( OrthographicCamera only ) 46 | this.minZoom = 0; 47 | this.maxZoom = Infinity; 48 | 49 | // How far you can orbit vertically, upper and lower limits. 50 | // Range is 0 to Math.PI radians. 51 | this.minPolarAngle = 0; // radians 52 | this.maxPolarAngle = Math.PI; // radians 53 | 54 | // How far you can orbit horizontally, upper and lower limits. 55 | // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI ) 56 | this.minAzimuthAngle = - Infinity; // radians 57 | this.maxAzimuthAngle = Infinity; // radians 58 | 59 | // Set to true to enable damping (inertia) 60 | // If damping is enabled, you must call controls.update() in your animation loop 61 | this.enableDamping = false; 62 | this.dampingFactor = 0.05; 63 | 64 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 65 | // Set to false to disable zooming 66 | this.enableZoom = true; 67 | this.zoomSpeed = 1.0; 68 | 69 | // Set to false to disable rotating 70 | this.enableRotate = true; 71 | this.rotateSpeed = 1.0; 72 | 73 | // Set to false to disable panning 74 | this.enablePan = true; 75 | this.panSpeed = 1.0; 76 | this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up 77 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 78 | 79 | // Set to true to automatically rotate around the target 80 | // If auto-rotate is enabled, you must call controls.update() in your animation loop 81 | this.autoRotate = false; 82 | this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60 83 | 84 | // The four arrow keys 85 | this.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' }; 86 | 87 | // Mouse buttons 88 | this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }; 89 | 90 | // Touch fingers 91 | this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN }; 92 | 93 | // for reset 94 | this.target0 = this.target.clone(); 95 | this.position0 = this.object.position.clone(); 96 | this.zoom0 = this.object.zoom; 97 | 98 | // the target DOM element for key events 99 | this._domElementKeyEvents = null; 100 | 101 | // 102 | // public methods 103 | // 104 | 105 | this.getPolarAngle = function () { 106 | 107 | return spherical.phi; 108 | 109 | }; 110 | 111 | this.getAzimuthalAngle = function () { 112 | 113 | return spherical.theta; 114 | 115 | }; 116 | 117 | this.getDistance = function () { 118 | 119 | return this.object.position.distanceTo( this.target ); 120 | 121 | }; 122 | 123 | this.listenToKeyEvents = function ( domElement ) { 124 | 125 | domElement.addEventListener( 'keydown', onKeyDown ); 126 | this._domElementKeyEvents = domElement; 127 | 128 | }; 129 | 130 | this.saveState = function () { 131 | 132 | scope.target0.copy( scope.target ); 133 | scope.position0.copy( scope.object.position ); 134 | scope.zoom0 = scope.object.zoom; 135 | 136 | }; 137 | 138 | this.reset = function () { 139 | 140 | scope.target.copy( scope.target0 ); 141 | scope.object.position.copy( scope.position0 ); 142 | scope.object.zoom = scope.zoom0; 143 | 144 | scope.object.updateProjectionMatrix(); 145 | scope.dispatchEvent( _changeEvent ); 146 | 147 | scope.update(); 148 | 149 | state = STATE.NONE; 150 | 151 | }; 152 | 153 | // this method is exposed, but perhaps it would be better if we can make it private... 154 | this.update = function () { 155 | 156 | const offset = new Vector3(); 157 | 158 | // so camera.up is the orbit axis 159 | const quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) ); 160 | const quatInverse = quat.clone().invert(); 161 | 162 | const lastPosition = new Vector3(); 163 | const lastQuaternion = new Quaternion(); 164 | 165 | const twoPI = 2 * Math.PI; 166 | 167 | return function update() { 168 | 169 | const position = scope.object.position; 170 | 171 | offset.copy( position ).sub( scope.target ); 172 | 173 | // rotate offset to "y-axis-is-up" space 174 | offset.applyQuaternion( quat ); 175 | 176 | // angle from z-axis around y-axis 177 | spherical.setFromVector3( offset ); 178 | 179 | if ( scope.autoRotate && state === STATE.NONE ) { 180 | 181 | rotateLeft( getAutoRotationAngle() ); 182 | 183 | } 184 | 185 | if ( scope.enableDamping ) { 186 | 187 | spherical.theta += sphericalDelta.theta * scope.dampingFactor; 188 | spherical.phi += sphericalDelta.phi * scope.dampingFactor; 189 | 190 | } else { 191 | 192 | spherical.theta += sphericalDelta.theta; 193 | spherical.phi += sphericalDelta.phi; 194 | 195 | } 196 | 197 | // restrict theta to be between desired limits 198 | 199 | let min = scope.minAzimuthAngle; 200 | let max = scope.maxAzimuthAngle; 201 | 202 | if ( isFinite( min ) && isFinite( max ) ) { 203 | 204 | if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI; 205 | 206 | if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI; 207 | 208 | if ( min <= max ) { 209 | 210 | spherical.theta = Math.max( min, Math.min( max, spherical.theta ) ); 211 | 212 | } else { 213 | 214 | spherical.theta = ( spherical.theta > ( min + max ) / 2 ) ? 215 | Math.max( min, spherical.theta ) : 216 | Math.min( max, spherical.theta ); 217 | 218 | } 219 | 220 | } 221 | 222 | // restrict phi to be between desired limits 223 | spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); 224 | 225 | spherical.makeSafe(); 226 | 227 | 228 | spherical.radius *= scale; 229 | 230 | // restrict radius to be between desired limits 231 | spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) ); 232 | 233 | // move target to panned location 234 | 235 | if ( scope.enableDamping === true ) { 236 | 237 | scope.target.addScaledVector( panOffset, scope.dampingFactor ); 238 | 239 | } else { 240 | 241 | scope.target.add( panOffset ); 242 | 243 | } 244 | 245 | offset.setFromSpherical( spherical ); 246 | 247 | // rotate offset back to "camera-up-vector-is-up" space 248 | offset.applyQuaternion( quatInverse ); 249 | 250 | position.copy( scope.target ).add( offset ); 251 | 252 | scope.object.lookAt( scope.target ); 253 | 254 | if ( scope.enableDamping === true ) { 255 | 256 | sphericalDelta.theta *= ( 1 - scope.dampingFactor ); 257 | sphericalDelta.phi *= ( 1 - scope.dampingFactor ); 258 | 259 | panOffset.multiplyScalar( 1 - scope.dampingFactor ); 260 | 261 | } else { 262 | 263 | sphericalDelta.set( 0, 0, 0 ); 264 | 265 | panOffset.set( 0, 0, 0 ); 266 | 267 | } 268 | 269 | scale = 1; 270 | 271 | // update condition is: 272 | // min(camera displacement, camera rotation in radians)^2 > EPS 273 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 274 | 275 | if ( zoomChanged || 276 | lastPosition.distanceToSquared( scope.object.position ) > EPS || 277 | 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) { 278 | 279 | scope.dispatchEvent( _changeEvent ); 280 | 281 | lastPosition.copy( scope.object.position ); 282 | lastQuaternion.copy( scope.object.quaternion ); 283 | zoomChanged = false; 284 | 285 | return true; 286 | 287 | } 288 | 289 | return false; 290 | 291 | }; 292 | 293 | }(); 294 | 295 | this.dispose = function () { 296 | 297 | scope.domElement.removeEventListener( 'contextmenu', onContextMenu ); 298 | 299 | scope.domElement.removeEventListener( 'pointerdown', onPointerDown ); 300 | scope.domElement.removeEventListener( 'pointercancel', onPointerCancel ); 301 | scope.domElement.removeEventListener( 'wheel', onMouseWheel ); 302 | 303 | scope.domElement.removeEventListener( 'pointermove', onPointerMove ); 304 | scope.domElement.removeEventListener( 'pointerup', onPointerUp ); 305 | 306 | 307 | if ( scope._domElementKeyEvents !== null ) { 308 | 309 | scope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown ); 310 | 311 | } 312 | 313 | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? 314 | 315 | }; 316 | 317 | // 318 | // internals 319 | // 320 | 321 | const scope = this; 322 | 323 | const STATE = { 324 | NONE: - 1, 325 | ROTATE: 0, 326 | DOLLY: 1, 327 | PAN: 2, 328 | TOUCH_ROTATE: 3, 329 | TOUCH_PAN: 4, 330 | TOUCH_DOLLY_PAN: 5, 331 | TOUCH_DOLLY_ROTATE: 6 332 | }; 333 | 334 | let state = STATE.NONE; 335 | 336 | const EPS = 0.000001; 337 | 338 | // current position in spherical coordinates 339 | const spherical = new Spherical(); 340 | const sphericalDelta = new Spherical(); 341 | 342 | let scale = 1; 343 | const panOffset = new Vector3(); 344 | let zoomChanged = false; 345 | 346 | const rotateStart = new Vector2(); 347 | const rotateEnd = new Vector2(); 348 | const rotateDelta = new Vector2(); 349 | 350 | const panStart = new Vector2(); 351 | const panEnd = new Vector2(); 352 | const panDelta = new Vector2(); 353 | 354 | const dollyStart = new Vector2(); 355 | const dollyEnd = new Vector2(); 356 | const dollyDelta = new Vector2(); 357 | 358 | const pointers = []; 359 | const pointerPositions = {}; 360 | 361 | function getAutoRotationAngle() { 362 | 363 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 364 | 365 | } 366 | 367 | function getZoomScale() { 368 | 369 | return Math.pow( 0.95, scope.zoomSpeed ); 370 | 371 | } 372 | 373 | function rotateLeft( angle ) { 374 | 375 | sphericalDelta.theta -= angle; 376 | 377 | } 378 | 379 | function rotateUp( angle ) { 380 | 381 | sphericalDelta.phi -= angle; 382 | 383 | } 384 | 385 | const panLeft = function () { 386 | 387 | const v = new Vector3(); 388 | 389 | return function panLeft( distance, objectMatrix ) { 390 | 391 | v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix 392 | v.multiplyScalar( - distance ); 393 | 394 | panOffset.add( v ); 395 | 396 | }; 397 | 398 | }(); 399 | 400 | const panUp = function () { 401 | 402 | const v = new Vector3(); 403 | 404 | return function panUp( distance, objectMatrix ) { 405 | 406 | if ( scope.screenSpacePanning === true ) { 407 | 408 | v.setFromMatrixColumn( objectMatrix, 1 ); 409 | 410 | } else { 411 | 412 | v.setFromMatrixColumn( objectMatrix, 0 ); 413 | v.crossVectors( scope.object.up, v ); 414 | 415 | } 416 | 417 | v.multiplyScalar( distance ); 418 | 419 | panOffset.add( v ); 420 | 421 | }; 422 | 423 | }(); 424 | 425 | // deltaX and deltaY are in pixels; right and down are positive 426 | const pan = function () { 427 | 428 | const offset = new Vector3(); 429 | 430 | return function pan( deltaX, deltaY ) { 431 | 432 | const element = scope.domElement; 433 | 434 | if ( scope.object.isPerspectiveCamera ) { 435 | 436 | // perspective 437 | const position = scope.object.position; 438 | offset.copy( position ).sub( scope.target ); 439 | let targetDistance = offset.length(); 440 | 441 | // half of the fov is center to top of screen 442 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); 443 | 444 | // we use only clientHeight here so aspect ratio does not distort speed 445 | panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); 446 | panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); 447 | 448 | } else if ( scope.object.isOrthographicCamera ) { 449 | 450 | // orthographic 451 | panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); 452 | panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); 453 | 454 | } else { 455 | 456 | // camera neither orthographic nor perspective 457 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 458 | scope.enablePan = false; 459 | 460 | } 461 | 462 | }; 463 | 464 | }(); 465 | 466 | function dollyOut( dollyScale ) { 467 | 468 | if ( scope.object.isPerspectiveCamera ) { 469 | 470 | scale /= dollyScale; 471 | 472 | } else if ( scope.object.isOrthographicCamera ) { 473 | 474 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) ); 475 | scope.object.updateProjectionMatrix(); 476 | zoomChanged = true; 477 | 478 | } else { 479 | 480 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 481 | scope.enableZoom = false; 482 | 483 | } 484 | 485 | } 486 | 487 | function dollyIn( dollyScale ) { 488 | 489 | if ( scope.object.isPerspectiveCamera ) { 490 | 491 | scale *= dollyScale; 492 | 493 | } else if ( scope.object.isOrthographicCamera ) { 494 | 495 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) ); 496 | scope.object.updateProjectionMatrix(); 497 | zoomChanged = true; 498 | 499 | } else { 500 | 501 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 502 | scope.enableZoom = false; 503 | 504 | } 505 | 506 | } 507 | 508 | // 509 | // event callbacks - update the object state 510 | // 511 | 512 | function handleMouseDownRotate( event ) { 513 | 514 | rotateStart.set( event.clientX, event.clientY ); 515 | 516 | } 517 | 518 | function handleMouseDownDolly( event ) { 519 | 520 | dollyStart.set( event.clientX, event.clientY ); 521 | 522 | } 523 | 524 | function handleMouseDownPan( event ) { 525 | 526 | panStart.set( event.clientX, event.clientY ); 527 | 528 | } 529 | 530 | function handleMouseMoveRotate( event ) { 531 | 532 | rotateEnd.set( event.clientX, event.clientY ); 533 | 534 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 535 | 536 | const element = scope.domElement; 537 | 538 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 539 | 540 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 541 | 542 | rotateStart.copy( rotateEnd ); 543 | 544 | scope.update(); 545 | 546 | } 547 | 548 | function handleMouseMoveDolly( event ) { 549 | 550 | dollyEnd.set( event.clientX, event.clientY ); 551 | 552 | dollyDelta.subVectors( dollyEnd, dollyStart ); 553 | 554 | if ( dollyDelta.y > 0 ) { 555 | 556 | dollyOut( getZoomScale() ); 557 | 558 | } else if ( dollyDelta.y < 0 ) { 559 | 560 | dollyIn( getZoomScale() ); 561 | 562 | } 563 | 564 | dollyStart.copy( dollyEnd ); 565 | 566 | scope.update(); 567 | 568 | } 569 | 570 | function handleMouseMovePan( event ) { 571 | 572 | panEnd.set( event.clientX, event.clientY ); 573 | 574 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 575 | 576 | pan( panDelta.x, panDelta.y ); 577 | 578 | panStart.copy( panEnd ); 579 | 580 | scope.update(); 581 | 582 | } 583 | 584 | function handleMouseUp( /*event*/ ) { 585 | 586 | // no-op 587 | 588 | } 589 | 590 | function handleMouseWheel( event ) { 591 | 592 | if ( event.deltaY < 0 ) { 593 | 594 | dollyIn( getZoomScale() ); 595 | 596 | } else if ( event.deltaY > 0 ) { 597 | 598 | dollyOut( getZoomScale() ); 599 | 600 | } 601 | 602 | scope.update(); 603 | 604 | } 605 | 606 | function handleKeyDown( event ) { 607 | 608 | let needsUpdate = false; 609 | 610 | switch ( event.code ) { 611 | 612 | case scope.keys.UP: 613 | pan( 0, scope.keyPanSpeed ); 614 | needsUpdate = true; 615 | break; 616 | 617 | case scope.keys.BOTTOM: 618 | pan( 0, - scope.keyPanSpeed ); 619 | needsUpdate = true; 620 | break; 621 | 622 | case scope.keys.LEFT: 623 | pan( scope.keyPanSpeed, 0 ); 624 | needsUpdate = true; 625 | break; 626 | 627 | case scope.keys.RIGHT: 628 | pan( - scope.keyPanSpeed, 0 ); 629 | needsUpdate = true; 630 | break; 631 | 632 | } 633 | 634 | if ( needsUpdate ) { 635 | 636 | // prevent the browser from scrolling on cursor keys 637 | event.preventDefault(); 638 | 639 | scope.update(); 640 | 641 | } 642 | 643 | 644 | } 645 | 646 | function handleTouchStartRotate() { 647 | 648 | if ( pointers.length === 1 ) { 649 | 650 | rotateStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY ); 651 | 652 | } else { 653 | 654 | const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX ); 655 | const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY ); 656 | 657 | rotateStart.set( x, y ); 658 | 659 | } 660 | 661 | } 662 | 663 | function handleTouchStartPan() { 664 | 665 | if ( pointers.length === 1 ) { 666 | 667 | panStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY ); 668 | 669 | } else { 670 | 671 | const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX ); 672 | const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY ); 673 | 674 | panStart.set( x, y ); 675 | 676 | } 677 | 678 | } 679 | 680 | function handleTouchStartDolly() { 681 | 682 | const dx = pointers[ 0 ].pageX - pointers[ 1 ].pageX; 683 | const dy = pointers[ 0 ].pageY - pointers[ 1 ].pageY; 684 | 685 | const distance = Math.sqrt( dx * dx + dy * dy ); 686 | 687 | dollyStart.set( 0, distance ); 688 | 689 | } 690 | 691 | function handleTouchStartDollyPan() { 692 | 693 | if ( scope.enableZoom ) handleTouchStartDolly(); 694 | 695 | if ( scope.enablePan ) handleTouchStartPan(); 696 | 697 | } 698 | 699 | function handleTouchStartDollyRotate() { 700 | 701 | if ( scope.enableZoom ) handleTouchStartDolly(); 702 | 703 | if ( scope.enableRotate ) handleTouchStartRotate(); 704 | 705 | } 706 | 707 | function handleTouchMoveRotate( event ) { 708 | 709 | if ( pointers.length == 1 ) { 710 | 711 | rotateEnd.set( event.pageX, event.pageY ); 712 | 713 | } else { 714 | 715 | const position = getSecondPointerPosition( event ); 716 | 717 | const x = 0.5 * ( event.pageX + position.x ); 718 | const y = 0.5 * ( event.pageY + position.y ); 719 | 720 | rotateEnd.set( x, y ); 721 | 722 | } 723 | 724 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 725 | 726 | const element = scope.domElement; 727 | 728 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 729 | 730 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 731 | 732 | rotateStart.copy( rotateEnd ); 733 | 734 | } 735 | 736 | function handleTouchMovePan( event ) { 737 | 738 | if ( pointers.length === 1 ) { 739 | 740 | panEnd.set( event.pageX, event.pageY ); 741 | 742 | } else { 743 | 744 | const position = getSecondPointerPosition( event ); 745 | 746 | const x = 0.5 * ( event.pageX + position.x ); 747 | const y = 0.5 * ( event.pageY + position.y ); 748 | 749 | panEnd.set( x, y ); 750 | 751 | } 752 | 753 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 754 | 755 | pan( panDelta.x, panDelta.y ); 756 | 757 | panStart.copy( panEnd ); 758 | 759 | } 760 | 761 | function handleTouchMoveDolly( event ) { 762 | 763 | const position = getSecondPointerPosition( event ); 764 | 765 | const dx = event.pageX - position.x; 766 | const dy = event.pageY - position.y; 767 | 768 | const distance = Math.sqrt( dx * dx + dy * dy ); 769 | 770 | dollyEnd.set( 0, distance ); 771 | 772 | dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) ); 773 | 774 | dollyOut( dollyDelta.y ); 775 | 776 | dollyStart.copy( dollyEnd ); 777 | 778 | } 779 | 780 | function handleTouchMoveDollyPan( event ) { 781 | 782 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 783 | 784 | if ( scope.enablePan ) handleTouchMovePan( event ); 785 | 786 | } 787 | 788 | function handleTouchMoveDollyRotate( event ) { 789 | 790 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 791 | 792 | if ( scope.enableRotate ) handleTouchMoveRotate( event ); 793 | 794 | } 795 | 796 | function handleTouchEnd( /*event*/ ) { 797 | 798 | // no-op 799 | 800 | } 801 | 802 | // 803 | // event handlers - FSM: listen for events and reset state 804 | // 805 | 806 | function onPointerDown( event ) { 807 | 808 | if ( scope.enabled === false ) return; 809 | 810 | if ( pointers.length === 0 ) { 811 | 812 | scope.domElement.setPointerCapture( event.pointerId ); 813 | 814 | scope.domElement.addEventListener( 'pointermove', onPointerMove ); 815 | scope.domElement.addEventListener( 'pointerup', onPointerUp ); 816 | 817 | } 818 | 819 | // 820 | 821 | addPointer( event ); 822 | 823 | if ( event.pointerType === 'touch' ) { 824 | 825 | onTouchStart( event ); 826 | 827 | } else { 828 | 829 | onMouseDown( event ); 830 | 831 | } 832 | 833 | } 834 | 835 | function onPointerMove( event ) { 836 | 837 | if ( scope.enabled === false ) return; 838 | 839 | if ( event.pointerType === 'touch' ) { 840 | 841 | onTouchMove( event ); 842 | 843 | } else { 844 | 845 | onMouseMove( event ); 846 | 847 | } 848 | 849 | } 850 | 851 | function onPointerUp( event ) { 852 | 853 | if ( scope.enabled === false ) return; 854 | 855 | if ( event.pointerType === 'touch' ) { 856 | 857 | onTouchEnd(); 858 | 859 | } else { 860 | 861 | onMouseUp( event ); 862 | 863 | } 864 | 865 | removePointer( event ); 866 | 867 | // 868 | 869 | if ( pointers.length === 0 ) { 870 | 871 | scope.domElement.releasePointerCapture( event.pointerId ); 872 | 873 | scope.domElement.removeEventListener( 'pointermove', onPointerMove ); 874 | scope.domElement.removeEventListener( 'pointerup', onPointerUp ); 875 | 876 | } 877 | 878 | } 879 | 880 | function onPointerCancel( event ) { 881 | 882 | removePointer( event ); 883 | 884 | } 885 | 886 | function onMouseDown( event ) { 887 | 888 | let mouseAction; 889 | 890 | switch ( event.button ) { 891 | 892 | case 0: 893 | 894 | mouseAction = scope.mouseButtons.LEFT; 895 | break; 896 | 897 | case 1: 898 | 899 | mouseAction = scope.mouseButtons.MIDDLE; 900 | break; 901 | 902 | case 2: 903 | 904 | mouseAction = scope.mouseButtons.RIGHT; 905 | break; 906 | 907 | default: 908 | 909 | mouseAction = - 1; 910 | 911 | } 912 | 913 | switch ( mouseAction ) { 914 | 915 | case MOUSE.DOLLY: 916 | 917 | if ( scope.enableZoom === false ) return; 918 | 919 | handleMouseDownDolly( event ); 920 | 921 | state = STATE.DOLLY; 922 | 923 | break; 924 | 925 | case MOUSE.ROTATE: 926 | 927 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 928 | 929 | if ( scope.enablePan === false ) return; 930 | 931 | handleMouseDownPan( event ); 932 | 933 | state = STATE.PAN; 934 | 935 | } else { 936 | 937 | if ( scope.enableRotate === false ) return; 938 | 939 | handleMouseDownRotate( event ); 940 | 941 | state = STATE.ROTATE; 942 | 943 | } 944 | 945 | break; 946 | 947 | case MOUSE.PAN: 948 | 949 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 950 | 951 | if ( scope.enableRotate === false ) return; 952 | 953 | handleMouseDownRotate( event ); 954 | 955 | state = STATE.ROTATE; 956 | 957 | } else { 958 | 959 | if ( scope.enablePan === false ) return; 960 | 961 | handleMouseDownPan( event ); 962 | 963 | state = STATE.PAN; 964 | 965 | } 966 | 967 | break; 968 | 969 | default: 970 | 971 | state = STATE.NONE; 972 | 973 | } 974 | 975 | if ( state !== STATE.NONE ) { 976 | 977 | scope.dispatchEvent( _startEvent ); 978 | 979 | } 980 | 981 | } 982 | 983 | function onMouseMove( event ) { 984 | 985 | if ( scope.enabled === false ) return; 986 | 987 | switch ( state ) { 988 | 989 | case STATE.ROTATE: 990 | 991 | if ( scope.enableRotate === false ) return; 992 | 993 | handleMouseMoveRotate( event ); 994 | 995 | break; 996 | 997 | case STATE.DOLLY: 998 | 999 | if ( scope.enableZoom === false ) return; 1000 | 1001 | handleMouseMoveDolly( event ); 1002 | 1003 | break; 1004 | 1005 | case STATE.PAN: 1006 | 1007 | if ( scope.enablePan === false ) return; 1008 | 1009 | handleMouseMovePan( event ); 1010 | 1011 | break; 1012 | 1013 | } 1014 | 1015 | } 1016 | 1017 | function onMouseUp( event ) { 1018 | 1019 | handleMouseUp( event ); 1020 | 1021 | scope.dispatchEvent( _endEvent ); 1022 | 1023 | state = STATE.NONE; 1024 | 1025 | } 1026 | 1027 | function onMouseWheel( event ) { 1028 | 1029 | if ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE ) return; 1030 | 1031 | event.preventDefault(); 1032 | 1033 | scope.dispatchEvent( _startEvent ); 1034 | 1035 | handleMouseWheel( event ); 1036 | 1037 | scope.dispatchEvent( _endEvent ); 1038 | 1039 | } 1040 | 1041 | function onKeyDown( event ) { 1042 | 1043 | if ( scope.enabled === false || scope.enablePan === false ) return; 1044 | 1045 | handleKeyDown( event ); 1046 | 1047 | } 1048 | 1049 | function onTouchStart( event ) { 1050 | 1051 | trackPointer( event ); 1052 | 1053 | switch ( pointers.length ) { 1054 | 1055 | case 1: 1056 | 1057 | switch ( scope.touches.ONE ) { 1058 | 1059 | case TOUCH.ROTATE: 1060 | 1061 | if ( scope.enableRotate === false ) return; 1062 | 1063 | handleTouchStartRotate(); 1064 | 1065 | state = STATE.TOUCH_ROTATE; 1066 | 1067 | break; 1068 | 1069 | case TOUCH.PAN: 1070 | 1071 | if ( scope.enablePan === false ) return; 1072 | 1073 | handleTouchStartPan(); 1074 | 1075 | state = STATE.TOUCH_PAN; 1076 | 1077 | break; 1078 | 1079 | default: 1080 | 1081 | state = STATE.NONE; 1082 | 1083 | } 1084 | 1085 | break; 1086 | 1087 | case 2: 1088 | 1089 | switch ( scope.touches.TWO ) { 1090 | 1091 | case TOUCH.DOLLY_PAN: 1092 | 1093 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 1094 | 1095 | handleTouchStartDollyPan(); 1096 | 1097 | state = STATE.TOUCH_DOLLY_PAN; 1098 | 1099 | break; 1100 | 1101 | case TOUCH.DOLLY_ROTATE: 1102 | 1103 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 1104 | 1105 | handleTouchStartDollyRotate(); 1106 | 1107 | state = STATE.TOUCH_DOLLY_ROTATE; 1108 | 1109 | break; 1110 | 1111 | default: 1112 | 1113 | state = STATE.NONE; 1114 | 1115 | } 1116 | 1117 | break; 1118 | 1119 | default: 1120 | 1121 | state = STATE.NONE; 1122 | 1123 | } 1124 | 1125 | if ( state !== STATE.NONE ) { 1126 | 1127 | scope.dispatchEvent( _startEvent ); 1128 | 1129 | } 1130 | 1131 | } 1132 | 1133 | function onTouchMove( event ) { 1134 | 1135 | trackPointer( event ); 1136 | 1137 | switch ( state ) { 1138 | 1139 | case STATE.TOUCH_ROTATE: 1140 | 1141 | if ( scope.enableRotate === false ) return; 1142 | 1143 | handleTouchMoveRotate( event ); 1144 | 1145 | scope.update(); 1146 | 1147 | break; 1148 | 1149 | case STATE.TOUCH_PAN: 1150 | 1151 | if ( scope.enablePan === false ) return; 1152 | 1153 | handleTouchMovePan( event ); 1154 | 1155 | scope.update(); 1156 | 1157 | break; 1158 | 1159 | case STATE.TOUCH_DOLLY_PAN: 1160 | 1161 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 1162 | 1163 | handleTouchMoveDollyPan( event ); 1164 | 1165 | scope.update(); 1166 | 1167 | break; 1168 | 1169 | case STATE.TOUCH_DOLLY_ROTATE: 1170 | 1171 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 1172 | 1173 | handleTouchMoveDollyRotate( event ); 1174 | 1175 | scope.update(); 1176 | 1177 | break; 1178 | 1179 | default: 1180 | 1181 | state = STATE.NONE; 1182 | 1183 | } 1184 | 1185 | } 1186 | 1187 | function onTouchEnd( event ) { 1188 | 1189 | handleTouchEnd( event ); 1190 | 1191 | scope.dispatchEvent( _endEvent ); 1192 | 1193 | state = STATE.NONE; 1194 | 1195 | } 1196 | 1197 | function onContextMenu( event ) { 1198 | 1199 | if ( scope.enabled === false ) return; 1200 | 1201 | event.preventDefault(); 1202 | 1203 | } 1204 | 1205 | function addPointer( event ) { 1206 | 1207 | pointers.push( event ); 1208 | 1209 | } 1210 | 1211 | function removePointer( event ) { 1212 | 1213 | delete pointerPositions[ event.pointerId ]; 1214 | 1215 | for ( let i = 0; i < pointers.length; i ++ ) { 1216 | 1217 | if ( pointers[ i ].pointerId == event.pointerId ) { 1218 | 1219 | pointers.splice( i, 1 ); 1220 | return; 1221 | 1222 | } 1223 | 1224 | } 1225 | 1226 | } 1227 | 1228 | function trackPointer( event ) { 1229 | 1230 | let position = pointerPositions[ event.pointerId ]; 1231 | 1232 | if ( position === undefined ) { 1233 | 1234 | position = new Vector2(); 1235 | pointerPositions[ event.pointerId ] = position; 1236 | 1237 | } 1238 | 1239 | position.set( event.pageX, event.pageY ); 1240 | 1241 | } 1242 | 1243 | function getSecondPointerPosition( event ) { 1244 | 1245 | const pointer = ( event.pointerId === pointers[ 0 ].pointerId ) ? pointers[ 1 ] : pointers[ 0 ]; 1246 | 1247 | return pointerPositions[ pointer.pointerId ]; 1248 | 1249 | } 1250 | 1251 | // 1252 | 1253 | scope.domElement.addEventListener( 'contextmenu', onContextMenu ); 1254 | 1255 | scope.domElement.addEventListener( 'pointerdown', onPointerDown ); 1256 | scope.domElement.addEventListener( 'pointercancel', onPointerCancel ); 1257 | scope.domElement.addEventListener( 'wheel', onMouseWheel, { passive: false } ); 1258 | 1259 | // force an update at start 1260 | 1261 | this.update(); 1262 | 1263 | } 1264 | 1265 | } 1266 | 1267 | 1268 | // This set of controls performs orbiting, dollying (zooming), and panning. 1269 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 1270 | // This is very similar to OrbitControls, another set of touch behavior 1271 | // 1272 | // Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate 1273 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 1274 | // Pan - left mouse, or arrow keys / touch: one-finger move 1275 | 1276 | class MapControls extends OrbitControls { 1277 | 1278 | constructor( object, domElement ) { 1279 | 1280 | super( object, domElement ); 1281 | 1282 | this.screenSpacePanning = false; // pan orthogonal to world-space direction camera.up 1283 | 1284 | this.mouseButtons.LEFT = MOUSE.PAN; 1285 | this.mouseButtons.RIGHT = MOUSE.ROTATE; 1286 | 1287 | this.touches.ONE = TOUCH.PAN; 1288 | this.touches.TWO = TOUCH.DOLLY_ROTATE; 1289 | 1290 | } 1291 | 1292 | } 1293 | 1294 | export { OrbitControls, MapControls }; 1295 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Suzanne Scene Running in Blender](https://www.soft8soft.com/wiki/images/b/b6/Suzanne_blender.jpg) 2 | 3 | # Three.js-Blender Template 4 | Use this starter project for creating 3D web applications with Blender and Three.js. It includes: 5 | 6 | * Blend file with "Suzanne" model and Principled BSDF shader. 7 | * Exported asset in glTF 2.0 format. 8 | * Equirectangular HDRI map for environment lighting. 9 | * Three.js modules. 10 | * Basic web server script written in Python. 11 | * index.html file with HTML/CSS/JavaScript code. 12 | 13 | See [this wiki page](https://www.soft8soft.com/wiki/index.php/Making_3D_web_apps_with_Blender_and_Three.js) for more info on how to make and run actual WebGL apps with this starter project. 14 | 15 | ## Support 16 | Got questions? Ask on the [Forums](https://www.soft8soft.com/forums/). 17 | 18 | ## License 19 | This project is licensed under the terms of the MIT license. 20 | -------------------------------------------------------------------------------- /RGBELoader.js: -------------------------------------------------------------------------------- 1 | import { 2 | DataTextureLoader, 3 | DataUtils, 4 | FloatType, 5 | HalfFloatType, 6 | LinearEncoding, 7 | LinearFilter, 8 | NearestFilter, 9 | RGBEEncoding, 10 | RGBEFormat, 11 | RGBFormat, 12 | UnsignedByteType 13 | } from './three.module.js'; 14 | 15 | // https://github.com/mrdoob/three.js/issues/5552 16 | // http://en.wikipedia.org/wiki/RGBE_image_format 17 | 18 | class RGBELoader extends DataTextureLoader { 19 | 20 | constructor( manager ) { 21 | 22 | super( manager ); 23 | 24 | this.type = HalfFloatType; 25 | 26 | } 27 | 28 | // adapted from http://www.graphics.cornell.edu/~bjw/rgbe.html 29 | 30 | parse( buffer ) { 31 | 32 | const 33 | /* return codes for rgbe routines */ 34 | //RGBE_RETURN_SUCCESS = 0, 35 | RGBE_RETURN_FAILURE = - 1, 36 | 37 | /* default error routine. change this to change error handling */ 38 | rgbe_read_error = 1, 39 | rgbe_write_error = 2, 40 | rgbe_format_error = 3, 41 | rgbe_memory_error = 4, 42 | rgbe_error = function ( rgbe_error_code, msg ) { 43 | 44 | switch ( rgbe_error_code ) { 45 | 46 | case rgbe_read_error: console.error( 'THREE.RGBELoader Read Error: ' + ( msg || '' ) ); 47 | break; 48 | case rgbe_write_error: console.error( 'THREE.RGBELoader Write Error: ' + ( msg || '' ) ); 49 | break; 50 | case rgbe_format_error: console.error( 'THREE.RGBELoader Bad File Format: ' + ( msg || '' ) ); 51 | break; 52 | default: 53 | case rgbe_memory_error: console.error( 'THREE.RGBELoader: Error: ' + ( msg || '' ) ); 54 | 55 | } 56 | 57 | return RGBE_RETURN_FAILURE; 58 | 59 | }, 60 | 61 | /* offsets to red, green, and blue components in a data (float) pixel */ 62 | //RGBE_DATA_RED = 0, 63 | //RGBE_DATA_GREEN = 1, 64 | //RGBE_DATA_BLUE = 2, 65 | 66 | /* number of floats per pixel, use 4 since stored in rgba image format */ 67 | //RGBE_DATA_SIZE = 4, 68 | 69 | /* flags indicating which fields in an rgbe_header_info are valid */ 70 | RGBE_VALID_PROGRAMTYPE = 1, 71 | RGBE_VALID_FORMAT = 2, 72 | RGBE_VALID_DIMENSIONS = 4, 73 | 74 | NEWLINE = '\n', 75 | 76 | fgets = function ( buffer, lineLimit, consume ) { 77 | 78 | const chunkSize = 128; 79 | 80 | lineLimit = ! lineLimit ? 1024 : lineLimit; 81 | let p = buffer.pos, 82 | i = - 1, len = 0, s = '', 83 | chunk = String.fromCharCode.apply( null, new Uint16Array( buffer.subarray( p, p + chunkSize ) ) ); 84 | 85 | while ( ( 0 > ( i = chunk.indexOf( NEWLINE ) ) ) && ( len < lineLimit ) && ( p < buffer.byteLength ) ) { 86 | 87 | s += chunk; len += chunk.length; 88 | p += chunkSize; 89 | chunk += String.fromCharCode.apply( null, new Uint16Array( buffer.subarray( p, p + chunkSize ) ) ); 90 | 91 | } 92 | 93 | if ( - 1 < i ) { 94 | 95 | /*for (i=l-1; i>=0; i--) { 96 | byteCode = m.charCodeAt(i); 97 | if (byteCode > 0x7f && byteCode <= 0x7ff) byteLen++; 98 | else if (byteCode > 0x7ff && byteCode <= 0xffff) byteLen += 2; 99 | if (byteCode >= 0xDC00 && byteCode <= 0xDFFF) i--; //trail surrogate 100 | }*/ 101 | if ( false !== consume ) buffer.pos += len + i + 1; 102 | return s + chunk.slice( 0, i ); 103 | 104 | } 105 | 106 | return false; 107 | 108 | }, 109 | 110 | /* minimal header reading. modify if you want to parse more information */ 111 | RGBE_ReadHeader = function ( buffer ) { 112 | 113 | 114 | // regexes to parse header info fields 115 | const magic_token_re = /^#\?(\S+)/, 116 | gamma_re = /^\s*GAMMA\s*=\s*(\d+(\.\d+)?)\s*$/, 117 | exposure_re = /^\s*EXPOSURE\s*=\s*(\d+(\.\d+)?)\s*$/, 118 | format_re = /^\s*FORMAT=(\S+)\s*$/, 119 | dimensions_re = /^\s*\-Y\s+(\d+)\s+\+X\s+(\d+)\s*$/, 120 | 121 | // RGBE format header struct 122 | header = { 123 | 124 | valid: 0, /* indicate which fields are valid */ 125 | 126 | string: '', /* the actual header string */ 127 | 128 | comments: '', /* comments found in header */ 129 | 130 | programtype: 'RGBE', /* listed at beginning of file to identify it after "#?". defaults to "RGBE" */ 131 | 132 | format: '', /* RGBE format, default 32-bit_rle_rgbe */ 133 | 134 | gamma: 1.0, /* image has already been gamma corrected with given gamma. defaults to 1.0 (no correction) */ 135 | 136 | exposure: 1.0, /* a value of 1.0 in an image corresponds to watts/steradian/m^2. defaults to 1.0 */ 137 | 138 | width: 0, height: 0 /* image dimensions, width/height */ 139 | 140 | }; 141 | 142 | let line, match; 143 | 144 | if ( buffer.pos >= buffer.byteLength || ! ( line = fgets( buffer ) ) ) { 145 | 146 | return rgbe_error( rgbe_read_error, 'no header found' ); 147 | 148 | } 149 | 150 | /* if you want to require the magic token then uncomment the next line */ 151 | if ( ! ( match = line.match( magic_token_re ) ) ) { 152 | 153 | return rgbe_error( rgbe_format_error, 'bad initial token' ); 154 | 155 | } 156 | 157 | header.valid |= RGBE_VALID_PROGRAMTYPE; 158 | header.programtype = match[ 1 ]; 159 | header.string += line + '\n'; 160 | 161 | while ( true ) { 162 | 163 | line = fgets( buffer ); 164 | if ( false === line ) break; 165 | header.string += line + '\n'; 166 | 167 | if ( '#' === line.charAt( 0 ) ) { 168 | 169 | header.comments += line + '\n'; 170 | continue; // comment line 171 | 172 | } 173 | 174 | if ( match = line.match( gamma_re ) ) { 175 | 176 | header.gamma = parseFloat( match[ 1 ], 10 ); 177 | 178 | } 179 | 180 | if ( match = line.match( exposure_re ) ) { 181 | 182 | header.exposure = parseFloat( match[ 1 ], 10 ); 183 | 184 | } 185 | 186 | if ( match = line.match( format_re ) ) { 187 | 188 | header.valid |= RGBE_VALID_FORMAT; 189 | header.format = match[ 1 ];//'32-bit_rle_rgbe'; 190 | 191 | } 192 | 193 | if ( match = line.match( dimensions_re ) ) { 194 | 195 | header.valid |= RGBE_VALID_DIMENSIONS; 196 | header.height = parseInt( match[ 1 ], 10 ); 197 | header.width = parseInt( match[ 2 ], 10 ); 198 | 199 | } 200 | 201 | if ( ( header.valid & RGBE_VALID_FORMAT ) && ( header.valid & RGBE_VALID_DIMENSIONS ) ) break; 202 | 203 | } 204 | 205 | if ( ! ( header.valid & RGBE_VALID_FORMAT ) ) { 206 | 207 | return rgbe_error( rgbe_format_error, 'missing format specifier' ); 208 | 209 | } 210 | 211 | if ( ! ( header.valid & RGBE_VALID_DIMENSIONS ) ) { 212 | 213 | return rgbe_error( rgbe_format_error, 'missing image size specifier' ); 214 | 215 | } 216 | 217 | return header; 218 | 219 | }, 220 | 221 | RGBE_ReadPixels_RLE = function ( buffer, w, h ) { 222 | 223 | const scanline_width = w; 224 | 225 | if ( 226 | // run length encoding is not allowed so read flat 227 | ( ( scanline_width < 8 ) || ( scanline_width > 0x7fff ) ) || 228 | // this file is not run length encoded 229 | ( ( 2 !== buffer[ 0 ] ) || ( 2 !== buffer[ 1 ] ) || ( buffer[ 2 ] & 0x80 ) ) 230 | ) { 231 | 232 | // return the flat buffer 233 | return new Uint8Array( buffer ); 234 | 235 | } 236 | 237 | if ( scanline_width !== ( ( buffer[ 2 ] << 8 ) | buffer[ 3 ] ) ) { 238 | 239 | return rgbe_error( rgbe_format_error, 'wrong scanline width' ); 240 | 241 | } 242 | 243 | const data_rgba = new Uint8Array( 4 * w * h ); 244 | 245 | if ( ! data_rgba.length ) { 246 | 247 | return rgbe_error( rgbe_memory_error, 'unable to allocate buffer space' ); 248 | 249 | } 250 | 251 | let offset = 0, pos = 0; 252 | 253 | const ptr_end = 4 * scanline_width; 254 | const rgbeStart = new Uint8Array( 4 ); 255 | const scanline_buffer = new Uint8Array( ptr_end ); 256 | let num_scanlines = h; 257 | 258 | // read in each successive scanline 259 | while ( ( num_scanlines > 0 ) && ( pos < buffer.byteLength ) ) { 260 | 261 | if ( pos + 4 > buffer.byteLength ) { 262 | 263 | return rgbe_error( rgbe_read_error ); 264 | 265 | } 266 | 267 | rgbeStart[ 0 ] = buffer[ pos ++ ]; 268 | rgbeStart[ 1 ] = buffer[ pos ++ ]; 269 | rgbeStart[ 2 ] = buffer[ pos ++ ]; 270 | rgbeStart[ 3 ] = buffer[ pos ++ ]; 271 | 272 | if ( ( 2 != rgbeStart[ 0 ] ) || ( 2 != rgbeStart[ 1 ] ) || ( ( ( rgbeStart[ 2 ] << 8 ) | rgbeStart[ 3 ] ) != scanline_width ) ) { 273 | 274 | return rgbe_error( rgbe_format_error, 'bad rgbe scanline format' ); 275 | 276 | } 277 | 278 | // read each of the four channels for the scanline into the buffer 279 | // first red, then green, then blue, then exponent 280 | let ptr = 0, count; 281 | 282 | while ( ( ptr < ptr_end ) && ( pos < buffer.byteLength ) ) { 283 | 284 | count = buffer[ pos ++ ]; 285 | const isEncodedRun = count > 128; 286 | if ( isEncodedRun ) count -= 128; 287 | 288 | if ( ( 0 === count ) || ( ptr + count > ptr_end ) ) { 289 | 290 | return rgbe_error( rgbe_format_error, 'bad scanline data' ); 291 | 292 | } 293 | 294 | if ( isEncodedRun ) { 295 | 296 | // a (encoded) run of the same value 297 | const byteValue = buffer[ pos ++ ]; 298 | for ( let i = 0; i < count; i ++ ) { 299 | 300 | scanline_buffer[ ptr ++ ] = byteValue; 301 | 302 | } 303 | //ptr += count; 304 | 305 | } else { 306 | 307 | // a literal-run 308 | scanline_buffer.set( buffer.subarray( pos, pos + count ), ptr ); 309 | ptr += count; pos += count; 310 | 311 | } 312 | 313 | } 314 | 315 | 316 | // now convert data from buffer into rgba 317 | // first red, then green, then blue, then exponent (alpha) 318 | const l = scanline_width; //scanline_buffer.byteLength; 319 | for ( let i = 0; i < l; i ++ ) { 320 | 321 | let off = 0; 322 | data_rgba[ offset ] = scanline_buffer[ i + off ]; 323 | off += scanline_width; //1; 324 | data_rgba[ offset + 1 ] = scanline_buffer[ i + off ]; 325 | off += scanline_width; //1; 326 | data_rgba[ offset + 2 ] = scanline_buffer[ i + off ]; 327 | off += scanline_width; //1; 328 | data_rgba[ offset + 3 ] = scanline_buffer[ i + off ]; 329 | offset += 4; 330 | 331 | } 332 | 333 | num_scanlines --; 334 | 335 | } 336 | 337 | return data_rgba; 338 | 339 | }; 340 | 341 | const RGBEByteToRGBFloat = function ( sourceArray, sourceOffset, destArray, destOffset ) { 342 | 343 | const e = sourceArray[ sourceOffset + 3 ]; 344 | const scale = Math.pow( 2.0, e - 128.0 ) / 255.0; 345 | 346 | destArray[ destOffset + 0 ] = sourceArray[ sourceOffset + 0 ] * scale; 347 | destArray[ destOffset + 1 ] = sourceArray[ sourceOffset + 1 ] * scale; 348 | destArray[ destOffset + 2 ] = sourceArray[ sourceOffset + 2 ] * scale; 349 | 350 | }; 351 | 352 | const RGBEByteToRGBHalf = function ( sourceArray, sourceOffset, destArray, destOffset ) { 353 | 354 | const e = sourceArray[ sourceOffset + 3 ]; 355 | const scale = Math.pow( 2.0, e - 128.0 ) / 255.0; 356 | 357 | // clamping to 65504, the maximum representable value in float16 358 | destArray[ destOffset + 0 ] = DataUtils.toHalfFloat( Math.min( sourceArray[ sourceOffset + 0 ] * scale, 65504 ) ); 359 | destArray[ destOffset + 1 ] = DataUtils.toHalfFloat( Math.min( sourceArray[ sourceOffset + 1 ] * scale, 65504 ) ); 360 | destArray[ destOffset + 2 ] = DataUtils.toHalfFloat( Math.min( sourceArray[ sourceOffset + 2 ] * scale, 65504 ) ); 361 | 362 | }; 363 | 364 | const byteArray = new Uint8Array( buffer ); 365 | byteArray.pos = 0; 366 | const rgbe_header_info = RGBE_ReadHeader( byteArray ); 367 | 368 | if ( RGBE_RETURN_FAILURE !== rgbe_header_info ) { 369 | 370 | const w = rgbe_header_info.width, 371 | h = rgbe_header_info.height, 372 | image_rgba_data = RGBE_ReadPixels_RLE( byteArray.subarray( byteArray.pos ), w, h ); 373 | 374 | if ( RGBE_RETURN_FAILURE !== image_rgba_data ) { 375 | 376 | let data, format, type; 377 | let numElements; 378 | 379 | switch ( this.type ) { 380 | 381 | case UnsignedByteType: 382 | 383 | data = image_rgba_data; 384 | format = RGBEFormat; // handled as THREE.RGBAFormat in shaders 385 | type = UnsignedByteType; 386 | break; 387 | 388 | case FloatType: 389 | 390 | numElements = image_rgba_data.length / 4; 391 | const floatArray = new Float32Array( numElements * 3 ); 392 | 393 | for ( let j = 0; j < numElements; j ++ ) { 394 | 395 | RGBEByteToRGBFloat( image_rgba_data, j * 4, floatArray, j * 3 ); 396 | 397 | } 398 | 399 | data = floatArray; 400 | format = RGBFormat; 401 | type = FloatType; 402 | break; 403 | 404 | case HalfFloatType: 405 | 406 | numElements = image_rgba_data.length / 4; 407 | const halfArray = new Uint16Array( numElements * 3 ); 408 | 409 | for ( let j = 0; j < numElements; j ++ ) { 410 | 411 | RGBEByteToRGBHalf( image_rgba_data, j * 4, halfArray, j * 3 ); 412 | 413 | } 414 | 415 | data = halfArray; 416 | format = RGBFormat; 417 | type = HalfFloatType; 418 | break; 419 | 420 | default: 421 | 422 | console.error( 'THREE.RGBELoader: unsupported type: ', this.type ); 423 | break; 424 | 425 | } 426 | 427 | return { 428 | width: w, height: h, 429 | data: data, 430 | header: rgbe_header_info.string, 431 | gamma: rgbe_header_info.gamma, 432 | exposure: rgbe_header_info.exposure, 433 | format: format, 434 | type: type 435 | }; 436 | 437 | } 438 | 439 | } 440 | 441 | return null; 442 | 443 | } 444 | 445 | setDataType( value ) { 446 | 447 | this.type = value; 448 | return this; 449 | 450 | } 451 | 452 | load( url, onLoad, onProgress, onError ) { 453 | 454 | function onLoadCallback( texture, texData ) { 455 | 456 | switch ( texture.type ) { 457 | 458 | case UnsignedByteType: 459 | 460 | texture.encoding = RGBEEncoding; 461 | texture.minFilter = NearestFilter; 462 | texture.magFilter = NearestFilter; 463 | texture.generateMipmaps = false; 464 | texture.flipY = true; 465 | break; 466 | 467 | case FloatType: 468 | 469 | texture.encoding = LinearEncoding; 470 | texture.minFilter = LinearFilter; 471 | texture.magFilter = LinearFilter; 472 | texture.generateMipmaps = false; 473 | texture.flipY = true; 474 | break; 475 | 476 | case HalfFloatType: 477 | 478 | texture.encoding = LinearEncoding; 479 | texture.minFilter = LinearFilter; 480 | texture.magFilter = LinearFilter; 481 | texture.generateMipmaps = false; 482 | texture.flipY = true; 483 | break; 484 | 485 | } 486 | 487 | if ( onLoad ) onLoad( texture, texData ); 488 | 489 | } 490 | 491 | return super.load( url, onLoadCallback, onProgress, onError ); 492 | 493 | } 494 | 495 | } 496 | 497 | export { RGBELoader }; 498 | -------------------------------------------------------------------------------- /environment.hdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Soft8Soft/threejs-blender-template/b26e55701c0a1323ce9aa57d357cddf0de3f854f/environment.hdr -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Blender-to-Three.js App Template 5 | 6 | 7 | 12 | 13 | 14 | 15 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | import http.server 2 | import os 3 | import bpy 4 | 5 | from threading import Thread, current_thread 6 | from functools import partial 7 | 8 | def ServeDirectoryWithHTTP(directory='.'): 9 | hostname = 'localhost' 10 | port = 8000 11 | directory = os.path.abspath(directory) 12 | handler = partial(http.server.SimpleHTTPRequestHandler, directory=directory) 13 | httpd = http.server.HTTPServer((hostname, port), handler, False) 14 | httpd.allow_reuse_address = True 15 | 16 | httpd.server_bind() 17 | httpd.server_activate() 18 | 19 | def serve_forever(httpd): 20 | with httpd: 21 | httpd.serve_forever() 22 | 23 | thread = Thread(target=serve_forever, args=(httpd, )) 24 | thread.setDaemon(True) 25 | thread.start() 26 | 27 | app_root = os.path.dirname(bpy.context.space_data.text.filepath) 28 | ServeDirectoryWithHTTP(app_root) 29 | -------------------------------------------------------------------------------- /suzanne.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Soft8Soft/threejs-blender-template/b26e55701c0a1323ce9aa57d357cddf0de3f854f/suzanne.bin -------------------------------------------------------------------------------- /suzanne.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Soft8Soft/threejs-blender-template/b26e55701c0a1323ce9aa57d357cddf0de3f854f/suzanne.blend -------------------------------------------------------------------------------- /suzanne.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset" : { 3 | "generator" : "Khronos glTF Blender I/O v1.6.16", 4 | "version" : "2.0" 5 | }, 6 | "scene" : 0, 7 | "scenes" : [ 8 | { 9 | "name" : "Scene", 10 | "nodes" : [ 11 | 0, 12 | 1, 13 | 2 14 | ] 15 | } 16 | ], 17 | "nodes" : [ 18 | { 19 | "name" : "Light", 20 | "rotation" : [ 21 | 0.16907575726509094, 22 | 0.7558803558349609, 23 | -0.27217137813568115, 24 | 0.570947527885437 25 | ], 26 | "translation" : [ 27 | 4.076245307922363, 28 | 5.903861999511719, 29 | -1.0054539442062378 30 | ] 31 | }, 32 | { 33 | "name" : "Camera", 34 | "rotation" : [ 35 | 0.483536034822464, 36 | 0.33687159419059753, 37 | -0.20870360732078552, 38 | 0.7804827094078064 39 | ], 40 | "translation" : [ 41 | 7.358891487121582, 42 | 4.958309173583984, 43 | 6.925790786743164 44 | ] 45 | }, 46 | { 47 | "mesh" : 0, 48 | "name" : "Suzanne", 49 | "rotation" : [ 50 | 0, 51 | 0.39927300810813904, 52 | 0, 53 | 0.9168322086334229 54 | ] 55 | } 56 | ], 57 | "materials" : [ 58 | { 59 | "doubleSided" : true, 60 | "name" : "Material.001", 61 | "pbrMetallicRoughness" : { 62 | "baseColorFactor" : [ 63 | 0.013649391010403633, 64 | 0.8000000715255737, 65 | 0, 66 | 1 67 | ], 68 | "metallicFactor" : 0, 69 | "roughnessFactor" : 0.5 70 | } 71 | } 72 | ], 73 | "meshes" : [ 74 | { 75 | "name" : "Suzanne", 76 | "primitives" : [ 77 | { 78 | "attributes" : { 79 | "POSITION" : 0, 80 | "NORMAL" : 1, 81 | "TEXCOORD_0" : 2 82 | }, 83 | "indices" : 3, 84 | "material" : 0 85 | } 86 | ] 87 | } 88 | ], 89 | "accessors" : [ 90 | { 91 | "bufferView" : 0, 92 | "componentType" : 5126, 93 | "count" : 556, 94 | "max" : [ 95 | 1.3671875, 96 | 0.984375, 97 | 0.8515625 98 | ], 99 | "min" : [ 100 | -1.3671875, 101 | -0.984375, 102 | -0.8515625 103 | ], 104 | "type" : "VEC3" 105 | }, 106 | { 107 | "bufferView" : 1, 108 | "componentType" : 5126, 109 | "count" : 556, 110 | "type" : "VEC3" 111 | }, 112 | { 113 | "bufferView" : 2, 114 | "componentType" : 5126, 115 | "count" : 556, 116 | "type" : "VEC2" 117 | }, 118 | { 119 | "bufferView" : 3, 120 | "componentType" : 5123, 121 | "count" : 2904, 122 | "type" : "SCALAR" 123 | } 124 | ], 125 | "bufferViews" : [ 126 | { 127 | "buffer" : 0, 128 | "byteLength" : 6672, 129 | "byteOffset" : 0 130 | }, 131 | { 132 | "buffer" : 0, 133 | "byteLength" : 6672, 134 | "byteOffset" : 6672 135 | }, 136 | { 137 | "buffer" : 0, 138 | "byteLength" : 4448, 139 | "byteOffset" : 13344 140 | }, 141 | { 142 | "buffer" : 0, 143 | "byteLength" : 5808, 144 | "byteOffset" : 17792 145 | } 146 | ], 147 | "buffers" : [ 148 | { 149 | "byteLength" : 23600, 150 | "uri" : "suzanne.bin" 151 | } 152 | ] 153 | } 154 | --------------------------------------------------------------------------------