Spaces:
Running
Running
import { | |
AnimationClip, | |
Bone, | |
Box3, | |
BufferAttribute, | |
BufferGeometry, | |
ClampToEdgeWrapping, | |
Color, | |
ColorManagement, | |
DirectionalLight, | |
DoubleSide, | |
FileLoader, | |
FrontSide, | |
Group, | |
ImageBitmapLoader, | |
InstancedMesh, | |
InterleavedBuffer, | |
InterleavedBufferAttribute, | |
Interpolant, | |
InterpolateDiscrete, | |
InterpolateLinear, | |
Line, | |
LineBasicMaterial, | |
LineLoop, | |
LineSegments, | |
LinearFilter, | |
LinearMipmapLinearFilter, | |
LinearMipmapNearestFilter, | |
LinearSRGBColorSpace, | |
Loader, | |
LoaderUtils, | |
Material, | |
MathUtils, | |
Matrix4, | |
Mesh, | |
MeshBasicMaterial, | |
MeshPhysicalMaterial, | |
MeshStandardMaterial, | |
MirroredRepeatWrapping, | |
NearestFilter, | |
NearestMipmapLinearFilter, | |
NearestMipmapNearestFilter, | |
NumberKeyframeTrack, | |
Object3D, | |
OrthographicCamera, | |
PerspectiveCamera, | |
PointLight, | |
Points, | |
PointsMaterial, | |
PropertyBinding, | |
Quaternion, | |
QuaternionKeyframeTrack, | |
RepeatWrapping, | |
Skeleton, | |
SkinnedMesh, | |
Sphere, | |
SpotLight, | |
Texture, | |
TextureLoader, | |
TriangleFanDrawMode, | |
TriangleStripDrawMode, | |
Vector2, | |
Vector3, | |
VectorKeyframeTrack, | |
SRGBColorSpace, | |
InstancedBufferAttribute | |
} from 'three'; | |
import { toTrianglesDrawMode } from '../utils/BufferGeometryUtils.js'; | |
class GLTFLoader extends Loader { | |
constructor( manager ) { | |
super( manager ); | |
this.dracoLoader = null; | |
this.ktx2Loader = null; | |
this.meshoptDecoder = null; | |
this.pluginCallbacks = []; | |
this.register( function ( parser ) { | |
return new GLTFMaterialsClearcoatExtension( parser ); | |
} ); | |
this.register( function ( parser ) { | |
return new GLTFMaterialsDispersionExtension( parser ); | |
} ); | |
this.register( function ( parser ) { | |
return new GLTFTextureBasisUExtension( parser ); | |
} ); | |
this.register( function ( parser ) { | |
return new GLTFTextureWebPExtension( parser ); | |
} ); | |
this.register( function ( parser ) { | |
return new GLTFTextureAVIFExtension( parser ); | |
} ); | |
this.register( function ( parser ) { | |
return new GLTFMaterialsSheenExtension( parser ); | |
} ); | |
this.register( function ( parser ) { | |
return new GLTFMaterialsTransmissionExtension( parser ); | |
} ); | |
this.register( function ( parser ) { | |
return new GLTFMaterialsVolumeExtension( parser ); | |
} ); | |
this.register( function ( parser ) { | |
return new GLTFMaterialsIorExtension( parser ); | |
} ); | |
this.register( function ( parser ) { | |
return new GLTFMaterialsEmissiveStrengthExtension( parser ); | |
} ); | |
this.register( function ( parser ) { | |
return new GLTFMaterialsSpecularExtension( parser ); | |
} ); | |
this.register( function ( parser ) { | |
return new GLTFMaterialsIridescenceExtension( parser ); | |
} ); | |
this.register( function ( parser ) { | |
return new GLTFMaterialsAnisotropyExtension( parser ); | |
} ); | |
this.register( function ( parser ) { | |
return new GLTFMaterialsBumpExtension( parser ); | |
} ); | |
this.register( function ( parser ) { | |
return new GLTFLightsExtension( parser ); | |
} ); | |
this.register( function ( parser ) { | |
return new GLTFMeshoptCompression( parser ); | |
} ); | |
this.register( function ( parser ) { | |
return new GLTFMeshGpuInstancing( parser ); | |
} ); | |
} | |
load( url, onLoad, onProgress, onError ) { | |
const scope = this; | |
let resourcePath; | |
if ( this.resourcePath !== '' ) { | |
resourcePath = this.resourcePath; | |
} else if ( this.path !== '' ) { | |
// If a base path is set, resources will be relative paths from that plus the relative path of the gltf file | |
// Example path = 'https://my-cnd-server.com/', url = 'assets/models/model.gltf' | |
// resourcePath = 'https://my-cnd-server.com/assets/models/' | |
// referenced resource 'model.bin' will be loaded from 'https://my-cnd-server.com/assets/models/model.bin' | |
// referenced resource '../textures/texture.png' will be loaded from 'https://my-cnd-server.com/assets/textures/texture.png' | |
const relativeUrl = LoaderUtils.extractUrlBase( url ); | |
resourcePath = LoaderUtils.resolveURL( relativeUrl, this.path ); | |
} else { | |
resourcePath = LoaderUtils.extractUrlBase( url ); | |
} | |
// Tells the LoadingManager to track an extra item, which resolves after | |
// the model is fully loaded. This means the count of items loaded will | |
// be incorrect, but ensures manager.onLoad() does not fire early. | |
this.manager.itemStart( url ); | |
const _onError = function ( e ) { | |
if ( onError ) { | |
onError( e ); | |
} else { | |
console.error( e ); | |
} | |
scope.manager.itemError( url ); | |
scope.manager.itemEnd( url ); | |
}; | |
const loader = new FileLoader( this.manager ); | |
loader.setPath( this.path ); | |
loader.setResponseType( 'arraybuffer' ); | |
loader.setRequestHeader( this.requestHeader ); | |
loader.setWithCredentials( this.withCredentials ); | |
loader.load( url, function ( data ) { | |
try { | |
scope.parse( data, resourcePath, function ( gltf ) { | |
onLoad( gltf ); | |
scope.manager.itemEnd( url ); | |
}, _onError ); | |
} catch ( e ) { | |
_onError( e ); | |
} | |
}, onProgress, _onError ); | |
} | |
setDRACOLoader( dracoLoader ) { | |
this.dracoLoader = dracoLoader; | |
return this; | |
} | |
setKTX2Loader( ktx2Loader ) { | |
this.ktx2Loader = ktx2Loader; | |
return this; | |
} | |
setMeshoptDecoder( meshoptDecoder ) { | |
this.meshoptDecoder = meshoptDecoder; | |
return this; | |
} | |
register( callback ) { | |
if ( this.pluginCallbacks.indexOf( callback ) === - 1 ) { | |
this.pluginCallbacks.push( callback ); | |
} | |
return this; | |
} | |
unregister( callback ) { | |
if ( this.pluginCallbacks.indexOf( callback ) !== - 1 ) { | |
this.pluginCallbacks.splice( this.pluginCallbacks.indexOf( callback ), 1 ); | |
} | |
return this; | |
} | |
parse( data, path, onLoad, onError ) { | |
let json; | |
const extensions = {}; | |
const plugins = {}; | |
const textDecoder = new TextDecoder(); | |
if ( typeof data === 'string' ) { | |
json = JSON.parse( data ); | |
} else if ( data instanceof ArrayBuffer ) { | |
const magic = textDecoder.decode( new Uint8Array( data, 0, 4 ) ); | |
if ( magic === BINARY_EXTENSION_HEADER_MAGIC ) { | |
try { | |
extensions[ EXTENSIONS.KHR_BINARY_GLTF ] = new GLTFBinaryExtension( data ); | |
} catch ( error ) { | |
if ( onError ) onError( error ); | |
return; | |
} | |
json = JSON.parse( extensions[ EXTENSIONS.KHR_BINARY_GLTF ].content ); | |
} else { | |
json = JSON.parse( textDecoder.decode( data ) ); | |
} | |
} else { | |
json = data; | |
} | |
if ( json.asset === undefined || json.asset.version[ 0 ] < 2 ) { | |
if ( onError ) onError( new Error( 'THREE.GLTFLoader: Unsupported asset. glTF versions >=2.0 are supported.' ) ); | |
return; | |
} | |
const parser = new GLTFParser( json, { | |
path: path || this.resourcePath || '', | |
crossOrigin: this.crossOrigin, | |
requestHeader: this.requestHeader, | |
manager: this.manager, | |
ktx2Loader: this.ktx2Loader, | |
meshoptDecoder: this.meshoptDecoder | |
} ); | |
parser.fileLoader.setRequestHeader( this.requestHeader ); | |
for ( let i = 0; i < this.pluginCallbacks.length; i ++ ) { | |
const plugin = this.pluginCallbacks[ i ]( parser ); | |
if ( ! plugin.name ) console.error( 'THREE.GLTFLoader: Invalid plugin found: missing name' ); | |
plugins[ plugin.name ] = plugin; | |
// Workaround to avoid determining as unknown extension | |
// in addUnknownExtensionsToUserData(). | |
// Remove this workaround if we move all the existing | |
// extension handlers to plugin system | |
extensions[ plugin.name ] = true; | |
} | |
if ( json.extensionsUsed ) { | |
for ( let i = 0; i < json.extensionsUsed.length; ++ i ) { | |
const extensionName = json.extensionsUsed[ i ]; | |
const extensionsRequired = json.extensionsRequired || []; | |
switch ( extensionName ) { | |
case EXTENSIONS.KHR_MATERIALS_UNLIT: | |
extensions[ extensionName ] = new GLTFMaterialsUnlitExtension(); | |
break; | |
case EXTENSIONS.KHR_DRACO_MESH_COMPRESSION: | |
extensions[ extensionName ] = new GLTFDracoMeshCompressionExtension( json, this.dracoLoader ); | |
break; | |
case EXTENSIONS.KHR_TEXTURE_TRANSFORM: | |
extensions[ extensionName ] = new GLTFTextureTransformExtension(); | |
break; | |
case EXTENSIONS.KHR_MESH_QUANTIZATION: | |
extensions[ extensionName ] = new GLTFMeshQuantizationExtension(); | |
break; | |
default: | |
if ( extensionsRequired.indexOf( extensionName ) >= 0 && plugins[ extensionName ] === undefined ) { | |
console.warn( 'THREE.GLTFLoader: Unknown extension "' + extensionName + '".' ); | |
} | |
} | |
} | |
} | |
parser.setExtensions( extensions ); | |
parser.setPlugins( plugins ); | |
parser.parse( onLoad, onError ); | |
} | |
parseAsync( data, path ) { | |
const scope = this; | |
return new Promise( function ( resolve, reject ) { | |
scope.parse( data, path, resolve, reject ); | |
} ); | |
} | |
} | |
/* GLTFREGISTRY */ | |
function GLTFRegistry() { | |
let objects = {}; | |
return { | |
get: function ( key ) { | |
return objects[ key ]; | |
}, | |
add: function ( key, object ) { | |
objects[ key ] = object; | |
}, | |
remove: function ( key ) { | |
delete objects[ key ]; | |
}, | |
removeAll: function () { | |
objects = {}; | |
} | |
}; | |
} | |
/*********************************/ | |
/********** EXTENSIONS ***********/ | |
/*********************************/ | |
const EXTENSIONS = { | |
KHR_BINARY_GLTF: 'KHR_binary_glTF', | |
KHR_DRACO_MESH_COMPRESSION: 'KHR_draco_mesh_compression', | |
KHR_LIGHTS_PUNCTUAL: 'KHR_lights_punctual', | |
KHR_MATERIALS_CLEARCOAT: 'KHR_materials_clearcoat', | |
KHR_MATERIALS_DISPERSION: 'KHR_materials_dispersion', | |
KHR_MATERIALS_IOR: 'KHR_materials_ior', | |
KHR_MATERIALS_SHEEN: 'KHR_materials_sheen', | |
KHR_MATERIALS_SPECULAR: 'KHR_materials_specular', | |
KHR_MATERIALS_TRANSMISSION: 'KHR_materials_transmission', | |
KHR_MATERIALS_IRIDESCENCE: 'KHR_materials_iridescence', | |
KHR_MATERIALS_ANISOTROPY: 'KHR_materials_anisotropy', | |
KHR_MATERIALS_UNLIT: 'KHR_materials_unlit', | |
KHR_MATERIALS_VOLUME: 'KHR_materials_volume', | |
KHR_TEXTURE_BASISU: 'KHR_texture_basisu', | |
KHR_TEXTURE_TRANSFORM: 'KHR_texture_transform', | |
KHR_MESH_QUANTIZATION: 'KHR_mesh_quantization', | |
KHR_MATERIALS_EMISSIVE_STRENGTH: 'KHR_materials_emissive_strength', | |
EXT_MATERIALS_BUMP: 'EXT_materials_bump', | |
EXT_TEXTURE_WEBP: 'EXT_texture_webp', | |
EXT_TEXTURE_AVIF: 'EXT_texture_avif', | |
EXT_MESHOPT_COMPRESSION: 'EXT_meshopt_compression', | |
EXT_MESH_GPU_INSTANCING: 'EXT_mesh_gpu_instancing' | |
}; | |
/** | |
* Punctual Lights Extension | |
* | |
* Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual | |
*/ | |
class GLTFLightsExtension { | |
constructor( parser ) { | |
this.parser = parser; | |
this.name = EXTENSIONS.KHR_LIGHTS_PUNCTUAL; | |
// Object3D instance caches | |
this.cache = { refs: {}, uses: {} }; | |
} | |
_markDefs() { | |
const parser = this.parser; | |
const nodeDefs = this.parser.json.nodes || []; | |
for ( let nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex ++ ) { | |
const nodeDef = nodeDefs[ nodeIndex ]; | |
if ( nodeDef.extensions | |
&& nodeDef.extensions[ this.name ] | |
&& nodeDef.extensions[ this.name ].light !== undefined ) { | |
parser._addNodeRef( this.cache, nodeDef.extensions[ this.name ].light ); | |
} | |
} | |
} | |
_loadLight( lightIndex ) { | |
const parser = this.parser; | |
const cacheKey = 'light:' + lightIndex; | |
let dependency = parser.cache.get( cacheKey ); | |
if ( dependency ) return dependency; | |
const json = parser.json; | |
const extensions = ( json.extensions && json.extensions[ this.name ] ) || {}; | |
const lightDefs = extensions.lights || []; | |
const lightDef = lightDefs[ lightIndex ]; | |
let lightNode; | |
const color = new Color( 0xffffff ); | |
if ( lightDef.color !== undefined ) color.setRGB( lightDef.color[ 0 ], lightDef.color[ 1 ], lightDef.color[ 2 ], LinearSRGBColorSpace ); | |
const range = lightDef.range !== undefined ? lightDef.range : 0; | |
switch ( lightDef.type ) { | |
case 'directional': | |
lightNode = new DirectionalLight( color ); | |
lightNode.target.position.set( 0, 0, - 1 ); | |
lightNode.add( lightNode.target ); | |
break; | |
case 'point': | |
lightNode = new PointLight( color ); | |
lightNode.distance = range; | |
break; | |
case 'spot': | |
lightNode = new SpotLight( color ); | |
lightNode.distance = range; | |
// Handle spotlight properties. | |
lightDef.spot = lightDef.spot || {}; | |
lightDef.spot.innerConeAngle = lightDef.spot.innerConeAngle !== undefined ? lightDef.spot.innerConeAngle : 0; | |
lightDef.spot.outerConeAngle = lightDef.spot.outerConeAngle !== undefined ? lightDef.spot.outerConeAngle : Math.PI / 4.0; | |
lightNode.angle = lightDef.spot.outerConeAngle; | |
lightNode.penumbra = 1.0 - lightDef.spot.innerConeAngle / lightDef.spot.outerConeAngle; | |
lightNode.target.position.set( 0, 0, - 1 ); | |
lightNode.add( lightNode.target ); | |
break; | |
default: | |
throw new Error( 'THREE.GLTFLoader: Unexpected light type: ' + lightDef.type ); | |
} | |
// Some lights (e.g. spot) default to a position other than the origin. Reset the position | |
// here, because node-level parsing will only override position if explicitly specified. | |
lightNode.position.set( 0, 0, 0 ); | |
lightNode.decay = 2; | |
assignExtrasToUserData( lightNode, lightDef ); | |
if ( lightDef.intensity !== undefined ) lightNode.intensity = lightDef.intensity; | |
lightNode.name = parser.createUniqueName( lightDef.name || ( 'light_' + lightIndex ) ); | |
dependency = Promise.resolve( lightNode ); | |
parser.cache.add( cacheKey, dependency ); | |
return dependency; | |
} | |
getDependency( type, index ) { | |
if ( type !== 'light' ) return; | |
return this._loadLight( index ); | |
} | |
createNodeAttachment( nodeIndex ) { | |
const self = this; | |
const parser = this.parser; | |
const json = parser.json; | |
const nodeDef = json.nodes[ nodeIndex ]; | |
const lightDef = ( nodeDef.extensions && nodeDef.extensions[ this.name ] ) || {}; | |
const lightIndex = lightDef.light; | |
if ( lightIndex === undefined ) return null; | |
return this._loadLight( lightIndex ).then( function ( light ) { | |
return parser._getNodeRef( self.cache, lightIndex, light ); | |
} ); | |
} | |
} | |
/** | |
* Unlit Materials Extension | |
* | |
* Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_unlit | |
*/ | |
class GLTFMaterialsUnlitExtension { | |
constructor() { | |
this.name = EXTENSIONS.KHR_MATERIALS_UNLIT; | |
} | |
getMaterialType() { | |
return MeshBasicMaterial; | |
} | |
extendParams( materialParams, materialDef, parser ) { | |
const pending = []; | |
materialParams.color = new Color( 1.0, 1.0, 1.0 ); | |
materialParams.opacity = 1.0; | |
const metallicRoughness = materialDef.pbrMetallicRoughness; | |
if ( metallicRoughness ) { | |
if ( Array.isArray( metallicRoughness.baseColorFactor ) ) { | |
const array = metallicRoughness.baseColorFactor; | |
materialParams.color.setRGB( array[ 0 ], array[ 1 ], array[ 2 ], LinearSRGBColorSpace ); | |
materialParams.opacity = array[ 3 ]; | |
} | |
if ( metallicRoughness.baseColorTexture !== undefined ) { | |
pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture, SRGBColorSpace ) ); | |
} | |
} | |
return Promise.all( pending ); | |
} | |
} | |
/** | |
* Materials Emissive Strength Extension | |
* | |
* Specification: https://github.com/KhronosGroup/glTF/blob/5768b3ce0ef32bc39cdf1bef10b948586635ead3/extensions/2.0/Khronos/KHR_materials_emissive_strength/README.md | |
*/ | |
class GLTFMaterialsEmissiveStrengthExtension { | |
constructor( parser ) { | |
this.parser = parser; | |
this.name = EXTENSIONS.KHR_MATERIALS_EMISSIVE_STRENGTH; | |
} | |
extendMaterialParams( materialIndex, materialParams ) { | |
const parser = this.parser; | |
const materialDef = parser.json.materials[ materialIndex ]; | |
if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { | |
return Promise.resolve(); | |
} | |
const emissiveStrength = materialDef.extensions[ this.name ].emissiveStrength; | |
if ( emissiveStrength !== undefined ) { | |
materialParams.emissiveIntensity = emissiveStrength; | |
} | |
return Promise.resolve(); | |
} | |
} | |
/** | |
* Clearcoat Materials Extension | |
* | |
* Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_clearcoat | |
*/ | |
class GLTFMaterialsClearcoatExtension { | |
constructor( parser ) { | |
this.parser = parser; | |
this.name = EXTENSIONS.KHR_MATERIALS_CLEARCOAT; | |
} | |
getMaterialType( materialIndex ) { | |
const parser = this.parser; | |
const materialDef = parser.json.materials[ materialIndex ]; | |
if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; | |
return MeshPhysicalMaterial; | |
} | |
extendMaterialParams( materialIndex, materialParams ) { | |
const parser = this.parser; | |
const materialDef = parser.json.materials[ materialIndex ]; | |
if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { | |
return Promise.resolve(); | |
} | |
const pending = []; | |
const extension = materialDef.extensions[ this.name ]; | |
if ( extension.clearcoatFactor !== undefined ) { | |
materialParams.clearcoat = extension.clearcoatFactor; | |
} | |
if ( extension.clearcoatTexture !== undefined ) { | |
pending.push( parser.assignTexture( materialParams, 'clearcoatMap', extension.clearcoatTexture ) ); | |
} | |
if ( extension.clearcoatRoughnessFactor !== undefined ) { | |
materialParams.clearcoatRoughness = extension.clearcoatRoughnessFactor; | |
} | |
if ( extension.clearcoatRoughnessTexture !== undefined ) { | |
pending.push( parser.assignTexture( materialParams, 'clearcoatRoughnessMap', extension.clearcoatRoughnessTexture ) ); | |
} | |
if ( extension.clearcoatNormalTexture !== undefined ) { | |
pending.push( parser.assignTexture( materialParams, 'clearcoatNormalMap', extension.clearcoatNormalTexture ) ); | |
if ( extension.clearcoatNormalTexture.scale !== undefined ) { | |
const scale = extension.clearcoatNormalTexture.scale; | |
materialParams.clearcoatNormalScale = new Vector2( scale, scale ); | |
} | |
} | |
return Promise.all( pending ); | |
} | |
} | |
/** | |
* Materials dispersion Extension | |
* | |
* Specification: https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_dispersion | |
*/ | |
class GLTFMaterialsDispersionExtension { | |
constructor( parser ) { | |
this.parser = parser; | |
this.name = EXTENSIONS.KHR_MATERIALS_DISPERSION; | |
} | |
getMaterialType( materialIndex ) { | |
const parser = this.parser; | |
const materialDef = parser.json.materials[ materialIndex ]; | |
if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; | |
return MeshPhysicalMaterial; | |
} | |
extendMaterialParams( materialIndex, materialParams ) { | |
const parser = this.parser; | |
const materialDef = parser.json.materials[ materialIndex ]; | |
if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { | |
return Promise.resolve(); | |
} | |
const extension = materialDef.extensions[ this.name ]; | |
materialParams.dispersion = extension.dispersion !== undefined ? extension.dispersion : 0; | |
return Promise.resolve(); | |
} | |
} | |
/** | |
* Iridescence Materials Extension | |
* | |
* Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_iridescence | |
*/ | |
class GLTFMaterialsIridescenceExtension { | |
constructor( parser ) { | |
this.parser = parser; | |
this.name = EXTENSIONS.KHR_MATERIALS_IRIDESCENCE; | |
} | |
getMaterialType( materialIndex ) { | |
const parser = this.parser; | |
const materialDef = parser.json.materials[ materialIndex ]; | |
if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; | |
return MeshPhysicalMaterial; | |
} | |
extendMaterialParams( materialIndex, materialParams ) { | |
const parser = this.parser; | |
const materialDef = parser.json.materials[ materialIndex ]; | |
if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { | |
return Promise.resolve(); | |
} | |
const pending = []; | |
const extension = materialDef.extensions[ this.name ]; | |
if ( extension.iridescenceFactor !== undefined ) { | |
materialParams.iridescence = extension.iridescenceFactor; | |
} | |
if ( extension.iridescenceTexture !== undefined ) { | |
pending.push( parser.assignTexture( materialParams, 'iridescenceMap', extension.iridescenceTexture ) ); | |
} | |
if ( extension.iridescenceIor !== undefined ) { | |
materialParams.iridescenceIOR = extension.iridescenceIor; | |
} | |
if ( materialParams.iridescenceThicknessRange === undefined ) { | |
materialParams.iridescenceThicknessRange = [ 100, 400 ]; | |
} | |
if ( extension.iridescenceThicknessMinimum !== undefined ) { | |
materialParams.iridescenceThicknessRange[ 0 ] = extension.iridescenceThicknessMinimum; | |
} | |
if ( extension.iridescenceThicknessMaximum !== undefined ) { | |
materialParams.iridescenceThicknessRange[ 1 ] = extension.iridescenceThicknessMaximum; | |
} | |
if ( extension.iridescenceThicknessTexture !== undefined ) { | |
pending.push( parser.assignTexture( materialParams, 'iridescenceThicknessMap', extension.iridescenceThicknessTexture ) ); | |
} | |
return Promise.all( pending ); | |
} | |
} | |
/** | |
* Sheen Materials Extension | |
* | |
* Specification: https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_sheen | |
*/ | |
class GLTFMaterialsSheenExtension { | |
constructor( parser ) { | |
this.parser = parser; | |
this.name = EXTENSIONS.KHR_MATERIALS_SHEEN; | |
} | |
getMaterialType( materialIndex ) { | |
const parser = this.parser; | |
const materialDef = parser.json.materials[ materialIndex ]; | |
if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; | |
return MeshPhysicalMaterial; | |
} | |
extendMaterialParams( materialIndex, materialParams ) { | |
const parser = this.parser; | |
const materialDef = parser.json.materials[ materialIndex ]; | |
if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { | |
return Promise.resolve(); | |
} | |
const pending = []; | |
materialParams.sheenColor = new Color( 0, 0, 0 ); | |
materialParams.sheenRoughness = 0; | |
materialParams.sheen = 1; | |
const extension = materialDef.extensions[ this.name ]; | |
if ( extension.sheenColorFactor !== undefined ) { | |
const colorFactor = extension.sheenColorFactor; | |
materialParams.sheenColor.setRGB( colorFactor[ 0 ], colorFactor[ 1 ], colorFactor[ 2 ], LinearSRGBColorSpace ); | |
} | |
if ( extension.sheenRoughnessFactor !== undefined ) { | |
materialParams.sheenRoughness = extension.sheenRoughnessFactor; | |
} | |
if ( extension.sheenColorTexture !== undefined ) { | |
pending.push( parser.assignTexture( materialParams, 'sheenColorMap', extension.sheenColorTexture, SRGBColorSpace ) ); | |
} | |
if ( extension.sheenRoughnessTexture !== undefined ) { | |
pending.push( parser.assignTexture( materialParams, 'sheenRoughnessMap', extension.sheenRoughnessTexture ) ); | |
} | |
return Promise.all( pending ); | |
} | |
} | |
/** | |
* Transmission Materials Extension | |
* | |
* Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_transmission | |
* Draft: https://github.com/KhronosGroup/glTF/pull/1698 | |
*/ | |
class GLTFMaterialsTransmissionExtension { | |
constructor( parser ) { | |
this.parser = parser; | |
this.name = EXTENSIONS.KHR_MATERIALS_TRANSMISSION; | |
} | |
getMaterialType( materialIndex ) { | |
const parser = this.parser; | |
const materialDef = parser.json.materials[ materialIndex ]; | |
if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; | |
return MeshPhysicalMaterial; | |
} | |
extendMaterialParams( materialIndex, materialParams ) { | |
const parser = this.parser; | |
const materialDef = parser.json.materials[ materialIndex ]; | |
if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { | |
return Promise.resolve(); | |
} | |
const pending = []; | |
const extension = materialDef.extensions[ this.name ]; | |
if ( extension.transmissionFactor !== undefined ) { | |
materialParams.transmission = extension.transmissionFactor; | |
} | |
if ( extension.transmissionTexture !== undefined ) { | |
pending.push( parser.assignTexture( materialParams, 'transmissionMap', extension.transmissionTexture ) ); | |
} | |
return Promise.all( pending ); | |
} | |
} | |
/** | |
* Materials Volume Extension | |
* | |
* Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_volume | |
*/ | |
class GLTFMaterialsVolumeExtension { | |
constructor( parser ) { | |
this.parser = parser; | |
this.name = EXTENSIONS.KHR_MATERIALS_VOLUME; | |
} | |
getMaterialType( materialIndex ) { | |
const parser = this.parser; | |
const materialDef = parser.json.materials[ materialIndex ]; | |
if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; | |
return MeshPhysicalMaterial; | |
} | |
extendMaterialParams( materialIndex, materialParams ) { | |
const parser = this.parser; | |
const materialDef = parser.json.materials[ materialIndex ]; | |
if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { | |
return Promise.resolve(); | |
} | |
const pending = []; | |
const extension = materialDef.extensions[ this.name ]; | |
materialParams.thickness = extension.thicknessFactor !== undefined ? extension.thicknessFactor : 0; | |
if ( extension.thicknessTexture !== undefined ) { | |
pending.push( parser.assignTexture( materialParams, 'thicknessMap', extension.thicknessTexture ) ); | |
} | |
materialParams.attenuationDistance = extension.attenuationDistance || Infinity; | |
const colorArray = extension.attenuationColor || [ 1, 1, 1 ]; | |
materialParams.attenuationColor = new Color().setRGB( colorArray[ 0 ], colorArray[ 1 ], colorArray[ 2 ], LinearSRGBColorSpace ); | |
return Promise.all( pending ); | |
} | |
} | |
/** | |
* Materials ior Extension | |
* | |
* Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_ior | |
*/ | |
class GLTFMaterialsIorExtension { | |
constructor( parser ) { | |
this.parser = parser; | |
this.name = EXTENSIONS.KHR_MATERIALS_IOR; | |
} | |
getMaterialType( materialIndex ) { | |
const parser = this.parser; | |
const materialDef = parser.json.materials[ materialIndex ]; | |
if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; | |
return MeshPhysicalMaterial; | |
} | |
extendMaterialParams( materialIndex, materialParams ) { | |
const parser = this.parser; | |
const materialDef = parser.json.materials[ materialIndex ]; | |
if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { | |
return Promise.resolve(); | |
} | |
const extension = materialDef.extensions[ this.name ]; | |
materialParams.ior = extension.ior !== undefined ? extension.ior : 1.5; | |
return Promise.resolve(); | |
} | |
} | |
/** | |
* Materials specular Extension | |
* | |
* Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_specular | |
*/ | |
class GLTFMaterialsSpecularExtension { | |
constructor( parser ) { | |
this.parser = parser; | |
this.name = EXTENSIONS.KHR_MATERIALS_SPECULAR; | |
} | |
getMaterialType( materialIndex ) { | |
const parser = this.parser; | |
const materialDef = parser.json.materials[ materialIndex ]; | |
if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; | |
return MeshPhysicalMaterial; | |
} | |
extendMaterialParams( materialIndex, materialParams ) { | |
const parser = this.parser; | |
const materialDef = parser.json.materials[ materialIndex ]; | |
if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { | |
return Promise.resolve(); | |
} | |
const pending = []; | |
const extension = materialDef.extensions[ this.name ]; | |
materialParams.specularIntensity = extension.specularFactor !== undefined ? extension.specularFactor : 1.0; | |
if ( extension.specularTexture !== undefined ) { | |
pending.push( parser.assignTexture( materialParams, 'specularIntensityMap', extension.specularTexture ) ); | |
} | |
const colorArray = extension.specularColorFactor || [ 1, 1, 1 ]; | |
materialParams.specularColor = new Color().setRGB( colorArray[ 0 ], colorArray[ 1 ], colorArray[ 2 ], LinearSRGBColorSpace ); | |
if ( extension.specularColorTexture !== undefined ) { | |
pending.push( parser.assignTexture( materialParams, 'specularColorMap', extension.specularColorTexture, SRGBColorSpace ) ); | |
} | |
return Promise.all( pending ); | |
} | |
} | |
/** | |
* Materials bump Extension | |
* | |
* Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/EXT_materials_bump | |
*/ | |
class GLTFMaterialsBumpExtension { | |
constructor( parser ) { | |
this.parser = parser; | |
this.name = EXTENSIONS.EXT_MATERIALS_BUMP; | |
} | |
getMaterialType( materialIndex ) { | |
const parser = this.parser; | |
const materialDef = parser.json.materials[ materialIndex ]; | |
if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; | |
return MeshPhysicalMaterial; | |
} | |
extendMaterialParams( materialIndex, materialParams ) { | |
const parser = this.parser; | |
const materialDef = parser.json.materials[ materialIndex ]; | |
if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { | |
return Promise.resolve(); | |
} | |
const pending = []; | |
const extension = materialDef.extensions[ this.name ]; | |
materialParams.bumpScale = extension.bumpFactor !== undefined ? extension.bumpFactor : 1.0; | |
if ( extension.bumpTexture !== undefined ) { | |
pending.push( parser.assignTexture( materialParams, 'bumpMap', extension.bumpTexture ) ); | |
} | |
return Promise.all( pending ); | |
} | |
} | |
/** | |
* Materials anisotropy Extension | |
* | |
* Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_anisotropy | |
*/ | |
class GLTFMaterialsAnisotropyExtension { | |
constructor( parser ) { | |
this.parser = parser; | |
this.name = EXTENSIONS.KHR_MATERIALS_ANISOTROPY; | |
} | |
getMaterialType( materialIndex ) { | |
const parser = this.parser; | |
const materialDef = parser.json.materials[ materialIndex ]; | |
if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; | |
return MeshPhysicalMaterial; | |
} | |
extendMaterialParams( materialIndex, materialParams ) { | |
const parser = this.parser; | |
const materialDef = parser.json.materials[ materialIndex ]; | |
if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { | |
return Promise.resolve(); | |
} | |
const pending = []; | |
const extension = materialDef.extensions[ this.name ]; | |
if ( extension.anisotropyStrength !== undefined ) { | |
materialParams.anisotropy = extension.anisotropyStrength; | |
} | |
if ( extension.anisotropyRotation !== undefined ) { | |
materialParams.anisotropyRotation = extension.anisotropyRotation; | |
} | |
if ( extension.anisotropyTexture !== undefined ) { | |
pending.push( parser.assignTexture( materialParams, 'anisotropyMap', extension.anisotropyTexture ) ); | |
} | |
return Promise.all( pending ); | |
} | |
} | |
/** | |
* BasisU Texture Extension | |
* | |
* Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_basisu | |
*/ | |
class GLTFTextureBasisUExtension { | |
constructor( parser ) { | |
this.parser = parser; | |
this.name = EXTENSIONS.KHR_TEXTURE_BASISU; | |
} | |
loadTexture( textureIndex ) { | |
const parser = this.parser; | |
const json = parser.json; | |
const textureDef = json.textures[ textureIndex ]; | |
if ( ! textureDef.extensions || ! textureDef.extensions[ this.name ] ) { | |
return null; | |
} | |
const extension = textureDef.extensions[ this.name ]; | |
const loader = parser.options.ktx2Loader; | |
if ( ! loader ) { | |
if ( json.extensionsRequired && json.extensionsRequired.indexOf( this.name ) >= 0 ) { | |
throw new Error( 'THREE.GLTFLoader: setKTX2Loader must be called before loading KTX2 textures' ); | |
} else { | |
// Assumes that the extension is optional and that a fallback texture is present | |
return null; | |
} | |
} | |
return parser.loadTextureImage( textureIndex, extension.source, loader ); | |
} | |
} | |
/** | |
* WebP Texture Extension | |
* | |
* Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_texture_webp | |
*/ | |
class GLTFTextureWebPExtension { | |
constructor( parser ) { | |
this.parser = parser; | |
this.name = EXTENSIONS.EXT_TEXTURE_WEBP; | |
this.isSupported = null; | |
} | |
loadTexture( textureIndex ) { | |
const name = this.name; | |
const parser = this.parser; | |
const json = parser.json; | |
const textureDef = json.textures[ textureIndex ]; | |
if ( ! textureDef.extensions || ! textureDef.extensions[ name ] ) { | |
return null; | |
} | |
const extension = textureDef.extensions[ name ]; | |
const source = json.images[ extension.source ]; | |
let loader = parser.textureLoader; | |
if ( source.uri ) { | |
const handler = parser.options.manager.getHandler( source.uri ); | |
if ( handler !== null ) loader = handler; | |
} | |
return this.detectSupport().then( function ( isSupported ) { | |
if ( isSupported ) return parser.loadTextureImage( textureIndex, extension.source, loader ); | |
if ( json.extensionsRequired && json.extensionsRequired.indexOf( name ) >= 0 ) { | |
throw new Error( 'THREE.GLTFLoader: WebP required by asset but unsupported.' ); | |
} | |
// Fall back to PNG or JPEG. | |
return parser.loadTexture( textureIndex ); | |
} ); | |
} | |
detectSupport() { | |
if ( ! this.isSupported ) { | |
this.isSupported = new Promise( function ( resolve ) { | |
const image = new Image(); | |
// Lossy test image. Support for lossy images doesn't guarantee support for all | |
// WebP images, unfortunately. | |
image.src = 'data:image/webp;base64,UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA'; | |
image.onload = image.onerror = function () { | |
resolve( image.height === 1 ); | |
}; | |
} ); | |
} | |
return this.isSupported; | |
} | |
} | |
/** | |
* AVIF Texture Extension | |
* | |
* Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_texture_avif | |
*/ | |
class GLTFTextureAVIFExtension { | |
constructor( parser ) { | |
this.parser = parser; | |
this.name = EXTENSIONS.EXT_TEXTURE_AVIF; | |
this.isSupported = null; | |
} | |
loadTexture( textureIndex ) { | |
const name = this.name; | |
const parser = this.parser; | |
const json = parser.json; | |
const textureDef = json.textures[ textureIndex ]; | |
if ( ! textureDef.extensions || ! textureDef.extensions[ name ] ) { | |
return null; | |
} | |
const extension = textureDef.extensions[ name ]; | |
const source = json.images[ extension.source ]; | |
let loader = parser.textureLoader; | |
if ( source.uri ) { | |
const handler = parser.options.manager.getHandler( source.uri ); | |
if ( handler !== null ) loader = handler; | |
} | |
return this.detectSupport().then( function ( isSupported ) { | |
if ( isSupported ) return parser.loadTextureImage( textureIndex, extension.source, loader ); | |
if ( json.extensionsRequired && json.extensionsRequired.indexOf( name ) >= 0 ) { | |
throw new Error( 'THREE.GLTFLoader: AVIF required by asset but unsupported.' ); | |
} | |
// Fall back to PNG or JPEG. | |
return parser.loadTexture( textureIndex ); | |
} ); | |
} | |
detectSupport() { | |
if ( ! this.isSupported ) { | |
this.isSupported = new Promise( function ( resolve ) { | |
const image = new Image(); | |
// Lossy test image. | |
image.src = 'data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAABcAAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAEAAAABAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQAMAAAAABNjb2xybmNseAACAAIABoAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAAB9tZGF0EgAKCBgABogQEDQgMgkQAAAAB8dSLfI='; | |
image.onload = image.onerror = function () { | |
resolve( image.height === 1 ); | |
}; | |
} ); | |
} | |
return this.isSupported; | |
} | |
} | |
/** | |
* meshopt BufferView Compression Extension | |
* | |
* Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_meshopt_compression | |
*/ | |
class GLTFMeshoptCompression { | |
constructor( parser ) { | |
this.name = EXTENSIONS.EXT_MESHOPT_COMPRESSION; | |
this.parser = parser; | |
} | |
loadBufferView( index ) { | |
const json = this.parser.json; | |
const bufferView = json.bufferViews[ index ]; | |
if ( bufferView.extensions && bufferView.extensions[ this.name ] ) { | |
const extensionDef = bufferView.extensions[ this.name ]; | |
const buffer = this.parser.getDependency( 'buffer', extensionDef.buffer ); | |
const decoder = this.parser.options.meshoptDecoder; | |
if ( ! decoder || ! decoder.supported ) { | |
if ( json.extensionsRequired && json.extensionsRequired.indexOf( this.name ) >= 0 ) { | |
throw new Error( 'THREE.GLTFLoader: setMeshoptDecoder must be called before loading compressed files' ); | |
} else { | |
// Assumes that the extension is optional and that fallback buffer data is present | |
return null; | |
} | |
} | |
return buffer.then( function ( res ) { | |
const byteOffset = extensionDef.byteOffset || 0; | |
const byteLength = extensionDef.byteLength || 0; | |
const count = extensionDef.count; | |
const stride = extensionDef.byteStride; | |
const source = new Uint8Array( res, byteOffset, byteLength ); | |
if ( decoder.decodeGltfBufferAsync ) { | |
return decoder.decodeGltfBufferAsync( count, stride, source, extensionDef.mode, extensionDef.filter ).then( function ( res ) { | |
return res.buffer; | |
} ); | |
} else { | |
// Support for MeshoptDecoder 0.18 or earlier, without decodeGltfBufferAsync | |
return decoder.ready.then( function () { | |
const result = new ArrayBuffer( count * stride ); | |
decoder.decodeGltfBuffer( new Uint8Array( result ), count, stride, source, extensionDef.mode, extensionDef.filter ); | |
return result; | |
} ); | |
} | |
} ); | |
} else { | |
return null; | |
} | |
} | |
} | |
/** | |
* GPU Instancing Extension | |
* | |
* Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_mesh_gpu_instancing | |
* | |
*/ | |
class GLTFMeshGpuInstancing { | |
constructor( parser ) { | |
this.name = EXTENSIONS.EXT_MESH_GPU_INSTANCING; | |
this.parser = parser; | |
} | |
createNodeMesh( nodeIndex ) { | |
const json = this.parser.json; | |
const nodeDef = json.nodes[ nodeIndex ]; | |
if ( ! nodeDef.extensions || ! nodeDef.extensions[ this.name ] || | |
nodeDef.mesh === undefined ) { | |
return null; | |
} | |
const meshDef = json.meshes[ nodeDef.mesh ]; | |
// No Points or Lines + Instancing support yet | |
for ( const primitive of meshDef.primitives ) { | |
if ( primitive.mode !== WEBGL_CONSTANTS.TRIANGLES && | |
primitive.mode !== WEBGL_CONSTANTS.TRIANGLE_STRIP && | |
primitive.mode !== WEBGL_CONSTANTS.TRIANGLE_FAN && | |
primitive.mode !== undefined ) { | |
return null; | |
} | |
} | |
const extensionDef = nodeDef.extensions[ this.name ]; | |
const attributesDef = extensionDef.attributes; | |
// @TODO: Can we support InstancedMesh + SkinnedMesh? | |
const pending = []; | |
const attributes = {}; | |
for ( const key in attributesDef ) { | |
pending.push( this.parser.getDependency( 'accessor', attributesDef[ key ] ).then( accessor => { | |
attributes[ key ] = accessor; | |
return attributes[ key ]; | |
} ) ); | |
} | |
if ( pending.length < 1 ) { | |
return null; | |
} | |
pending.push( this.parser.createNodeMesh( nodeIndex ) ); | |
return Promise.all( pending ).then( results => { | |
const nodeObject = results.pop(); | |
const meshes = nodeObject.isGroup ? nodeObject.children : [ nodeObject ]; | |
const count = results[ 0 ].count; // All attribute counts should be same | |
const instancedMeshes = []; | |
for ( const mesh of meshes ) { | |
// Temporal variables | |
const m = new Matrix4(); | |
const p = new Vector3(); | |
const q = new Quaternion(); | |
const s = new Vector3( 1, 1, 1 ); | |
const instancedMesh = new InstancedMesh( mesh.geometry, mesh.material, count ); | |
for ( let i = 0; i < count; i ++ ) { | |
if ( attributes.TRANSLATION ) { | |
p.fromBufferAttribute( attributes.TRANSLATION, i ); | |
} | |
if ( attributes.ROTATION ) { | |
q.fromBufferAttribute( attributes.ROTATION, i ); | |
} | |
if ( attributes.SCALE ) { | |
s.fromBufferAttribute( attributes.SCALE, i ); | |
} | |
instancedMesh.setMatrixAt( i, m.compose( p, q, s ) ); | |
} | |
// Add instance attributes to the geometry, excluding TRS. | |
for ( const attributeName in attributes ) { | |
if ( attributeName === '_COLOR_0' ) { | |
const attr = attributes[ attributeName ]; | |
instancedMesh.instanceColor = new InstancedBufferAttribute( attr.array, attr.itemSize, attr.normalized ); | |
} else if ( attributeName !== 'TRANSLATION' && | |
attributeName !== 'ROTATION' && | |
attributeName !== 'SCALE' ) { | |
mesh.geometry.setAttribute( attributeName, attributes[ attributeName ] ); | |
} | |
} | |
// Just in case | |
Object3D.prototype.copy.call( instancedMesh, mesh ); | |
this.parser.assignFinalMaterial( instancedMesh ); | |
instancedMeshes.push( instancedMesh ); | |
} | |
if ( nodeObject.isGroup ) { | |
nodeObject.clear(); | |
nodeObject.add( ... instancedMeshes ); | |
return nodeObject; | |
} | |
return instancedMeshes[ 0 ]; | |
} ); | |
} | |
} | |
/* BINARY EXTENSION */ | |
const BINARY_EXTENSION_HEADER_MAGIC = 'glTF'; | |
const BINARY_EXTENSION_HEADER_LENGTH = 12; | |
const BINARY_EXTENSION_CHUNK_TYPES = { JSON: 0x4E4F534A, BIN: 0x004E4942 }; | |
class GLTFBinaryExtension { | |
constructor( data ) { | |
this.name = EXTENSIONS.KHR_BINARY_GLTF; | |
this.content = null; | |
this.body = null; | |
const headerView = new DataView( data, 0, BINARY_EXTENSION_HEADER_LENGTH ); | |
const textDecoder = new TextDecoder(); | |
this.header = { | |
magic: textDecoder.decode( new Uint8Array( data.slice( 0, 4 ) ) ), | |
version: headerView.getUint32( 4, true ), | |
length: headerView.getUint32( 8, true ) | |
}; | |
if ( this.header.magic !== BINARY_EXTENSION_HEADER_MAGIC ) { | |
throw new Error( 'THREE.GLTFLoader: Unsupported glTF-Binary header.' ); | |
} else if ( this.header.version < 2.0 ) { | |
throw new Error( 'THREE.GLTFLoader: Legacy binary file detected.' ); | |
} | |
const chunkContentsLength = this.header.length - BINARY_EXTENSION_HEADER_LENGTH; | |
const chunkView = new DataView( data, BINARY_EXTENSION_HEADER_LENGTH ); | |
let chunkIndex = 0; | |
while ( chunkIndex < chunkContentsLength ) { | |
const chunkLength = chunkView.getUint32( chunkIndex, true ); | |
chunkIndex += 4; | |
const chunkType = chunkView.getUint32( chunkIndex, true ); | |
chunkIndex += 4; | |
if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.JSON ) { | |
const contentArray = new Uint8Array( data, BINARY_EXTENSION_HEADER_LENGTH + chunkIndex, chunkLength ); | |
this.content = textDecoder.decode( contentArray ); | |
} else if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.BIN ) { | |
const byteOffset = BINARY_EXTENSION_HEADER_LENGTH + chunkIndex; | |
this.body = data.slice( byteOffset, byteOffset + chunkLength ); | |
} | |
// Clients must ignore chunks with unknown types. | |
chunkIndex += chunkLength; | |
} | |
if ( this.content === null ) { | |
throw new Error( 'THREE.GLTFLoader: JSON content not found.' ); | |
} | |
} | |
} | |
/** | |
* DRACO Mesh Compression Extension | |
* | |
* Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_draco_mesh_compression | |
*/ | |
class GLTFDracoMeshCompressionExtension { | |
constructor( json, dracoLoader ) { | |
if ( ! dracoLoader ) { | |
throw new Error( 'THREE.GLTFLoader: No DRACOLoader instance provided.' ); | |
} | |
this.name = EXTENSIONS.KHR_DRACO_MESH_COMPRESSION; | |
this.json = json; | |
this.dracoLoader = dracoLoader; | |
this.dracoLoader.preload(); | |
} | |
decodePrimitive( primitive, parser ) { | |
const json = this.json; | |
const dracoLoader = this.dracoLoader; | |
const bufferViewIndex = primitive.extensions[ this.name ].bufferView; | |
const gltfAttributeMap = primitive.extensions[ this.name ].attributes; | |
const threeAttributeMap = {}; | |
const attributeNormalizedMap = {}; | |
const attributeTypeMap = {}; | |
for ( const attributeName in gltfAttributeMap ) { | |
const threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase(); | |
threeAttributeMap[ threeAttributeName ] = gltfAttributeMap[ attributeName ]; | |
} | |
for ( const attributeName in primitive.attributes ) { | |
const threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase(); | |
if ( gltfAttributeMap[ attributeName ] !== undefined ) { | |
const accessorDef = json.accessors[ primitive.attributes[ attributeName ] ]; | |
const componentType = WEBGL_COMPONENT_TYPES[ accessorDef.componentType ]; | |
attributeTypeMap[ threeAttributeName ] = componentType.name; | |
attributeNormalizedMap[ threeAttributeName ] = accessorDef.normalized === true; | |
} | |
} | |
return parser.getDependency( 'bufferView', bufferViewIndex ).then( function ( bufferView ) { | |
return new Promise( function ( resolve, reject ) { | |
dracoLoader.decodeDracoFile( bufferView, function ( geometry ) { | |
for ( const attributeName in geometry.attributes ) { | |
const attribute = geometry.attributes[ attributeName ]; | |
const normalized = attributeNormalizedMap[ attributeName ]; | |
if ( normalized !== undefined ) attribute.normalized = normalized; | |
} | |
resolve( geometry ); | |
}, threeAttributeMap, attributeTypeMap, LinearSRGBColorSpace, reject ); | |
} ); | |
} ); | |
} | |
} | |
/** | |
* Texture Transform Extension | |
* | |
* Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_transform | |
*/ | |
class GLTFTextureTransformExtension { | |
constructor() { | |
this.name = EXTENSIONS.KHR_TEXTURE_TRANSFORM; | |
} | |
extendTexture( texture, transform ) { | |
if ( ( transform.texCoord === undefined || transform.texCoord === texture.channel ) | |
&& transform.offset === undefined | |
&& transform.rotation === undefined | |
&& transform.scale === undefined ) { | |
// See https://github.com/mrdoob/three.js/issues/21819. | |
return texture; | |
} | |
texture = texture.clone(); | |
if ( transform.texCoord !== undefined ) { | |
texture.channel = transform.texCoord; | |
} | |
if ( transform.offset !== undefined ) { | |
texture.offset.fromArray( transform.offset ); | |
} | |
if ( transform.rotation !== undefined ) { | |
texture.rotation = transform.rotation; | |
} | |
if ( transform.scale !== undefined ) { | |
texture.repeat.fromArray( transform.scale ); | |
} | |
texture.needsUpdate = true; | |
return texture; | |
} | |
} | |
/** | |
* Mesh Quantization Extension | |
* | |
* Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_mesh_quantization | |
*/ | |
class GLTFMeshQuantizationExtension { | |
constructor() { | |
this.name = EXTENSIONS.KHR_MESH_QUANTIZATION; | |
} | |
} | |
/*********************************/ | |
/********** INTERPOLATION ********/ | |
/*********************************/ | |
// Spline Interpolation | |
// Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#appendix-c-spline-interpolation | |
class GLTFCubicSplineInterpolant extends Interpolant { | |
constructor( parameterPositions, sampleValues, sampleSize, resultBuffer ) { | |
super( parameterPositions, sampleValues, sampleSize, resultBuffer ); | |
} | |
copySampleValue_( index ) { | |
// Copies a sample value to the result buffer. See description of glTF | |
// CUBICSPLINE values layout in interpolate_() function below. | |
const result = this.resultBuffer, | |
values = this.sampleValues, | |
valueSize = this.valueSize, | |
offset = index * valueSize * 3 + valueSize; | |
for ( let i = 0; i !== valueSize; i ++ ) { | |
result[ i ] = values[ offset + i ]; | |
} | |
return result; | |
} | |
interpolate_( i1, t0, t, t1 ) { | |
const result = this.resultBuffer; | |
const values = this.sampleValues; | |
const stride = this.valueSize; | |
const stride2 = stride * 2; | |
const stride3 = stride * 3; | |
const td = t1 - t0; | |
const p = ( t - t0 ) / td; | |
const pp = p * p; | |
const ppp = pp * p; | |
const offset1 = i1 * stride3; | |
const offset0 = offset1 - stride3; | |
const s2 = - 2 * ppp + 3 * pp; | |
const s3 = ppp - pp; | |
const s0 = 1 - s2; | |
const s1 = s3 - pp + p; | |
// Layout of keyframe output values for CUBICSPLINE animations: | |
// [ inTangent_1, splineVertex_1, outTangent_1, inTangent_2, splineVertex_2, ... ] | |
for ( let i = 0; i !== stride; i ++ ) { | |
const p0 = values[ offset0 + i + stride ]; // splineVertex_k | |
const m0 = values[ offset0 + i + stride2 ] * td; // outTangent_k * (t_k+1 - t_k) | |
const p1 = values[ offset1 + i + stride ]; // splineVertex_k+1 | |
const m1 = values[ offset1 + i ] * td; // inTangent_k+1 * (t_k+1 - t_k) | |
result[ i ] = s0 * p0 + s1 * m0 + s2 * p1 + s3 * m1; | |
} | |
return result; | |
} | |
} | |
const _q = new Quaternion(); | |
class GLTFCubicSplineQuaternionInterpolant extends GLTFCubicSplineInterpolant { | |
interpolate_( i1, t0, t, t1 ) { | |
const result = super.interpolate_( i1, t0, t, t1 ); | |
_q.fromArray( result ).normalize().toArray( result ); | |
return result; | |
} | |
} | |
/*********************************/ | |
/********** INTERNALS ************/ | |
/*********************************/ | |
/* CONSTANTS */ | |
const WEBGL_CONSTANTS = { | |
FLOAT: 5126, | |
//FLOAT_MAT2: 35674, | |
FLOAT_MAT3: 35675, | |
FLOAT_MAT4: 35676, | |
FLOAT_VEC2: 35664, | |
FLOAT_VEC3: 35665, | |
FLOAT_VEC4: 35666, | |
LINEAR: 9729, | |
REPEAT: 10497, | |
SAMPLER_2D: 35678, | |
POINTS: 0, | |
LINES: 1, | |
LINE_LOOP: 2, | |
LINE_STRIP: 3, | |
TRIANGLES: 4, | |
TRIANGLE_STRIP: 5, | |
TRIANGLE_FAN: 6, | |
UNSIGNED_BYTE: 5121, | |
UNSIGNED_SHORT: 5123 | |
}; | |
const WEBGL_COMPONENT_TYPES = { | |
5120: Int8Array, | |
5121: Uint8Array, | |
5122: Int16Array, | |
5123: Uint16Array, | |
5125: Uint32Array, | |
5126: Float32Array | |
}; | |
const WEBGL_FILTERS = { | |
9728: NearestFilter, | |
9729: LinearFilter, | |
9984: NearestMipmapNearestFilter, | |
9985: LinearMipmapNearestFilter, | |
9986: NearestMipmapLinearFilter, | |
9987: LinearMipmapLinearFilter | |
}; | |
const WEBGL_WRAPPINGS = { | |
33071: ClampToEdgeWrapping, | |
33648: MirroredRepeatWrapping, | |
10497: RepeatWrapping | |
}; | |
const WEBGL_TYPE_SIZES = { | |
'SCALAR': 1, | |
'VEC2': 2, | |
'VEC3': 3, | |
'VEC4': 4, | |
'MAT2': 4, | |
'MAT3': 9, | |
'MAT4': 16 | |
}; | |
const ATTRIBUTES = { | |
POSITION: 'position', | |
NORMAL: 'normal', | |
TANGENT: 'tangent', | |
TEXCOORD_0: 'uv', | |
TEXCOORD_1: 'uv1', | |
TEXCOORD_2: 'uv2', | |
TEXCOORD_3: 'uv3', | |
COLOR_0: 'color', | |
WEIGHTS_0: 'skinWeight', | |
JOINTS_0: 'skinIndex', | |
}; | |
const PATH_PROPERTIES = { | |
scale: 'scale', | |
translation: 'position', | |
rotation: 'quaternion', | |
weights: 'morphTargetInfluences' | |
}; | |
const INTERPOLATION = { | |
CUBICSPLINE: undefined, // We use a custom interpolant (GLTFCubicSplineInterpolation) for CUBICSPLINE tracks. Each | |
// keyframe track will be initialized with a default interpolation type, then modified. | |
LINEAR: InterpolateLinear, | |
STEP: InterpolateDiscrete | |
}; | |
const ALPHA_MODES = { | |
OPAQUE: 'OPAQUE', | |
MASK: 'MASK', | |
BLEND: 'BLEND' | |
}; | |
/** | |
* Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#default-material | |
* | |
* @param {Object<String, Material>} cache | |
* @return {Material} | |
*/ | |
function createDefaultMaterial( cache ) { | |
if ( cache[ 'DefaultMaterial' ] === undefined ) { | |
cache[ 'DefaultMaterial' ] = new MeshStandardMaterial( { | |
color: 0xFFFFFF, | |
emissive: 0x000000, | |
metalness: 1, | |
roughness: 1, | |
transparent: false, | |
depthTest: true, | |
side: FrontSide | |
} ); | |
} | |
return cache[ 'DefaultMaterial' ]; | |
} | |
function addUnknownExtensionsToUserData( knownExtensions, object, objectDef ) { | |
// Add unknown glTF extensions to an object's userData. | |
for ( const name in objectDef.extensions ) { | |
if ( knownExtensions[ name ] === undefined ) { | |
object.userData.gltfExtensions = object.userData.gltfExtensions || {}; | |
object.userData.gltfExtensions[ name ] = objectDef.extensions[ name ]; | |
} | |
} | |
} | |
/** | |
* @param {Object3D|Material|BufferGeometry} object | |
* @param {GLTF.definition} gltfDef | |
*/ | |
function assignExtrasToUserData( object, gltfDef ) { | |
if ( gltfDef.extras !== undefined ) { | |
if ( typeof gltfDef.extras === 'object' ) { | |
Object.assign( object.userData, gltfDef.extras ); | |
} else { | |
console.warn( 'THREE.GLTFLoader: Ignoring primitive type .extras, ' + gltfDef.extras ); | |
} | |
} | |
} | |
/** | |
* Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#morph-targets | |
* | |
* @param {BufferGeometry} geometry | |
* @param {Array<GLTF.Target>} targets | |
* @param {GLTFParser} parser | |
* @return {Promise<BufferGeometry>} | |
*/ | |
function addMorphTargets( geometry, targets, parser ) { | |
let hasMorphPosition = false; | |
let hasMorphNormal = false; | |
let hasMorphColor = false; | |
for ( let i = 0, il = targets.length; i < il; i ++ ) { | |
const target = targets[ i ]; | |
if ( target.POSITION !== undefined ) hasMorphPosition = true; | |
if ( target.NORMAL !== undefined ) hasMorphNormal = true; | |
if ( target.COLOR_0 !== undefined ) hasMorphColor = true; | |
if ( hasMorphPosition && hasMorphNormal && hasMorphColor ) break; | |
} | |
if ( ! hasMorphPosition && ! hasMorphNormal && ! hasMorphColor ) return Promise.resolve( geometry ); | |
const pendingPositionAccessors = []; | |
const pendingNormalAccessors = []; | |
const pendingColorAccessors = []; | |
for ( let i = 0, il = targets.length; i < il; i ++ ) { | |
const target = targets[ i ]; | |
if ( hasMorphPosition ) { | |
const pendingAccessor = target.POSITION !== undefined | |
? parser.getDependency( 'accessor', target.POSITION ) | |
: geometry.attributes.position; | |
pendingPositionAccessors.push( pendingAccessor ); | |
} | |
if ( hasMorphNormal ) { | |
const pendingAccessor = target.NORMAL !== undefined | |
? parser.getDependency( 'accessor', target.NORMAL ) | |
: geometry.attributes.normal; | |
pendingNormalAccessors.push( pendingAccessor ); | |
} | |
if ( hasMorphColor ) { | |
const pendingAccessor = target.COLOR_0 !== undefined | |
? parser.getDependency( 'accessor', target.COLOR_0 ) | |
: geometry.attributes.color; | |
pendingColorAccessors.push( pendingAccessor ); | |
} | |
} | |
return Promise.all( [ | |
Promise.all( pendingPositionAccessors ), | |
Promise.all( pendingNormalAccessors ), | |
Promise.all( pendingColorAccessors ) | |
] ).then( function ( accessors ) { | |
const morphPositions = accessors[ 0 ]; | |
const morphNormals = accessors[ 1 ]; | |
const morphColors = accessors[ 2 ]; | |
if ( hasMorphPosition ) geometry.morphAttributes.position = morphPositions; | |
if ( hasMorphNormal ) geometry.morphAttributes.normal = morphNormals; | |
if ( hasMorphColor ) geometry.morphAttributes.color = morphColors; | |
geometry.morphTargetsRelative = true; | |
return geometry; | |
} ); | |
} | |
/** | |
* @param {Mesh} mesh | |
* @param {GLTF.Mesh} meshDef | |
*/ | |
function updateMorphTargets( mesh, meshDef ) { | |
mesh.updateMorphTargets(); | |
if ( meshDef.weights !== undefined ) { | |
for ( let i = 0, il = meshDef.weights.length; i < il; i ++ ) { | |
mesh.morphTargetInfluences[ i ] = meshDef.weights[ i ]; | |
} | |
} | |
// .extras has user-defined data, so check that .extras.targetNames is an array. | |
if ( meshDef.extras && Array.isArray( meshDef.extras.targetNames ) ) { | |
const targetNames = meshDef.extras.targetNames; | |
if ( mesh.morphTargetInfluences.length === targetNames.length ) { | |
mesh.morphTargetDictionary = {}; | |
for ( let i = 0, il = targetNames.length; i < il; i ++ ) { | |
mesh.morphTargetDictionary[ targetNames[ i ] ] = i; | |
} | |
} else { | |
console.warn( 'THREE.GLTFLoader: Invalid extras.targetNames length. Ignoring names.' ); | |
} | |
} | |
} | |
function createPrimitiveKey( primitiveDef ) { | |
let geometryKey; | |
const dracoExtension = primitiveDef.extensions && primitiveDef.extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ]; | |
if ( dracoExtension ) { | |
geometryKey = 'draco:' + dracoExtension.bufferView | |
+ ':' + dracoExtension.indices | |
+ ':' + createAttributesKey( dracoExtension.attributes ); | |
} else { | |
geometryKey = primitiveDef.indices + ':' + createAttributesKey( primitiveDef.attributes ) + ':' + primitiveDef.mode; | |
} | |
if ( primitiveDef.targets !== undefined ) { | |
for ( let i = 0, il = primitiveDef.targets.length; i < il; i ++ ) { | |
geometryKey += ':' + createAttributesKey( primitiveDef.targets[ i ] ); | |
} | |
} | |
return geometryKey; | |
} | |
function createAttributesKey( attributes ) { | |
let attributesKey = ''; | |
const keys = Object.keys( attributes ).sort(); | |
for ( let i = 0, il = keys.length; i < il; i ++ ) { | |
attributesKey += keys[ i ] + ':' + attributes[ keys[ i ] ] + ';'; | |
} | |
return attributesKey; | |
} | |
function getNormalizedComponentScale( constructor ) { | |
// Reference: | |
// https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_mesh_quantization#encoding-quantized-data | |
switch ( constructor ) { | |
case Int8Array: | |
return 1 / 127; | |
case Uint8Array: | |
return 1 / 255; | |
case Int16Array: | |
return 1 / 32767; | |
case Uint16Array: | |
return 1 / 65535; | |
default: | |
throw new Error( 'THREE.GLTFLoader: Unsupported normalized accessor component type.' ); | |
} | |
} | |
function getImageURIMimeType( uri ) { | |
if ( uri.search( /\.jpe?g($|\?)/i ) > 0 || uri.search( /^data\:image\/jpeg/ ) === 0 ) return 'image/jpeg'; | |
if ( uri.search( /\.webp($|\?)/i ) > 0 || uri.search( /^data\:image\/webp/ ) === 0 ) return 'image/webp'; | |
if ( uri.search( /\.ktx2($|\?)/i ) > 0 || uri.search( /^data\:image\/ktx2/ ) === 0 ) return 'image/ktx2'; | |
return 'image/png'; | |
} | |
const _identityMatrix = new Matrix4(); | |
/* GLTF PARSER */ | |
class GLTFParser { | |
constructor( json = {}, options = {} ) { | |
this.json = json; | |
this.extensions = {}; | |
this.plugins = {}; | |
this.options = options; | |
// loader object cache | |
this.cache = new GLTFRegistry(); | |
// associations between Three.js objects and glTF elements | |
this.associations = new Map(); | |
// BufferGeometry caching | |
this.primitiveCache = {}; | |
// Node cache | |
this.nodeCache = {}; | |
// Object3D instance caches | |
this.meshCache = { refs: {}, uses: {} }; | |
this.cameraCache = { refs: {}, uses: {} }; | |
this.lightCache = { refs: {}, uses: {} }; | |
this.sourceCache = {}; | |
this.textureCache = {}; | |
// Track node names, to ensure no duplicates | |
this.nodeNamesUsed = {}; | |
// Use an ImageBitmapLoader if imageBitmaps are supported. Moves much of the | |
// expensive work of uploading a texture to the GPU off the main thread. | |
let isSafari = false; | |
let safariVersion = - 1; | |
let isFirefox = false; | |
let firefoxVersion = - 1; | |
if ( typeof navigator !== 'undefined' ) { | |
const userAgent = navigator.userAgent; | |
isSafari = /^((?!chrome|android).)*safari/i.test( userAgent ) === true; | |
const safariMatch = userAgent.match( /Version\/(\d+)/ ); | |
safariVersion = isSafari && safariMatch ? parseInt( safariMatch[ 1 ], 10 ) : - 1; | |
isFirefox = userAgent.indexOf( 'Firefox' ) > - 1; | |
firefoxVersion = isFirefox ? userAgent.match( /Firefox\/([0-9]+)\./ )[ 1 ] : - 1; | |
} | |
if ( typeof createImageBitmap === 'undefined' || ( isSafari && safariVersion < 17 ) || ( isFirefox && firefoxVersion < 98 ) ) { | |
this.textureLoader = new TextureLoader( this.options.manager ); | |
} else { | |
this.textureLoader = new ImageBitmapLoader( this.options.manager ); | |
} | |
this.textureLoader.setCrossOrigin( this.options.crossOrigin ); | |
this.textureLoader.setRequestHeader( this.options.requestHeader ); | |
this.fileLoader = new FileLoader( this.options.manager ); | |
this.fileLoader.setResponseType( 'arraybuffer' ); | |
if ( this.options.crossOrigin === 'use-credentials' ) { | |
this.fileLoader.setWithCredentials( true ); | |
} | |
} | |
setExtensions( extensions ) { | |
this.extensions = extensions; | |
} | |
setPlugins( plugins ) { | |
this.plugins = plugins; | |
} | |
parse( onLoad, onError ) { | |
const parser = this; | |
const json = this.json; | |
const extensions = this.extensions; | |
// Clear the loader cache | |
this.cache.removeAll(); | |
this.nodeCache = {}; | |
// Mark the special nodes/meshes in json for efficient parse | |
this._invokeAll( function ( ext ) { | |
return ext._markDefs && ext._markDefs(); | |
} ); | |
Promise.all( this._invokeAll( function ( ext ) { | |
return ext.beforeRoot && ext.beforeRoot(); | |
} ) ).then( function () { | |
return Promise.all( [ | |
parser.getDependencies( 'scene' ), | |
parser.getDependencies( 'animation' ), | |
parser.getDependencies( 'camera' ), | |
] ); | |
} ).then( function ( dependencies ) { | |
const result = { | |
scene: dependencies[ 0 ][ json.scene || 0 ], | |
scenes: dependencies[ 0 ], | |
animations: dependencies[ 1 ], | |
cameras: dependencies[ 2 ], | |
asset: json.asset, | |
parser: parser, | |
userData: {} | |
}; | |
addUnknownExtensionsToUserData( extensions, result, json ); | |
assignExtrasToUserData( result, json ); | |
return Promise.all( parser._invokeAll( function ( ext ) { | |
return ext.afterRoot && ext.afterRoot( result ); | |
} ) ).then( function () { | |
for ( const scene of result.scenes ) { | |
scene.updateMatrixWorld(); | |
} | |
onLoad( result ); | |
} ); | |
} ).catch( onError ); | |
} | |
/** | |
* Marks the special nodes/meshes in json for efficient parse. | |
*/ | |
_markDefs() { | |
const nodeDefs = this.json.nodes || []; | |
const skinDefs = this.json.skins || []; | |
const meshDefs = this.json.meshes || []; | |
// Nothing in the node definition indicates whether it is a Bone or an | |
// Object3D. Use the skins' joint references to mark bones. | |
for ( let skinIndex = 0, skinLength = skinDefs.length; skinIndex < skinLength; skinIndex ++ ) { | |
const joints = skinDefs[ skinIndex ].joints; | |
for ( let i = 0, il = joints.length; i < il; i ++ ) { | |
nodeDefs[ joints[ i ] ].isBone = true; | |
} | |
} | |
// Iterate over all nodes, marking references to shared resources, | |
// as well as skeleton joints. | |
for ( let nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex ++ ) { | |
const nodeDef = nodeDefs[ nodeIndex ]; | |
if ( nodeDef.mesh !== undefined ) { | |
this._addNodeRef( this.meshCache, nodeDef.mesh ); | |
// Nothing in the mesh definition indicates whether it is | |
// a SkinnedMesh or Mesh. Use the node's mesh reference | |
// to mark SkinnedMesh if node has skin. | |
if ( nodeDef.skin !== undefined ) { | |
meshDefs[ nodeDef.mesh ].isSkinnedMesh = true; | |
} | |
} | |
if ( nodeDef.camera !== undefined ) { | |
this._addNodeRef( this.cameraCache, nodeDef.camera ); | |
} | |
} | |
} | |
/** | |
* Counts references to shared node / Object3D resources. These resources | |
* can be reused, or "instantiated", at multiple nodes in the scene | |
* hierarchy. Mesh, Camera, and Light instances are instantiated and must | |
* be marked. Non-scenegraph resources (like Materials, Geometries, and | |
* Textures) can be reused directly and are not marked here. | |
* | |
* Example: CesiumMilkTruck sample model reuses "Wheel" meshes. | |
* | |
* @param {Object} cache | |
* @param {Object3D} index | |
*/ | |
_addNodeRef( cache, index ) { | |
if ( index === undefined ) return; | |
if ( cache.refs[ index ] === undefined ) { | |
cache.refs[ index ] = cache.uses[ index ] = 0; | |
} | |
cache.refs[ index ] ++; | |
} | |
/** | |
* Returns a reference to a shared resource, cloning it if necessary. | |
* | |
* @param {Object} cache | |
* @param {Number} index | |
* @param {Object} object | |
* @return {Object} | |
*/ | |
_getNodeRef( cache, index, object ) { | |
if ( cache.refs[ index ] <= 1 ) return object; | |
const ref = object.clone(); | |
// Propagates mappings to the cloned object, prevents mappings on the | |
// original object from being lost. | |
const updateMappings = ( original, clone ) => { | |
const mappings = this.associations.get( original ); | |
if ( mappings != null ) { | |
this.associations.set( clone, mappings ); | |
} | |
for ( const [ i, child ] of original.children.entries() ) { | |
updateMappings( child, clone.children[ i ] ); | |
} | |
}; | |
updateMappings( object, ref ); | |
ref.name += '_instance_' + ( cache.uses[ index ] ++ ); | |
return ref; | |
} | |
_invokeOne( func ) { | |
const extensions = Object.values( this.plugins ); | |
extensions.push( this ); | |
for ( let i = 0; i < extensions.length; i ++ ) { | |
const result = func( extensions[ i ] ); | |
if ( result ) return result; | |
} | |
return null; | |
} | |
_invokeAll( func ) { | |
const extensions = Object.values( this.plugins ); | |
extensions.unshift( this ); | |
const pending = []; | |
for ( let i = 0; i < extensions.length; i ++ ) { | |
const result = func( extensions[ i ] ); | |
if ( result ) pending.push( result ); | |
} | |
return pending; | |
} | |
/** | |
* Requests the specified dependency asynchronously, with caching. | |
* @param {string} type | |
* @param {number} index | |
* @return {Promise<Object3D|Material|THREE.Texture|AnimationClip|ArrayBuffer|Object>} | |
*/ | |
getDependency( type, index ) { | |
const cacheKey = type + ':' + index; | |
let dependency = this.cache.get( cacheKey ); | |
if ( ! dependency ) { | |
switch ( type ) { | |
case 'scene': | |
dependency = this.loadScene( index ); | |
break; | |
case 'node': | |
dependency = this._invokeOne( function ( ext ) { | |
return ext.loadNode && ext.loadNode( index ); | |
} ); | |
break; | |
case 'mesh': | |
dependency = this._invokeOne( function ( ext ) { | |
return ext.loadMesh && ext.loadMesh( index ); | |
} ); | |
break; | |
case 'accessor': | |
dependency = this.loadAccessor( index ); | |
break; | |
case 'bufferView': | |
dependency = this._invokeOne( function ( ext ) { | |
return ext.loadBufferView && ext.loadBufferView( index ); | |
} ); | |
break; | |
case 'buffer': | |
dependency = this.loadBuffer( index ); | |
break; | |
case 'material': | |
dependency = this._invokeOne( function ( ext ) { | |
return ext.loadMaterial && ext.loadMaterial( index ); | |
} ); | |
break; | |
case 'texture': | |
dependency = this._invokeOne( function ( ext ) { | |
return ext.loadTexture && ext.loadTexture( index ); | |
} ); | |
break; | |
case 'skin': | |
dependency = this.loadSkin( index ); | |
break; | |
case 'animation': | |
dependency = this._invokeOne( function ( ext ) { | |
return ext.loadAnimation && ext.loadAnimation( index ); | |
} ); | |
break; | |
case 'camera': | |
dependency = this.loadCamera( index ); | |
break; | |
default: | |
dependency = this._invokeOne( function ( ext ) { | |
return ext != this && ext.getDependency && ext.getDependency( type, index ); | |
} ); | |
if ( ! dependency ) { | |
throw new Error( 'Unknown type: ' + type ); | |
} | |
break; | |
} | |
this.cache.add( cacheKey, dependency ); | |
} | |
return dependency; | |
} | |
/** | |
* Requests all dependencies of the specified type asynchronously, with caching. | |
* @param {string} type | |
* @return {Promise<Array<Object>>} | |
*/ | |
getDependencies( type ) { | |
let dependencies = this.cache.get( type ); | |
if ( ! dependencies ) { | |
const parser = this; | |
const defs = this.json[ type + ( type === 'mesh' ? 'es' : 's' ) ] || []; | |
dependencies = Promise.all( defs.map( function ( def, index ) { | |
return parser.getDependency( type, index ); | |
} ) ); | |
this.cache.add( type, dependencies ); | |
} | |
return dependencies; | |
} | |
/** | |
* Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views | |
* @param {number} bufferIndex | |
* @return {Promise<ArrayBuffer>} | |
*/ | |
loadBuffer( bufferIndex ) { | |
const bufferDef = this.json.buffers[ bufferIndex ]; | |
const loader = this.fileLoader; | |
if ( bufferDef.type && bufferDef.type !== 'arraybuffer' ) { | |
throw new Error( 'THREE.GLTFLoader: ' + bufferDef.type + ' buffer type is not supported.' ); | |
} | |
// If present, GLB container is required to be the first buffer. | |
if ( bufferDef.uri === undefined && bufferIndex === 0 ) { | |
return Promise.resolve( this.extensions[ EXTENSIONS.KHR_BINARY_GLTF ].body ); | |
} | |
const options = this.options; | |
return new Promise( function ( resolve, reject ) { | |
loader.load( LoaderUtils.resolveURL( bufferDef.uri, options.path ), resolve, undefined, function () { | |
reject( new Error( 'THREE.GLTFLoader: Failed to load buffer "' + bufferDef.uri + '".' ) ); | |
} ); | |
} ); | |
} | |
/** | |
* Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views | |
* @param {number} bufferViewIndex | |
* @return {Promise<ArrayBuffer>} | |
*/ | |
loadBufferView( bufferViewIndex ) { | |
const bufferViewDef = this.json.bufferViews[ bufferViewIndex ]; | |
return this.getDependency( 'buffer', bufferViewDef.buffer ).then( function ( buffer ) { | |
const byteLength = bufferViewDef.byteLength || 0; | |
const byteOffset = bufferViewDef.byteOffset || 0; | |
return buffer.slice( byteOffset, byteOffset + byteLength ); | |
} ); | |
} | |
/** | |
* Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#accessors | |
* @param {number} accessorIndex | |
* @return {Promise<BufferAttribute|InterleavedBufferAttribute>} | |
*/ | |
loadAccessor( accessorIndex ) { | |
const parser = this; | |
const json = this.json; | |
const accessorDef = this.json.accessors[ accessorIndex ]; | |
if ( accessorDef.bufferView === undefined && accessorDef.sparse === undefined ) { | |
const itemSize = WEBGL_TYPE_SIZES[ accessorDef.type ]; | |
const TypedArray = WEBGL_COMPONENT_TYPES[ accessorDef.componentType ]; | |
const normalized = accessorDef.normalized === true; | |
const array = new TypedArray( accessorDef.count * itemSize ); | |
return Promise.resolve( new BufferAttribute( array, itemSize, normalized ) ); | |
} | |
const pendingBufferViews = []; | |
if ( accessorDef.bufferView !== undefined ) { | |
pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.bufferView ) ); | |
} else { | |
pendingBufferViews.push( null ); | |
} | |
if ( accessorDef.sparse !== undefined ) { | |
pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.sparse.indices.bufferView ) ); | |
pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.sparse.values.bufferView ) ); | |
} | |
return Promise.all( pendingBufferViews ).then( function ( bufferViews ) { | |
const bufferView = bufferViews[ 0 ]; | |
const itemSize = WEBGL_TYPE_SIZES[ accessorDef.type ]; | |
const TypedArray = WEBGL_COMPONENT_TYPES[ accessorDef.componentType ]; | |
// For VEC3: itemSize is 3, elementBytes is 4, itemBytes is 12. | |
const elementBytes = TypedArray.BYTES_PER_ELEMENT; | |
const itemBytes = elementBytes * itemSize; | |
const byteOffset = accessorDef.byteOffset || 0; | |
const byteStride = accessorDef.bufferView !== undefined ? json.bufferViews[ accessorDef.bufferView ].byteStride : undefined; | |
const normalized = accessorDef.normalized === true; | |
let array, bufferAttribute; | |
// The buffer is not interleaved if the stride is the item size in bytes. | |
if ( byteStride && byteStride !== itemBytes ) { | |
// Each "slice" of the buffer, as defined by 'count' elements of 'byteStride' bytes, gets its own InterleavedBuffer | |
// This makes sure that IBA.count reflects accessor.count properly | |
const ibSlice = Math.floor( byteOffset / byteStride ); | |
const ibCacheKey = 'InterleavedBuffer:' + accessorDef.bufferView + ':' + accessorDef.componentType + ':' + ibSlice + ':' + accessorDef.count; | |
let ib = parser.cache.get( ibCacheKey ); | |
if ( ! ib ) { | |
array = new TypedArray( bufferView, ibSlice * byteStride, accessorDef.count * byteStride / elementBytes ); | |
// Integer parameters to IB/IBA are in array elements, not bytes. | |
ib = new InterleavedBuffer( array, byteStride / elementBytes ); | |
parser.cache.add( ibCacheKey, ib ); | |
} | |
bufferAttribute = new InterleavedBufferAttribute( ib, itemSize, ( byteOffset % byteStride ) / elementBytes, normalized ); | |
} else { | |
if ( bufferView === null ) { | |
array = new TypedArray( accessorDef.count * itemSize ); | |
} else { | |
array = new TypedArray( bufferView, byteOffset, accessorDef.count * itemSize ); | |
} | |
bufferAttribute = new BufferAttribute( array, itemSize, normalized ); | |
} | |
// https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#sparse-accessors | |
if ( accessorDef.sparse !== undefined ) { | |
const itemSizeIndices = WEBGL_TYPE_SIZES.SCALAR; | |
const TypedArrayIndices = WEBGL_COMPONENT_TYPES[ accessorDef.sparse.indices.componentType ]; | |
const byteOffsetIndices = accessorDef.sparse.indices.byteOffset || 0; | |
const byteOffsetValues = accessorDef.sparse.values.byteOffset || 0; | |
const sparseIndices = new TypedArrayIndices( bufferViews[ 1 ], byteOffsetIndices, accessorDef.sparse.count * itemSizeIndices ); | |
const sparseValues = new TypedArray( bufferViews[ 2 ], byteOffsetValues, accessorDef.sparse.count * itemSize ); | |
if ( bufferView !== null ) { | |
// Avoid modifying the original ArrayBuffer, if the bufferView wasn't initialized with zeroes. | |
bufferAttribute = new BufferAttribute( bufferAttribute.array.slice(), bufferAttribute.itemSize, bufferAttribute.normalized ); | |
} | |
// Ignore normalized since we copy from sparse | |
bufferAttribute.normalized = false; | |
for ( let i = 0, il = sparseIndices.length; i < il; i ++ ) { | |
const index = sparseIndices[ i ]; | |
bufferAttribute.setX( index, sparseValues[ i * itemSize ] ); | |
if ( itemSize >= 2 ) bufferAttribute.setY( index, sparseValues[ i * itemSize + 1 ] ); | |
if ( itemSize >= 3 ) bufferAttribute.setZ( index, sparseValues[ i * itemSize + 2 ] ); | |
if ( itemSize >= 4 ) bufferAttribute.setW( index, sparseValues[ i * itemSize + 3 ] ); | |
if ( itemSize >= 5 ) throw new Error( 'THREE.GLTFLoader: Unsupported itemSize in sparse BufferAttribute.' ); | |
} | |
bufferAttribute.normalized = normalized; | |
} | |
return bufferAttribute; | |
} ); | |
} | |
/** | |
* Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#textures | |
* @param {number} textureIndex | |
* @return {Promise<THREE.Texture|null>} | |
*/ | |
loadTexture( textureIndex ) { | |
const json = this.json; | |
const options = this.options; | |
const textureDef = json.textures[ textureIndex ]; | |
const sourceIndex = textureDef.source; | |
const sourceDef = json.images[ sourceIndex ]; | |
let loader = this.textureLoader; | |
if ( sourceDef.uri ) { | |
const handler = options.manager.getHandler( sourceDef.uri ); | |
if ( handler !== null ) loader = handler; | |
} | |
return this.loadTextureImage( textureIndex, sourceIndex, loader ); | |
} | |
loadTextureImage( textureIndex, sourceIndex, loader ) { | |
const parser = this; | |
const json = this.json; | |
const textureDef = json.textures[ textureIndex ]; | |
const sourceDef = json.images[ sourceIndex ]; | |
const cacheKey = ( sourceDef.uri || sourceDef.bufferView ) + ':' + textureDef.sampler; | |
if ( this.textureCache[ cacheKey ] ) { | |
// See https://github.com/mrdoob/three.js/issues/21559. | |
return this.textureCache[ cacheKey ]; | |
} | |
const promise = this.loadImageSource( sourceIndex, loader ).then( function ( texture ) { | |
texture.flipY = false; | |
texture.name = textureDef.name || sourceDef.name || ''; | |
if ( texture.name === '' && typeof sourceDef.uri === 'string' && sourceDef.uri.startsWith( 'data:image/' ) === false ) { | |
texture.name = sourceDef.uri; | |
} | |
const samplers = json.samplers || {}; | |
const sampler = samplers[ textureDef.sampler ] || {}; | |
texture.magFilter = WEBGL_FILTERS[ sampler.magFilter ] || LinearFilter; | |
texture.minFilter = WEBGL_FILTERS[ sampler.minFilter ] || LinearMipmapLinearFilter; | |
texture.wrapS = WEBGL_WRAPPINGS[ sampler.wrapS ] || RepeatWrapping; | |
texture.wrapT = WEBGL_WRAPPINGS[ sampler.wrapT ] || RepeatWrapping; | |
texture.generateMipmaps = ! texture.isCompressedTexture && texture.minFilter !== NearestFilter && texture.minFilter !== LinearFilter; | |
parser.associations.set( texture, { textures: textureIndex } ); | |
return texture; | |
} ).catch( function () { | |
return null; | |
} ); | |
this.textureCache[ cacheKey ] = promise; | |
return promise; | |
} | |
loadImageSource( sourceIndex, loader ) { | |
const parser = this; | |
const json = this.json; | |
const options = this.options; | |
if ( this.sourceCache[ sourceIndex ] !== undefined ) { | |
return this.sourceCache[ sourceIndex ].then( ( texture ) => texture.clone() ); | |
} | |
const sourceDef = json.images[ sourceIndex ]; | |
const URL = self.URL || self.webkitURL; | |
let sourceURI = sourceDef.uri || ''; | |
let isObjectURL = false; | |
if ( sourceDef.bufferView !== undefined ) { | |
// Load binary image data from bufferView, if provided. | |
sourceURI = parser.getDependency( 'bufferView', sourceDef.bufferView ).then( function ( bufferView ) { | |
isObjectURL = true; | |
const blob = new Blob( [ bufferView ], { type: sourceDef.mimeType } ); | |
sourceURI = URL.createObjectURL( blob ); | |
return sourceURI; | |
} ); | |
} else if ( sourceDef.uri === undefined ) { | |
throw new Error( 'THREE.GLTFLoader: Image ' + sourceIndex + ' is missing URI and bufferView' ); | |
} | |
const promise = Promise.resolve( sourceURI ).then( function ( sourceURI ) { | |
return new Promise( function ( resolve, reject ) { | |
let onLoad = resolve; | |
if ( loader.isImageBitmapLoader === true ) { | |
onLoad = function ( imageBitmap ) { | |
const texture = new Texture( imageBitmap ); | |
texture.needsUpdate = true; | |
resolve( texture ); | |
}; | |
} | |
loader.load( LoaderUtils.resolveURL( sourceURI, options.path ), onLoad, undefined, reject ); | |
} ); | |
} ).then( function ( texture ) { | |
// Clean up resources and configure Texture. | |
if ( isObjectURL === true ) { | |
URL.revokeObjectURL( sourceURI ); | |
} | |
assignExtrasToUserData( texture, sourceDef ); | |
texture.userData.mimeType = sourceDef.mimeType || getImageURIMimeType( sourceDef.uri ); | |
return texture; | |
} ).catch( function ( error ) { | |
console.error( 'THREE.GLTFLoader: Couldn\'t load texture', sourceURI ); | |
throw error; | |
} ); | |
this.sourceCache[ sourceIndex ] = promise; | |
return promise; | |
} | |
/** | |
* Asynchronously assigns a texture to the given material parameters. | |
* | |
* @param {Object} materialParams | |
* @param {string} mapName | |
* @param {Object} mapDef | |
* @param {string} colorSpace | |
* @return {Promise<Texture>} | |
*/ | |
assignTexture( materialParams, mapName, mapDef, colorSpace ) { | |
const parser = this; | |
return this.getDependency( 'texture', mapDef.index ).then( function ( texture ) { | |
if ( ! texture ) return null; | |
if ( mapDef.texCoord !== undefined && mapDef.texCoord > 0 ) { | |
texture = texture.clone(); | |
texture.channel = mapDef.texCoord; | |
} | |
if ( parser.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ] ) { | |
const transform = mapDef.extensions !== undefined ? mapDef.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ] : undefined; | |
if ( transform ) { | |
const gltfReference = parser.associations.get( texture ); | |
texture = parser.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ].extendTexture( texture, transform ); | |
parser.associations.set( texture, gltfReference ); | |
} | |
} | |
if ( colorSpace !== undefined ) { | |
texture.colorSpace = colorSpace; | |
} | |
materialParams[ mapName ] = texture; | |
return texture; | |
} ); | |
} | |
/** | |
* Assigns final material to a Mesh, Line, or Points instance. The instance | |
* already has a material (generated from the glTF material options alone) | |
* but reuse of the same glTF material may require multiple threejs materials | |
* to accommodate different primitive types, defines, etc. New materials will | |
* be created if necessary, and reused from a cache. | |
* @param {Object3D} mesh Mesh, Line, or Points instance. | |
*/ | |
assignFinalMaterial( mesh ) { | |
const geometry = mesh.geometry; | |
let material = mesh.material; | |
const useDerivativeTangents = geometry.attributes.tangent === undefined; | |
const useVertexColors = geometry.attributes.color !== undefined; | |
const useFlatShading = geometry.attributes.normal === undefined; | |
if ( mesh.isPoints ) { | |
const cacheKey = 'PointsMaterial:' + material.uuid; | |
let pointsMaterial = this.cache.get( cacheKey ); | |
if ( ! pointsMaterial ) { | |
pointsMaterial = new PointsMaterial(); | |
Material.prototype.copy.call( pointsMaterial, material ); | |
pointsMaterial.color.copy( material.color ); | |
pointsMaterial.map = material.map; | |
pointsMaterial.sizeAttenuation = false; // glTF spec says points should be 1px | |
this.cache.add( cacheKey, pointsMaterial ); | |
} | |
material = pointsMaterial; | |
} else if ( mesh.isLine ) { | |
const cacheKey = 'LineBasicMaterial:' + material.uuid; | |
let lineMaterial = this.cache.get( cacheKey ); | |
if ( ! lineMaterial ) { | |
lineMaterial = new LineBasicMaterial(); | |
Material.prototype.copy.call( lineMaterial, material ); | |
lineMaterial.color.copy( material.color ); | |
lineMaterial.map = material.map; | |
this.cache.add( cacheKey, lineMaterial ); | |
} | |
material = lineMaterial; | |
} | |
// Clone the material if it will be modified | |
if ( useDerivativeTangents || useVertexColors || useFlatShading ) { | |
let cacheKey = 'ClonedMaterial:' + material.uuid + ':'; | |
if ( useDerivativeTangents ) cacheKey += 'derivative-tangents:'; | |
if ( useVertexColors ) cacheKey += 'vertex-colors:'; | |
if ( useFlatShading ) cacheKey += 'flat-shading:'; | |
let cachedMaterial = this.cache.get( cacheKey ); | |
if ( ! cachedMaterial ) { | |
cachedMaterial = material.clone(); | |
if ( useVertexColors ) cachedMaterial.vertexColors = true; | |
if ( useFlatShading ) cachedMaterial.flatShading = true; | |
if ( useDerivativeTangents ) { | |
// https://github.com/mrdoob/three.js/issues/11438#issuecomment-507003995 | |
if ( cachedMaterial.normalScale ) cachedMaterial.normalScale.y *= - 1; | |
if ( cachedMaterial.clearcoatNormalScale ) cachedMaterial.clearcoatNormalScale.y *= - 1; | |
} | |
this.cache.add( cacheKey, cachedMaterial ); | |
this.associations.set( cachedMaterial, this.associations.get( material ) ); | |
} | |
material = cachedMaterial; | |
} | |
mesh.material = material; | |
} | |
getMaterialType( /* materialIndex */ ) { | |
return MeshStandardMaterial; | |
} | |
/** | |
* Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#materials | |
* @param {number} materialIndex | |
* @return {Promise<Material>} | |
*/ | |
loadMaterial( materialIndex ) { | |
const parser = this; | |
const json = this.json; | |
const extensions = this.extensions; | |
const materialDef = json.materials[ materialIndex ]; | |
let materialType; | |
const materialParams = {}; | |
const materialExtensions = materialDef.extensions || {}; | |
const pending = []; | |
if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ] ) { | |
const kmuExtension = extensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ]; | |
materialType = kmuExtension.getMaterialType(); | |
pending.push( kmuExtension.extendParams( materialParams, materialDef, parser ) ); | |
} else { | |
// Specification: | |
// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-material | |
const metallicRoughness = materialDef.pbrMetallicRoughness || {}; | |
materialParams.color = new Color( 1.0, 1.0, 1.0 ); | |
materialParams.opacity = 1.0; | |
if ( Array.isArray( metallicRoughness.baseColorFactor ) ) { | |
const array = metallicRoughness.baseColorFactor; | |
materialParams.color.setRGB( array[ 0 ], array[ 1 ], array[ 2 ], LinearSRGBColorSpace ); | |
materialParams.opacity = array[ 3 ]; | |
} | |
if ( metallicRoughness.baseColorTexture !== undefined ) { | |
pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture, SRGBColorSpace ) ); | |
} | |
materialParams.metalness = metallicRoughness.metallicFactor !== undefined ? metallicRoughness.metallicFactor : 1.0; | |
materialParams.roughness = metallicRoughness.roughnessFactor !== undefined ? metallicRoughness.roughnessFactor : 1.0; | |
if ( metallicRoughness.metallicRoughnessTexture !== undefined ) { | |
pending.push( parser.assignTexture( materialParams, 'metalnessMap', metallicRoughness.metallicRoughnessTexture ) ); | |
pending.push( parser.assignTexture( materialParams, 'roughnessMap', metallicRoughness.metallicRoughnessTexture ) ); | |
} | |
materialType = this._invokeOne( function ( ext ) { | |
return ext.getMaterialType && ext.getMaterialType( materialIndex ); | |
} ); | |
pending.push( Promise.all( this._invokeAll( function ( ext ) { | |
return ext.extendMaterialParams && ext.extendMaterialParams( materialIndex, materialParams ); | |
} ) ) ); | |
} | |
if ( materialDef.doubleSided === true ) { | |
materialParams.side = DoubleSide; | |
} | |
const alphaMode = materialDef.alphaMode || ALPHA_MODES.OPAQUE; | |
if ( alphaMode === ALPHA_MODES.BLEND ) { | |
materialParams.transparent = true; | |
// See: https://github.com/mrdoob/three.js/issues/17706 | |
materialParams.depthWrite = false; | |
} else { | |
materialParams.transparent = false; | |
if ( alphaMode === ALPHA_MODES.MASK ) { | |
materialParams.alphaTest = materialDef.alphaCutoff !== undefined ? materialDef.alphaCutoff : 0.5; | |
} | |
} | |
if ( materialDef.normalTexture !== undefined && materialType !== MeshBasicMaterial ) { | |
pending.push( parser.assignTexture( materialParams, 'normalMap', materialDef.normalTexture ) ); | |
materialParams.normalScale = new Vector2( 1, 1 ); | |
if ( materialDef.normalTexture.scale !== undefined ) { | |
const scale = materialDef.normalTexture.scale; | |
materialParams.normalScale.set( scale, scale ); | |
} | |
} | |
if ( materialDef.occlusionTexture !== undefined && materialType !== MeshBasicMaterial ) { | |
pending.push( parser.assignTexture( materialParams, 'aoMap', materialDef.occlusionTexture ) ); | |
if ( materialDef.occlusionTexture.strength !== undefined ) { | |
materialParams.aoMapIntensity = materialDef.occlusionTexture.strength; | |
} | |
} | |
if ( materialDef.emissiveFactor !== undefined && materialType !== MeshBasicMaterial ) { | |
const emissiveFactor = materialDef.emissiveFactor; | |
materialParams.emissive = new Color().setRGB( emissiveFactor[ 0 ], emissiveFactor[ 1 ], emissiveFactor[ 2 ], LinearSRGBColorSpace ); | |
} | |
if ( materialDef.emissiveTexture !== undefined && materialType !== MeshBasicMaterial ) { | |
pending.push( parser.assignTexture( materialParams, 'emissiveMap', materialDef.emissiveTexture, SRGBColorSpace ) ); | |
} | |
return Promise.all( pending ).then( function () { | |
const material = new materialType( materialParams ); | |
if ( materialDef.name ) material.name = materialDef.name; | |
assignExtrasToUserData( material, materialDef ); | |
parser.associations.set( material, { materials: materialIndex } ); | |
if ( materialDef.extensions ) addUnknownExtensionsToUserData( extensions, material, materialDef ); | |
return material; | |
} ); | |
} | |
/** | |
* When Object3D instances are targeted by animation, they need unique names. | |
* | |
* @param {String} originalName | |
* @return {String} | |
*/ | |
createUniqueName( originalName ) { | |
const sanitizedName = PropertyBinding.sanitizeNodeName( originalName || '' ); | |
if ( sanitizedName in this.nodeNamesUsed ) { | |
return sanitizedName + '_' + ( ++ this.nodeNamesUsed[ sanitizedName ] ); | |
} else { | |
this.nodeNamesUsed[ sanitizedName ] = 0; | |
return sanitizedName; | |
} | |
} | |
/** | |
* Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#geometry | |
* | |
* Creates BufferGeometries from primitives. | |
* | |
* @param {Array<GLTF.Primitive>} primitives | |
* @return {Promise<Array<BufferGeometry>>} | |
*/ | |
loadGeometries( primitives ) { | |
const parser = this; | |
const extensions = this.extensions; | |
const cache = this.primitiveCache; | |
function createDracoPrimitive( primitive ) { | |
return extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ] | |
.decodePrimitive( primitive, parser ) | |
.then( function ( geometry ) { | |
return addPrimitiveAttributes( geometry, primitive, parser ); | |
} ); | |
} | |
const pending = []; | |
for ( let i = 0, il = primitives.length; i < il; i ++ ) { | |
const primitive = primitives[ i ]; | |
const cacheKey = createPrimitiveKey( primitive ); | |
// See if we've already created this geometry | |
const cached = cache[ cacheKey ]; | |
if ( cached ) { | |
// Use the cached geometry if it exists | |
pending.push( cached.promise ); | |
} else { | |
let geometryPromise; | |
if ( primitive.extensions && primitive.extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ] ) { | |
// Use DRACO geometry if available | |
geometryPromise = createDracoPrimitive( primitive ); | |
} else { | |
// Otherwise create a new geometry | |
geometryPromise = addPrimitiveAttributes( new BufferGeometry(), primitive, parser ); | |
} | |
// Cache this geometry | |
cache[ cacheKey ] = { primitive: primitive, promise: geometryPromise }; | |
pending.push( geometryPromise ); | |
} | |
} | |
return Promise.all( pending ); | |
} | |
/** | |
* Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#meshes | |
* @param {number} meshIndex | |
* @return {Promise<Group|Mesh|SkinnedMesh>} | |
*/ | |
loadMesh( meshIndex ) { | |
const parser = this; | |
const json = this.json; | |
const extensions = this.extensions; | |
const meshDef = json.meshes[ meshIndex ]; | |
const primitives = meshDef.primitives; | |
const pending = []; | |
for ( let i = 0, il = primitives.length; i < il; i ++ ) { | |
const material = primitives[ i ].material === undefined | |
? createDefaultMaterial( this.cache ) | |
: this.getDependency( 'material', primitives[ i ].material ); | |
pending.push( material ); | |
} | |
pending.push( parser.loadGeometries( primitives ) ); | |
return Promise.all( pending ).then( function ( results ) { | |
const materials = results.slice( 0, results.length - 1 ); | |
const geometries = results[ results.length - 1 ]; | |
const meshes = []; | |
for ( let i = 0, il = geometries.length; i < il; i ++ ) { | |
const geometry = geometries[ i ]; | |
const primitive = primitives[ i ]; | |
// 1. create Mesh | |
let mesh; | |
const material = materials[ i ]; | |
if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLES || | |
primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP || | |
primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN || | |
primitive.mode === undefined ) { | |
// .isSkinnedMesh isn't in glTF spec. See ._markDefs() | |
mesh = meshDef.isSkinnedMesh === true | |
? new SkinnedMesh( geometry, material ) | |
: new Mesh( geometry, material ); | |
if ( mesh.isSkinnedMesh === true ) { | |
// normalize skin weights to fix malformed assets (see #15319) | |
mesh.normalizeSkinWeights(); | |
} | |
if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ) { | |
mesh.geometry = toTrianglesDrawMode( mesh.geometry, TriangleStripDrawMode ); | |
} else if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN ) { | |
mesh.geometry = toTrianglesDrawMode( mesh.geometry, TriangleFanDrawMode ); | |
} | |
} else if ( primitive.mode === WEBGL_CONSTANTS.LINES ) { | |
mesh = new LineSegments( geometry, material ); | |
} else if ( primitive.mode === WEBGL_CONSTANTS.LINE_STRIP ) { | |
mesh = new Line( geometry, material ); | |
} else if ( primitive.mode === WEBGL_CONSTANTS.LINE_LOOP ) { | |
mesh = new LineLoop( geometry, material ); | |
} else if ( primitive.mode === WEBGL_CONSTANTS.POINTS ) { | |
mesh = new Points( geometry, material ); | |
} else { | |
throw new Error( 'THREE.GLTFLoader: Primitive mode unsupported: ' + primitive.mode ); | |
} | |
if ( Object.keys( mesh.geometry.morphAttributes ).length > 0 ) { | |
updateMorphTargets( mesh, meshDef ); | |
} | |
mesh.name = parser.createUniqueName( meshDef.name || ( 'mesh_' + meshIndex ) ); | |
assignExtrasToUserData( mesh, meshDef ); | |
if ( primitive.extensions ) addUnknownExtensionsToUserData( extensions, mesh, primitive ); | |
parser.assignFinalMaterial( mesh ); | |
meshes.push( mesh ); | |
} | |
for ( let i = 0, il = meshes.length; i < il; i ++ ) { | |
parser.associations.set( meshes[ i ], { | |
meshes: meshIndex, | |
primitives: i | |
} ); | |
} | |
if ( meshes.length === 1 ) { | |
if ( meshDef.extensions ) addUnknownExtensionsToUserData( extensions, meshes[ 0 ], meshDef ); | |
return meshes[ 0 ]; | |
} | |
const group = new Group(); | |
if ( meshDef.extensions ) addUnknownExtensionsToUserData( extensions, group, meshDef ); | |
parser.associations.set( group, { meshes: meshIndex } ); | |
for ( let i = 0, il = meshes.length; i < il; i ++ ) { | |
group.add( meshes[ i ] ); | |
} | |
return group; | |
} ); | |
} | |
/** | |
* Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#cameras | |
* @param {number} cameraIndex | |
* @return {Promise<THREE.Camera>} | |
*/ | |
loadCamera( cameraIndex ) { | |
let camera; | |
const cameraDef = this.json.cameras[ cameraIndex ]; | |
const params = cameraDef[ cameraDef.type ]; | |
if ( ! params ) { | |
console.warn( 'THREE.GLTFLoader: Missing camera parameters.' ); | |
return; | |
} | |
if ( cameraDef.type === 'perspective' ) { | |
camera = new PerspectiveCamera( MathUtils.radToDeg( params.yfov ), params.aspectRatio || 1, params.znear || 1, params.zfar || 2e6 ); | |
} else if ( cameraDef.type === 'orthographic' ) { | |
camera = new OrthographicCamera( - params.xmag, params.xmag, params.ymag, - params.ymag, params.znear, params.zfar ); | |
} | |
if ( cameraDef.name ) camera.name = this.createUniqueName( cameraDef.name ); | |
assignExtrasToUserData( camera, cameraDef ); | |
return Promise.resolve( camera ); | |
} | |
/** | |
* Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#skins | |
* @param {number} skinIndex | |
* @return {Promise<Skeleton>} | |
*/ | |
loadSkin( skinIndex ) { | |
const skinDef = this.json.skins[ skinIndex ]; | |
const pending = []; | |
for ( let i = 0, il = skinDef.joints.length; i < il; i ++ ) { | |
pending.push( this._loadNodeShallow( skinDef.joints[ i ] ) ); | |
} | |
if ( skinDef.inverseBindMatrices !== undefined ) { | |
pending.push( this.getDependency( 'accessor', skinDef.inverseBindMatrices ) ); | |
} else { | |
pending.push( null ); | |
} | |
return Promise.all( pending ).then( function ( results ) { | |
const inverseBindMatrices = results.pop(); | |
const jointNodes = results; | |
// Note that bones (joint nodes) may or may not be in the | |
// scene graph at this time. | |
const bones = []; | |
const boneInverses = []; | |
for ( let i = 0, il = jointNodes.length; i < il; i ++ ) { | |
const jointNode = jointNodes[ i ]; | |
if ( jointNode ) { | |
bones.push( jointNode ); | |
const mat = new Matrix4(); | |
if ( inverseBindMatrices !== null ) { | |
mat.fromArray( inverseBindMatrices.array, i * 16 ); | |
} | |
boneInverses.push( mat ); | |
} else { | |
console.warn( 'THREE.GLTFLoader: Joint "%s" could not be found.', skinDef.joints[ i ] ); | |
} | |
} | |
return new Skeleton( bones, boneInverses ); | |
} ); | |
} | |
/** | |
* Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#animations | |
* @param {number} animationIndex | |
* @return {Promise<AnimationClip>} | |
*/ | |
loadAnimation( animationIndex ) { | |
const json = this.json; | |
const parser = this; | |
const animationDef = json.animations[ animationIndex ]; | |
const animationName = animationDef.name ? animationDef.name : 'animation_' + animationIndex; | |
const pendingNodes = []; | |
const pendingInputAccessors = []; | |
const pendingOutputAccessors = []; | |
const pendingSamplers = []; | |
const pendingTargets = []; | |
for ( let i = 0, il = animationDef.channels.length; i < il; i ++ ) { | |
const channel = animationDef.channels[ i ]; | |
const sampler = animationDef.samplers[ channel.sampler ]; | |
const target = channel.target; | |
const name = target.node; | |
const input = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.input ] : sampler.input; | |
const output = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.output ] : sampler.output; | |
if ( target.node === undefined ) continue; | |
pendingNodes.push( this.getDependency( 'node', name ) ); | |
pendingInputAccessors.push( this.getDependency( 'accessor', input ) ); | |
pendingOutputAccessors.push( this.getDependency( 'accessor', output ) ); | |
pendingSamplers.push( sampler ); | |
pendingTargets.push( target ); | |
} | |
return Promise.all( [ | |
Promise.all( pendingNodes ), | |
Promise.all( pendingInputAccessors ), | |
Promise.all( pendingOutputAccessors ), | |
Promise.all( pendingSamplers ), | |
Promise.all( pendingTargets ) | |
] ).then( function ( dependencies ) { | |
const nodes = dependencies[ 0 ]; | |
const inputAccessors = dependencies[ 1 ]; | |
const outputAccessors = dependencies[ 2 ]; | |
const samplers = dependencies[ 3 ]; | |
const targets = dependencies[ 4 ]; | |
const tracks = []; | |
for ( let i = 0, il = nodes.length; i < il; i ++ ) { | |
const node = nodes[ i ]; | |
const inputAccessor = inputAccessors[ i ]; | |
const outputAccessor = outputAccessors[ i ]; | |
const sampler = samplers[ i ]; | |
const target = targets[ i ]; | |
if ( node === undefined ) continue; | |
if ( node.updateMatrix ) { | |
node.updateMatrix(); | |
} | |
const createdTracks = parser._createAnimationTracks( node, inputAccessor, outputAccessor, sampler, target ); | |
if ( createdTracks ) { | |
for ( let k = 0; k < createdTracks.length; k ++ ) { | |
tracks.push( createdTracks[ k ] ); | |
} | |
} | |
} | |
return new AnimationClip( animationName, undefined, tracks ); | |
} ); | |
} | |
createNodeMesh( nodeIndex ) { | |
const json = this.json; | |
const parser = this; | |
const nodeDef = json.nodes[ nodeIndex ]; | |
if ( nodeDef.mesh === undefined ) return null; | |
return parser.getDependency( 'mesh', nodeDef.mesh ).then( function ( mesh ) { | |
const node = parser._getNodeRef( parser.meshCache, nodeDef.mesh, mesh ); | |
// if weights are provided on the node, override weights on the mesh. | |
if ( nodeDef.weights !== undefined ) { | |
node.traverse( function ( o ) { | |
if ( ! o.isMesh ) return; | |
for ( let i = 0, il = nodeDef.weights.length; i < il; i ++ ) { | |
o.morphTargetInfluences[ i ] = nodeDef.weights[ i ]; | |
} | |
} ); | |
} | |
return node; | |
} ); | |
} | |
/** | |
* Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodes-and-hierarchy | |
* @param {number} nodeIndex | |
* @return {Promise<Object3D>} | |
*/ | |
loadNode( nodeIndex ) { | |
const json = this.json; | |
const parser = this; | |
const nodeDef = json.nodes[ nodeIndex ]; | |
const nodePending = parser._loadNodeShallow( nodeIndex ); | |
const childPending = []; | |
const childrenDef = nodeDef.children || []; | |
for ( let i = 0, il = childrenDef.length; i < il; i ++ ) { | |
childPending.push( parser.getDependency( 'node', childrenDef[ i ] ) ); | |
} | |
const skeletonPending = nodeDef.skin === undefined | |
? Promise.resolve( null ) | |
: parser.getDependency( 'skin', nodeDef.skin ); | |
return Promise.all( [ | |
nodePending, | |
Promise.all( childPending ), | |
skeletonPending | |
] ).then( function ( results ) { | |
const node = results[ 0 ]; | |
const children = results[ 1 ]; | |
const skeleton = results[ 2 ]; | |
if ( skeleton !== null ) { | |
// This full traverse should be fine because | |
// child glTF nodes have not been added to this node yet. | |
node.traverse( function ( mesh ) { | |
if ( ! mesh.isSkinnedMesh ) return; | |
mesh.bind( skeleton, _identityMatrix ); | |
} ); | |
} | |
for ( let i = 0, il = children.length; i < il; i ++ ) { | |
node.add( children[ i ] ); | |
} | |
return node; | |
} ); | |
} | |
// ._loadNodeShallow() parses a single node. | |
// skin and child nodes are created and added in .loadNode() (no '_' prefix). | |
_loadNodeShallow( nodeIndex ) { | |
const json = this.json; | |
const extensions = this.extensions; | |
const parser = this; | |
// This method is called from .loadNode() and .loadSkin(). | |
// Cache a node to avoid duplication. | |
if ( this.nodeCache[ nodeIndex ] !== undefined ) { | |
return this.nodeCache[ nodeIndex ]; | |
} | |
const nodeDef = json.nodes[ nodeIndex ]; | |
// reserve node's name before its dependencies, so the root has the intended name. | |
const nodeName = nodeDef.name ? parser.createUniqueName( nodeDef.name ) : ''; | |
const pending = []; | |
const meshPromise = parser._invokeOne( function ( ext ) { | |
return ext.createNodeMesh && ext.createNodeMesh( nodeIndex ); | |
} ); | |
if ( meshPromise ) { | |
pending.push( meshPromise ); | |
} | |
if ( nodeDef.camera !== undefined ) { | |
pending.push( parser.getDependency( 'camera', nodeDef.camera ).then( function ( camera ) { | |
return parser._getNodeRef( parser.cameraCache, nodeDef.camera, camera ); | |
} ) ); | |
} | |
parser._invokeAll( function ( ext ) { | |
return ext.createNodeAttachment && ext.createNodeAttachment( nodeIndex ); | |
} ).forEach( function ( promise ) { | |
pending.push( promise ); | |
} ); | |
this.nodeCache[ nodeIndex ] = Promise.all( pending ).then( function ( objects ) { | |
let node; | |
// .isBone isn't in glTF spec. See ._markDefs | |
if ( nodeDef.isBone === true ) { | |
node = new Bone(); | |
} else if ( objects.length > 1 ) { | |
node = new Group(); | |
} else if ( objects.length === 1 ) { | |
node = objects[ 0 ]; | |
} else { | |
node = new Object3D(); | |
} | |
if ( node !== objects[ 0 ] ) { | |
for ( let i = 0, il = objects.length; i < il; i ++ ) { | |
node.add( objects[ i ] ); | |
} | |
} | |
if ( nodeDef.name ) { | |
node.userData.name = nodeDef.name; | |
node.name = nodeName; | |
} | |
assignExtrasToUserData( node, nodeDef ); | |
if ( nodeDef.extensions ) addUnknownExtensionsToUserData( extensions, node, nodeDef ); | |
if ( nodeDef.matrix !== undefined ) { | |
const matrix = new Matrix4(); | |
matrix.fromArray( nodeDef.matrix ); | |
node.applyMatrix4( matrix ); | |
} else { | |
if ( nodeDef.translation !== undefined ) { | |
node.position.fromArray( nodeDef.translation ); | |
} | |
if ( nodeDef.rotation !== undefined ) { | |
node.quaternion.fromArray( nodeDef.rotation ); | |
} | |
if ( nodeDef.scale !== undefined ) { | |
node.scale.fromArray( nodeDef.scale ); | |
} | |
} | |
if ( ! parser.associations.has( node ) ) { | |
parser.associations.set( node, {} ); | |
} | |
parser.associations.get( node ).nodes = nodeIndex; | |
return node; | |
} ); | |
return this.nodeCache[ nodeIndex ]; | |
} | |
/** | |
* Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#scenes | |
* @param {number} sceneIndex | |
* @return {Promise<Group>} | |
*/ | |
loadScene( sceneIndex ) { | |
const extensions = this.extensions; | |
const sceneDef = this.json.scenes[ sceneIndex ]; | |
const parser = this; | |
// Loader returns Group, not Scene. | |
// See: https://github.com/mrdoob/three.js/issues/18342#issuecomment-578981172 | |
const scene = new Group(); | |
if ( sceneDef.name ) scene.name = parser.createUniqueName( sceneDef.name ); | |
assignExtrasToUserData( scene, sceneDef ); | |
if ( sceneDef.extensions ) addUnknownExtensionsToUserData( extensions, scene, sceneDef ); | |
const nodeIds = sceneDef.nodes || []; | |
const pending = []; | |
for ( let i = 0, il = nodeIds.length; i < il; i ++ ) { | |
pending.push( parser.getDependency( 'node', nodeIds[ i ] ) ); | |
} | |
return Promise.all( pending ).then( function ( nodes ) { | |
for ( let i = 0, il = nodes.length; i < il; i ++ ) { | |
scene.add( nodes[ i ] ); | |
} | |
// Removes dangling associations, associations that reference a node that | |
// didn't make it into the scene. | |
const reduceAssociations = ( node ) => { | |
const reducedAssociations = new Map(); | |
for ( const [ key, value ] of parser.associations ) { | |
if ( key instanceof Material || key instanceof Texture ) { | |
reducedAssociations.set( key, value ); | |
} | |
} | |
node.traverse( ( node ) => { | |
const mappings = parser.associations.get( node ); | |
if ( mappings != null ) { | |
reducedAssociations.set( node, mappings ); | |
} | |
} ); | |
return reducedAssociations; | |
}; | |
parser.associations = reduceAssociations( scene ); | |
return scene; | |
} ); | |
} | |
_createAnimationTracks( node, inputAccessor, outputAccessor, sampler, target ) { | |
const tracks = []; | |
const targetName = node.name ? node.name : node.uuid; | |
const targetNames = []; | |
if ( PATH_PROPERTIES[ target.path ] === PATH_PROPERTIES.weights ) { | |
node.traverse( function ( object ) { | |
if ( object.morphTargetInfluences ) { | |
targetNames.push( object.name ? object.name : object.uuid ); | |
} | |
} ); | |
} else { | |
targetNames.push( targetName ); | |
} | |
let TypedKeyframeTrack; | |
switch ( PATH_PROPERTIES[ target.path ] ) { | |
case PATH_PROPERTIES.weights: | |
TypedKeyframeTrack = NumberKeyframeTrack; | |
break; | |
case PATH_PROPERTIES.rotation: | |
TypedKeyframeTrack = QuaternionKeyframeTrack; | |
break; | |
case PATH_PROPERTIES.position: | |
case PATH_PROPERTIES.scale: | |
TypedKeyframeTrack = VectorKeyframeTrack; | |
break; | |
default: | |
switch ( outputAccessor.itemSize ) { | |
case 1: | |
TypedKeyframeTrack = NumberKeyframeTrack; | |
break; | |
case 2: | |
case 3: | |
default: | |
TypedKeyframeTrack = VectorKeyframeTrack; | |
break; | |
} | |
break; | |
} | |
const interpolation = sampler.interpolation !== undefined ? INTERPOLATION[ sampler.interpolation ] : InterpolateLinear; | |
const outputArray = this._getArrayFromAccessor( outputAccessor ); | |
for ( let j = 0, jl = targetNames.length; j < jl; j ++ ) { | |
const track = new TypedKeyframeTrack( | |
targetNames[ j ] + '.' + PATH_PROPERTIES[ target.path ], | |
inputAccessor.array, | |
outputArray, | |
interpolation | |
); | |
// Override interpolation with custom factory method. | |
if ( sampler.interpolation === 'CUBICSPLINE' ) { | |
this._createCubicSplineTrackInterpolant( track ); | |
} | |
tracks.push( track ); | |
} | |
return tracks; | |
} | |
_getArrayFromAccessor( accessor ) { | |
let outputArray = accessor.array; | |
if ( accessor.normalized ) { | |
const scale = getNormalizedComponentScale( outputArray.constructor ); | |
const scaled = new Float32Array( outputArray.length ); | |
for ( let j = 0, jl = outputArray.length; j < jl; j ++ ) { | |
scaled[ j ] = outputArray[ j ] * scale; | |
} | |
outputArray = scaled; | |
} | |
return outputArray; | |
} | |
_createCubicSplineTrackInterpolant( track ) { | |
track.createInterpolant = function InterpolantFactoryMethodGLTFCubicSpline( result ) { | |
// A CUBICSPLINE keyframe in glTF has three output values for each input value, | |
// representing inTangent, splineVertex, and outTangent. As a result, track.getValueSize() | |
// must be divided by three to get the interpolant's sampleSize argument. | |
const interpolantType = ( this instanceof QuaternionKeyframeTrack ) ? GLTFCubicSplineQuaternionInterpolant : GLTFCubicSplineInterpolant; | |
return new interpolantType( this.times, this.values, this.getValueSize() / 3, result ); | |
}; | |
// Mark as CUBICSPLINE. `track.getInterpolation()` doesn't support custom interpolants. | |
track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline = true; | |
} | |
} | |
/** | |
* @param {BufferGeometry} geometry | |
* @param {GLTF.Primitive} primitiveDef | |
* @param {GLTFParser} parser | |
*/ | |
function computeBounds( geometry, primitiveDef, parser ) { | |
const attributes = primitiveDef.attributes; | |
const box = new Box3(); | |
if ( attributes.POSITION !== undefined ) { | |
const accessor = parser.json.accessors[ attributes.POSITION ]; | |
const min = accessor.min; | |
const max = accessor.max; | |
// glTF requires 'min' and 'max', but VRM (which extends glTF) currently ignores that requirement. | |
if ( min !== undefined && max !== undefined ) { | |
box.set( | |
new Vector3( min[ 0 ], min[ 1 ], min[ 2 ] ), | |
new Vector3( max[ 0 ], max[ 1 ], max[ 2 ] ) | |
); | |
if ( accessor.normalized ) { | |
const boxScale = getNormalizedComponentScale( WEBGL_COMPONENT_TYPES[ accessor.componentType ] ); | |
box.min.multiplyScalar( boxScale ); | |
box.max.multiplyScalar( boxScale ); | |
} | |
} else { | |
console.warn( 'THREE.GLTFLoader: Missing min/max properties for accessor POSITION.' ); | |
return; | |
} | |
} else { | |
return; | |
} | |
const targets = primitiveDef.targets; | |
if ( targets !== undefined ) { | |
const maxDisplacement = new Vector3(); | |
const vector = new Vector3(); | |
for ( let i = 0, il = targets.length; i < il; i ++ ) { | |
const target = targets[ i ]; | |
if ( target.POSITION !== undefined ) { | |
const accessor = parser.json.accessors[ target.POSITION ]; | |
const min = accessor.min; | |
const max = accessor.max; | |
// glTF requires 'min' and 'max', but VRM (which extends glTF) currently ignores that requirement. | |
if ( min !== undefined && max !== undefined ) { | |
// we need to get max of absolute components because target weight is [-1,1] | |
vector.setX( Math.max( Math.abs( min[ 0 ] ), Math.abs( max[ 0 ] ) ) ); | |
vector.setY( Math.max( Math.abs( min[ 1 ] ), Math.abs( max[ 1 ] ) ) ); | |
vector.setZ( Math.max( Math.abs( min[ 2 ] ), Math.abs( max[ 2 ] ) ) ); | |
if ( accessor.normalized ) { | |
const boxScale = getNormalizedComponentScale( WEBGL_COMPONENT_TYPES[ accessor.componentType ] ); | |
vector.multiplyScalar( boxScale ); | |
} | |
// Note: this assumes that the sum of all weights is at most 1. This isn't quite correct - it's more conservative | |
// to assume that each target can have a max weight of 1. However, for some use cases - notably, when morph targets | |
// are used to implement key-frame animations and as such only two are active at a time - this results in very large | |
// boxes. So for now we make a box that's sometimes a touch too small but is hopefully mostly of reasonable size. | |
maxDisplacement.max( vector ); | |
} else { | |
console.warn( 'THREE.GLTFLoader: Missing min/max properties for accessor POSITION.' ); | |
} | |
} | |
} | |
// As per comment above this box isn't conservative, but has a reasonable size for a very large number of morph targets. | |
box.expandByVector( maxDisplacement ); | |
} | |
geometry.boundingBox = box; | |
const sphere = new Sphere(); | |
box.getCenter( sphere.center ); | |
sphere.radius = box.min.distanceTo( box.max ) / 2; | |
geometry.boundingSphere = sphere; | |
} | |
/** | |
* @param {BufferGeometry} geometry | |
* @param {GLTF.Primitive} primitiveDef | |
* @param {GLTFParser} parser | |
* @return {Promise<BufferGeometry>} | |
*/ | |
function addPrimitiveAttributes( geometry, primitiveDef, parser ) { | |
const attributes = primitiveDef.attributes; | |
const pending = []; | |
function assignAttributeAccessor( accessorIndex, attributeName ) { | |
return parser.getDependency( 'accessor', accessorIndex ) | |
.then( function ( accessor ) { | |
geometry.setAttribute( attributeName, accessor ); | |
} ); | |
} | |
for ( const gltfAttributeName in attributes ) { | |
const threeAttributeName = ATTRIBUTES[ gltfAttributeName ] || gltfAttributeName.toLowerCase(); | |
// Skip attributes already provided by e.g. Draco extension. | |
if ( threeAttributeName in geometry.attributes ) continue; | |
pending.push( assignAttributeAccessor( attributes[ gltfAttributeName ], threeAttributeName ) ); | |
} | |
if ( primitiveDef.indices !== undefined && ! geometry.index ) { | |
const accessor = parser.getDependency( 'accessor', primitiveDef.indices ).then( function ( accessor ) { | |
geometry.setIndex( accessor ); | |
} ); | |
pending.push( accessor ); | |
} | |
if ( ColorManagement.workingColorSpace !== LinearSRGBColorSpace && 'COLOR_0' in attributes ) { | |
console.warn( `THREE.GLTFLoader: Converting vertex colors from "srgb-linear" to "${ColorManagement.workingColorSpace}" not supported.` ); | |
} | |
assignExtrasToUserData( geometry, primitiveDef ); | |
computeBounds( geometry, primitiveDef, parser ); | |
return Promise.all( pending ).then( function () { | |
return primitiveDef.targets !== undefined | |
? addMorphTargets( geometry, primitiveDef.targets, parser ) | |
: geometry; | |
} ); | |
} | |
export { GLTFLoader }; | |