From 73bdaae0e132ae856a0a369b98f9f6c501173c26 Mon Sep 17 00:00:00 2001 From: kovacsv Date: Mon, 9 Aug 2021 21:18:31 +0200 Subject: [PATCH] Add collada importer. --- libs/three_loaders/ColladaLoader.js | 3742 +++++++++++++++++++++++++++ source/threejs/threeimporter.js | 263 +- 2 files changed, 3926 insertions(+), 79 deletions(-) create mode 100644 libs/three_loaders/ColladaLoader.js diff --git a/libs/three_loaders/ColladaLoader.js b/libs/three_loaders/ColladaLoader.js new file mode 100644 index 0000000..419ff92 --- /dev/null +++ b/libs/three_loaders/ColladaLoader.js @@ -0,0 +1,3742 @@ +( function () { + + class ColladaLoader extends THREE.Loader { + + constructor( manager ) { + + super( manager ); + + } + + load( url, onLoad, onProgress, onError ) { + + const scope = this; + const path = scope.path === '' ? THREE.LoaderUtils.extractUrlBase( url ) : scope.path; + const loader = new THREE.FileLoader( scope.manager ); + loader.setPath( scope.path ); + loader.setRequestHeader( scope.requestHeader ); + loader.setWithCredentials( scope.withCredentials ); + loader.load( url, function ( text ) { + + try { + + onLoad( scope.parse( text, path ) ); + + } catch ( e ) { + + if ( onError ) { + + onError( e ); + + } else { + + console.error( e ); + + } + + scope.manager.itemError( url ); + + } + + }, onProgress, onError ); + + } + + parse( text, path ) { + + function getElementsByTagName( xml, name ) { + + // Non recursive xml.getElementsByTagName() ... + const array = []; + const childNodes = xml.childNodes; + + for ( let i = 0, l = childNodes.length; i < l; i ++ ) { + + const child = childNodes[ i ]; + + if ( child.nodeName === name ) { + + array.push( child ); + + } + + } + + return array; + + } + + function parseStrings( text ) { + + if ( text.length === 0 ) return []; + const parts = text.trim().split( /\s+/ ); + const array = new Array( parts.length ); + + for ( let i = 0, l = parts.length; i < l; i ++ ) { + + array[ i ] = parts[ i ]; + + } + + return array; + + } + + function parseFloats( text ) { + + if ( text.length === 0 ) return []; + const parts = text.trim().split( /\s+/ ); + const array = new Array( parts.length ); + + for ( let i = 0, l = parts.length; i < l; i ++ ) { + + array[ i ] = parseFloat( parts[ i ] ); + + } + + return array; + + } + + function parseInts( text ) { + + if ( text.length === 0 ) return []; + const parts = text.trim().split( /\s+/ ); + const array = new Array( parts.length ); + + for ( let i = 0, l = parts.length; i < l; i ++ ) { + + array[ i ] = parseInt( parts[ i ] ); + + } + + return array; + + } + + function parseId( text ) { + + return text.substring( 1 ); + + } + + function generateId() { + + return 'three_default_' + count ++; + + } + + function isEmpty( object ) { + + return Object.keys( object ).length === 0; + + } // asset + + + function parseAsset( xml ) { + + return { + unit: parseAssetUnit( getElementsByTagName( xml, 'unit' )[ 0 ] ), + upAxis: parseAssetUpAxis( getElementsByTagName( xml, 'up_axis' )[ 0 ] ) + }; + + } + + function parseAssetUnit( xml ) { + + if ( xml !== undefined && xml.hasAttribute( 'meter' ) === true ) { + + return parseFloat( xml.getAttribute( 'meter' ) ); + + } else { + + return 1; // default 1 meter + + } + + } + + function parseAssetUpAxis( xml ) { + + return xml !== undefined ? xml.textContent : 'Y_UP'; + + } // library + + + function parseLibrary( xml, libraryName, nodeName, parser ) { + + const library = getElementsByTagName( xml, libraryName )[ 0 ]; + + if ( library !== undefined ) { + + const elements = getElementsByTagName( library, nodeName ); + + for ( let i = 0; i < elements.length; i ++ ) { + + parser( elements[ i ] ); + + } + + } + + } + + function buildLibrary( data, builder ) { + + for ( const name in data ) { + + const object = data[ name ]; + object.build = builder( data[ name ] ); + + } + + } // get + + + function getBuild( data, builder ) { + + if ( data.build !== undefined ) return data.build; + data.build = builder( data ); + return data.build; + + } // animation + + + function parseAnimation( xml ) { + + const data = { + sources: {}, + samplers: {}, + channels: {} + }; + let hasChildren = false; + + for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + let id; + + switch ( child.nodeName ) { + + case 'source': + id = child.getAttribute( 'id' ); + data.sources[ id ] = parseSource( child ); + break; + + case 'sampler': + id = child.getAttribute( 'id' ); + data.samplers[ id ] = parseAnimationSampler( child ); + break; + + case 'channel': + id = child.getAttribute( 'target' ); + data.channels[ id ] = parseAnimationChannel( child ); + break; + + case 'animation': + // hierarchy of related animations + parseAnimation( child ); + hasChildren = true; + break; + + default: + console.log( child ); + + } + + } + + if ( hasChildren === false ) { + + // since 'id' attributes can be optional, it's necessary to generate a UUID for unqiue assignment + library.animations[ xml.getAttribute( 'id' ) || THREE.MathUtils.generateUUID() ] = data; + + } + + } + + function parseAnimationSampler( xml ) { + + const data = { + inputs: {} + }; + + for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'input': + const id = parseId( child.getAttribute( 'source' ) ); + const semantic = child.getAttribute( 'semantic' ); + data.inputs[ semantic ] = id; + break; + + } + + } + + return data; + + } + + function parseAnimationChannel( xml ) { + + const data = {}; + const target = xml.getAttribute( 'target' ); // parsing SID Addressing Syntax + + let parts = target.split( '/' ); + const id = parts.shift(); + let sid = parts.shift(); // check selection syntax + + const arraySyntax = sid.indexOf( '(' ) !== - 1; + const memberSyntax = sid.indexOf( '.' ) !== - 1; + + if ( memberSyntax ) { + + // member selection access + parts = sid.split( '.' ); + sid = parts.shift(); + data.member = parts.shift(); + + } else if ( arraySyntax ) { + + // array-access syntax. can be used to express fields in one-dimensional vectors or two-dimensional matrices. + const indices = sid.split( '(' ); + sid = indices.shift(); + + for ( let i = 0; i < indices.length; i ++ ) { + + indices[ i ] = parseInt( indices[ i ].replace( /\)/, '' ) ); + + } + + data.indices = indices; + + } + + data.id = id; + data.sid = sid; + data.arraySyntax = arraySyntax; + data.memberSyntax = memberSyntax; + data.sampler = parseId( xml.getAttribute( 'source' ) ); + return data; + + } + + function buildAnimation( data ) { + + const tracks = []; + const channels = data.channels; + const samplers = data.samplers; + const sources = data.sources; + + for ( const target in channels ) { + + if ( channels.hasOwnProperty( target ) ) { + + const channel = channels[ target ]; + const sampler = samplers[ channel.sampler ]; + const inputId = sampler.inputs.INPUT; + const outputId = sampler.inputs.OUTPUT; + const inputSource = sources[ inputId ]; + const outputSource = sources[ outputId ]; + const animation = buildAnimationChannel( channel, inputSource, outputSource ); + createKeyframeTracks( animation, tracks ); + + } + + } + + return tracks; + + } + + function getAnimation( id ) { + + return getBuild( library.animations[ id ], buildAnimation ); + + } + + function buildAnimationChannel( channel, inputSource, outputSource ) { + + const node = library.nodes[ channel.id ]; + const object3D = getNode( node.id ); + const transform = node.transforms[ channel.sid ]; + const defaultMatrix = node.matrix.clone().transpose(); + let time, stride; + let i, il, j, jl; + const data = {}; // the collada spec allows the animation of data in various ways. + // depending on the transform type (matrix, translate, rotate, scale), we execute different logic + + switch ( transform ) { + + case 'matrix': + for ( i = 0, il = inputSource.array.length; i < il; i ++ ) { + + time = inputSource.array[ i ]; + stride = i * outputSource.stride; + if ( data[ time ] === undefined ) data[ time ] = {}; + + if ( channel.arraySyntax === true ) { + + const value = outputSource.array[ stride ]; + const index = channel.indices[ 0 ] + 4 * channel.indices[ 1 ]; + data[ time ][ index ] = value; + + } else { + + for ( j = 0, jl = outputSource.stride; j < jl; j ++ ) { + + data[ time ][ j ] = outputSource.array[ stride + j ]; + + } + + } + + } + + break; + + case 'translate': + console.warn( 'THREE.ColladaLoader: Animation transform type "%s" not yet implemented.', transform ); + break; + + case 'rotate': + console.warn( 'THREE.ColladaLoader: Animation transform type "%s" not yet implemented.', transform ); + break; + + case 'scale': + console.warn( 'THREE.ColladaLoader: Animation transform type "%s" not yet implemented.', transform ); + break; + + } + + const keyframes = prepareAnimationData( data, defaultMatrix ); + const animation = { + name: object3D.uuid, + keyframes: keyframes + }; + return animation; + + } + + function prepareAnimationData( data, defaultMatrix ) { + + const keyframes = []; // transfer data into a sortable array + + for ( const time in data ) { + + keyframes.push( { + time: parseFloat( time ), + value: data[ time ] + } ); + + } // ensure keyframes are sorted by time + + + keyframes.sort( ascending ); // now we clean up all animation data, so we can use them for keyframe tracks + + for ( let i = 0; i < 16; i ++ ) { + + transformAnimationData( keyframes, i, defaultMatrix.elements[ i ] ); + + } + + return keyframes; // array sort function + + function ascending( a, b ) { + + return a.time - b.time; + + } + + } + + const position = new THREE.Vector3(); + const scale = new THREE.Vector3(); + const quaternion = new THREE.Quaternion(); + + function createKeyframeTracks( animation, tracks ) { + + const keyframes = animation.keyframes; + const name = animation.name; + const times = []; + const positionData = []; + const quaternionData = []; + const scaleData = []; + + for ( let i = 0, l = keyframes.length; i < l; i ++ ) { + + const keyframe = keyframes[ i ]; + const time = keyframe.time; + const value = keyframe.value; + matrix.fromArray( value ).transpose(); + matrix.decompose( position, quaternion, scale ); + times.push( time ); + positionData.push( position.x, position.y, position.z ); + quaternionData.push( quaternion.x, quaternion.y, quaternion.z, quaternion.w ); + scaleData.push( scale.x, scale.y, scale.z ); + + } + + if ( positionData.length > 0 ) tracks.push( new THREE.VectorKeyframeTrack( name + '.position', times, positionData ) ); + if ( quaternionData.length > 0 ) tracks.push( new THREE.QuaternionKeyframeTrack( name + '.quaternion', times, quaternionData ) ); + if ( scaleData.length > 0 ) tracks.push( new THREE.VectorKeyframeTrack( name + '.scale', times, scaleData ) ); + return tracks; + + } + + function transformAnimationData( keyframes, property, defaultValue ) { + + let keyframe; + let empty = true; + let i, l; // check, if values of a property are missing in our keyframes + + for ( i = 0, l = keyframes.length; i < l; i ++ ) { + + keyframe = keyframes[ i ]; + + if ( keyframe.value[ property ] === undefined ) { + + keyframe.value[ property ] = null; // mark as missing + + } else { + + empty = false; + + } + + } + + if ( empty === true ) { + + // no values at all, so we set a default value + for ( i = 0, l = keyframes.length; i < l; i ++ ) { + + keyframe = keyframes[ i ]; + keyframe.value[ property ] = defaultValue; + + } + + } else { + + // filling gaps + createMissingKeyframes( keyframes, property ); + + } + + } + + function createMissingKeyframes( keyframes, property ) { + + let prev, next; + + for ( let i = 0, l = keyframes.length; i < l; i ++ ) { + + const keyframe = keyframes[ i ]; + + if ( keyframe.value[ property ] === null ) { + + prev = getPrev( keyframes, i, property ); + next = getNext( keyframes, i, property ); + + if ( prev === null ) { + + keyframe.value[ property ] = next.value[ property ]; + continue; + + } + + if ( next === null ) { + + keyframe.value[ property ] = prev.value[ property ]; + continue; + + } + + interpolate( keyframe, prev, next, property ); + + } + + } + + } + + function getPrev( keyframes, i, property ) { + + while ( i >= 0 ) { + + const keyframe = keyframes[ i ]; + if ( keyframe.value[ property ] !== null ) return keyframe; + i --; + + } + + return null; + + } + + function getNext( keyframes, i, property ) { + + while ( i < keyframes.length ) { + + const keyframe = keyframes[ i ]; + if ( keyframe.value[ property ] !== null ) return keyframe; + i ++; + + } + + return null; + + } + + function interpolate( key, prev, next, property ) { + + if ( next.time - prev.time === 0 ) { + + key.value[ property ] = prev.value[ property ]; + return; + + } + + key.value[ property ] = ( key.time - prev.time ) * ( next.value[ property ] - prev.value[ property ] ) / ( next.time - prev.time ) + prev.value[ property ]; + + } // animation clips + + + function parseAnimationClip( xml ) { + + const data = { + name: xml.getAttribute( 'id' ) || 'default', + start: parseFloat( xml.getAttribute( 'start' ) || 0 ), + end: parseFloat( xml.getAttribute( 'end' ) || 0 ), + animations: [] + }; + + for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'instance_animation': + data.animations.push( parseId( child.getAttribute( 'url' ) ) ); + break; + + } + + } + + library.clips[ xml.getAttribute( 'id' ) ] = data; + + } + + function buildAnimationClip( data ) { + + const tracks = []; + const name = data.name; + const duration = data.end - data.start || - 1; + const animations = data.animations; + + for ( let i = 0, il = animations.length; i < il; i ++ ) { + + const animationTracks = getAnimation( animations[ i ] ); + + for ( let j = 0, jl = animationTracks.length; j < jl; j ++ ) { + + tracks.push( animationTracks[ j ] ); + + } + + } + + return new THREE.AnimationClip( name, duration, tracks ); + + } + + function getAnimationClip( id ) { + + return getBuild( library.clips[ id ], buildAnimationClip ); + + } // controller + + + function parseController( xml ) { + + const data = {}; + + for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'skin': + // there is exactly one skin per controller + data.id = parseId( child.getAttribute( 'source' ) ); + data.skin = parseSkin( child ); + break; + + case 'morph': + data.id = parseId( child.getAttribute( 'source' ) ); + console.warn( 'THREE.ColladaLoader: Morph target animation not supported yet.' ); + break; + + } + + } + + library.controllers[ xml.getAttribute( 'id' ) ] = data; + + } + + function parseSkin( xml ) { + + const data = { + sources: {} + }; + + for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'bind_shape_matrix': + data.bindShapeMatrix = parseFloats( child.textContent ); + break; + + case 'source': + const id = child.getAttribute( 'id' ); + data.sources[ id ] = parseSource( child ); + break; + + case 'joints': + data.joints = parseJoints( child ); + break; + + case 'vertex_weights': + data.vertexWeights = parseVertexWeights( child ); + break; + + } + + } + + return data; + + } + + function parseJoints( xml ) { + + const data = { + inputs: {} + }; + + for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'input': + const semantic = child.getAttribute( 'semantic' ); + const id = parseId( child.getAttribute( 'source' ) ); + data.inputs[ semantic ] = id; + break; + + } + + } + + return data; + + } + + function parseVertexWeights( xml ) { + + const data = { + inputs: {} + }; + + for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'input': + const semantic = child.getAttribute( 'semantic' ); + const id = parseId( child.getAttribute( 'source' ) ); + const offset = parseInt( child.getAttribute( 'offset' ) ); + data.inputs[ semantic ] = { + id: id, + offset: offset + }; + break; + + case 'vcount': + data.vcount = parseInts( child.textContent ); + break; + + case 'v': + data.v = parseInts( child.textContent ); + break; + + } + + } + + return data; + + } + + function buildController( data ) { + + const build = { + id: data.id + }; + const geometry = library.geometries[ build.id ]; + + if ( data.skin !== undefined ) { + + build.skin = buildSkin( data.skin ); // we enhance the 'sources' property of the corresponding geometry with our skin data + + geometry.sources.skinIndices = build.skin.indices; + geometry.sources.skinWeights = build.skin.weights; + + } + + return build; + + } + + function buildSkin( data ) { + + const BONE_LIMIT = 4; + const build = { + joints: [], + // this must be an array to preserve the joint order + indices: { + array: [], + stride: BONE_LIMIT + }, + weights: { + array: [], + stride: BONE_LIMIT + } + }; + const sources = data.sources; + const vertexWeights = data.vertexWeights; + const vcount = vertexWeights.vcount; + const v = vertexWeights.v; + const jointOffset = vertexWeights.inputs.JOINT.offset; + const weightOffset = vertexWeights.inputs.WEIGHT.offset; + const jointSource = data.sources[ data.joints.inputs.JOINT ]; + const inverseSource = data.sources[ data.joints.inputs.INV_BIND_MATRIX ]; + const weights = sources[ vertexWeights.inputs.WEIGHT.id ].array; + let stride = 0; + let i, j, l; // procces skin data for each vertex + + for ( i = 0, l = vcount.length; i < l; i ++ ) { + + const jointCount = vcount[ i ]; // this is the amount of joints that affect a single vertex + + const vertexSkinData = []; + + for ( j = 0; j < jointCount; j ++ ) { + + const skinIndex = v[ stride + jointOffset ]; + const weightId = v[ stride + weightOffset ]; + const skinWeight = weights[ weightId ]; + vertexSkinData.push( { + index: skinIndex, + weight: skinWeight + } ); + stride += 2; + + } // we sort the joints in descending order based on the weights. + // this ensures, we only procced the most important joints of the vertex + + + vertexSkinData.sort( descending ); // now we provide for each vertex a set of four index and weight values. + // the order of the skin data matches the order of vertices + + for ( j = 0; j < BONE_LIMIT; j ++ ) { + + const d = vertexSkinData[ j ]; + + if ( d !== undefined ) { + + build.indices.array.push( d.index ); + build.weights.array.push( d.weight ); + + } else { + + build.indices.array.push( 0 ); + build.weights.array.push( 0 ); + + } + + } + + } // setup bind matrix + + + if ( data.bindShapeMatrix ) { + + build.bindMatrix = new THREE.Matrix4().fromArray( data.bindShapeMatrix ).transpose(); + + } else { + + build.bindMatrix = new THREE.Matrix4().identity(); + + } // process bones and inverse bind matrix data + + + for ( i = 0, l = jointSource.array.length; i < l; i ++ ) { + + const name = jointSource.array[ i ]; + const boneInverse = new THREE.Matrix4().fromArray( inverseSource.array, i * inverseSource.stride ).transpose(); + build.joints.push( { + name: name, + boneInverse: boneInverse + } ); + + } + + return build; // array sort function + + function descending( a, b ) { + + return b.weight - a.weight; + + } + + } + + function getController( id ) { + + return getBuild( library.controllers[ id ], buildController ); + + } // image + + + function parseImage( xml ) { + + const data = { + init_from: getElementsByTagName( xml, 'init_from' )[ 0 ].textContent + }; + library.images[ xml.getAttribute( 'id' ) ] = data; + + } + + function buildImage( data ) { + + if ( data.build !== undefined ) return data.build; + return data.init_from; + + } + + function getImage( id ) { + + const data = library.images[ id ]; + + if ( data !== undefined ) { + + return getBuild( data, buildImage ); + + } + + console.warn( 'THREE.ColladaLoader: Couldn\'t find image with ID:', id ); + return null; + + } // effect + + + function parseEffect( xml ) { + + const data = {}; + + for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'profile_COMMON': + data.profile = parseEffectProfileCOMMON( child ); + break; + + } + + } + + library.effects[ xml.getAttribute( 'id' ) ] = data; + + } + + function parseEffectProfileCOMMON( xml ) { + + const data = { + surfaces: {}, + samplers: {} + }; + + for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'newparam': + parseEffectNewparam( child, data ); + break; + + case 'technique': + data.technique = parseEffectTechnique( child ); + break; + + case 'extra': + data.extra = parseEffectExtra( child ); + break; + + } + + } + + return data; + + } + + function parseEffectNewparam( xml, data ) { + + const sid = xml.getAttribute( 'sid' ); + + for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'surface': + data.surfaces[ sid ] = parseEffectSurface( child ); + break; + + case 'sampler2D': + data.samplers[ sid ] = parseEffectSampler( child ); + break; + + } + + } + + } + + function parseEffectSurface( xml ) { + + const data = {}; + + for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'init_from': + data.init_from = child.textContent; + break; + + } + + } + + return data; + + } + + function parseEffectSampler( xml ) { + + const data = {}; + + for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'source': + data.source = child.textContent; + break; + + } + + } + + return data; + + } + + function parseEffectTechnique( xml ) { + + const data = {}; + + for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'constant': + case 'lambert': + case 'blinn': + case 'phong': + data.type = child.nodeName; + data.parameters = parseEffectParameters( child ); + break; + + } + + } + + return data; + + } + + function parseEffectParameters( xml ) { + + const data = {}; + + for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'emission': + case 'diffuse': + case 'specular': + case 'bump': + case 'ambient': + case 'shininess': + case 'transparency': + data[ child.nodeName ] = parseEffectParameter( child ); + break; + + case 'transparent': + data[ child.nodeName ] = { + opaque: child.getAttribute( 'opaque' ), + data: parseEffectParameter( child ) + }; + break; + + } + + } + + return data; + + } + + function parseEffectParameter( xml ) { + + const data = {}; + + for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'color': + data[ child.nodeName ] = parseFloats( child.textContent ); + break; + + case 'float': + data[ child.nodeName ] = parseFloat( child.textContent ); + break; + + case 'texture': + data[ child.nodeName ] = { + id: child.getAttribute( 'texture' ), + extra: parseEffectParameterTexture( child ) + }; + break; + + } + + } + + return data; + + } + + function parseEffectParameterTexture( xml ) { + + const data = { + technique: {} + }; + + for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'extra': + parseEffectParameterTextureExtra( child, data ); + break; + + } + + } + + return data; + + } + + function parseEffectParameterTextureExtra( xml, data ) { + + for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'technique': + parseEffectParameterTextureExtraTechnique( child, data ); + break; + + } + + } + + } + + function parseEffectParameterTextureExtraTechnique( xml, data ) { + + for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'repeatU': + case 'repeatV': + case 'offsetU': + case 'offsetV': + data.technique[ child.nodeName ] = parseFloat( child.textContent ); + break; + + case 'wrapU': + case 'wrapV': + // some files have values for wrapU/wrapV which become NaN via parseInt + if ( child.textContent.toUpperCase() === 'TRUE' ) { + + data.technique[ child.nodeName ] = 1; + + } else if ( child.textContent.toUpperCase() === 'FALSE' ) { + + data.technique[ child.nodeName ] = 0; + + } else { + + data.technique[ child.nodeName ] = parseInt( child.textContent ); + + } + + break; + + } + + } + + } + + function parseEffectExtra( xml ) { + + const data = {}; + + for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'technique': + data.technique = parseEffectExtraTechnique( child ); + break; + + } + + } + + return data; + + } + + function parseEffectExtraTechnique( xml ) { + + const data = {}; + + for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'double_sided': + data[ child.nodeName ] = parseInt( child.textContent ); + break; + + } + + } + + return data; + + } + + function buildEffect( data ) { + + return data; + + } + + function getEffect( id ) { + + return getBuild( library.effects[ id ], buildEffect ); + + } // material + + + function parseMaterial( xml ) { + + const data = { + name: xml.getAttribute( 'name' ) + }; + + for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'instance_effect': + data.url = parseId( child.getAttribute( 'url' ) ); + break; + + } + + } + + library.materials[ xml.getAttribute( 'id' ) ] = data; + + } + + function getTextureLoader( image ) { + + let loader; + let extension = image.slice( ( image.lastIndexOf( '.' ) - 1 >>> 0 ) + 2 ); // http://www.jstips.co/en/javascript/get-file-extension/ + + extension = extension.toLowerCase(); + + switch ( extension ) { + + case 'tga': + loader = tgaLoader; + break; + + default: + loader = textureLoader; + + } + + return loader; + + } + + function buildMaterial( data ) { + + const effect = getEffect( data.url ); + const technique = effect.profile.technique; + const extra = effect.profile.extra; + let material; + + switch ( technique.type ) { + + case 'phong': + case 'blinn': + material = new THREE.MeshPhongMaterial(); + break; + + case 'lambert': + material = new THREE.MeshLambertMaterial(); + break; + + default: + material = new THREE.MeshBasicMaterial(); + break; + + } + + material.name = data.name || ''; + + function getTexture( textureObject ) { + + const sampler = effect.profile.samplers[ textureObject.id ]; + let image = null; // get image + + if ( sampler !== undefined ) { + + const surface = effect.profile.surfaces[ sampler.source ]; + image = getImage( surface.init_from ); + + } else { + + console.warn( 'THREE.ColladaLoader: Undefined sampler. Access image directly (see #12530).' ); + image = getImage( textureObject.id ); + + } // create texture if image is avaiable + + + if ( image !== null ) { + + const loader = getTextureLoader( image ); + + if ( loader !== undefined ) { + + const texture = loader.load( image ); + const extra = textureObject.extra; + + if ( extra !== undefined && extra.technique !== undefined && isEmpty( extra.technique ) === false ) { + + const technique = extra.technique; + texture.wrapS = technique.wrapU ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping; + texture.wrapT = technique.wrapV ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping; + texture.offset.set( technique.offsetU || 0, technique.offsetV || 0 ); + texture.repeat.set( technique.repeatU || 1, technique.repeatV || 1 ); + + } else { + + texture.wrapS = THREE.RepeatWrapping; + texture.wrapT = THREE.RepeatWrapping; + + } + + return texture; + + } else { + + console.warn( 'THREE.ColladaLoader: THREE.Loader for texture %s not found.', image ); + return null; + + } + + } else { + + console.warn( 'THREE.ColladaLoader: Couldn\'t create texture with ID:', textureObject.id ); + return null; + + } + + } + + const parameters = technique.parameters; + + for ( const key in parameters ) { + + const parameter = parameters[ key ]; + + switch ( key ) { + + case 'diffuse': + if ( parameter.color ) material.color.fromArray( parameter.color ); + if ( parameter.texture ) material.map = getTexture( parameter.texture ); + break; + + case 'specular': + if ( parameter.color && material.specular ) material.specular.fromArray( parameter.color ); + if ( parameter.texture ) material.specularMap = getTexture( parameter.texture ); + break; + + case 'bump': + if ( parameter.texture ) material.normalMap = getTexture( parameter.texture ); + break; + + case 'ambient': + if ( parameter.texture ) material.lightMap = getTexture( parameter.texture ); + break; + + case 'shininess': + if ( parameter.float && material.shininess ) material.shininess = parameter.float; + break; + + case 'emission': + if ( parameter.color && material.emissive ) material.emissive.fromArray( parameter.color ); + if ( parameter.texture ) material.emissiveMap = getTexture( parameter.texture ); + break; + + } + + } // + + + let transparent = parameters[ 'transparent' ]; + let transparency = parameters[ 'transparency' ]; // does not exist but + + if ( transparency === undefined && transparent ) { + + transparency = { + float: 1 + }; + + } // does not exist but + + + if ( transparent === undefined && transparency ) { + + transparent = { + opaque: 'A_ONE', + data: { + color: [ 1, 1, 1, 1 ] + } + }; + + } + + if ( transparent && transparency ) { + + // handle case if a texture exists but no color + if ( transparent.data.texture ) { + + // we do not set an alpha map (see #13792) + material.transparent = true; + + } else { + + const color = transparent.data.color; + + switch ( transparent.opaque ) { + + case 'A_ONE': + material.opacity = color[ 3 ] * transparency.float; + break; + + case 'RGB_ZERO': + material.opacity = 1 - color[ 0 ] * transparency.float; + break; + + case 'A_ZERO': + material.opacity = 1 - color[ 3 ] * transparency.float; + break; + + case 'RGB_ONE': + material.opacity = color[ 0 ] * transparency.float; + break; + + default: + console.warn( 'THREE.ColladaLoader: Invalid opaque type "%s" of transparent tag.', transparent.opaque ); + + } + + if ( material.opacity < 1 ) material.transparent = true; + + } + + } // + + + if ( extra !== undefined && extra.technique !== undefined && extra.technique.double_sided === 1 ) { + + material.side = THREE.DoubleSide; + + } + + return material; + + } + + function getMaterial( id ) { + + return getBuild( library.materials[ id ], buildMaterial ); + + } // camera + + + function parseCamera( xml ) { + + const data = { + name: xml.getAttribute( 'name' ) + }; + + for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'optics': + data.optics = parseCameraOptics( child ); + break; + + } + + } + + library.cameras[ xml.getAttribute( 'id' ) ] = data; + + } + + function parseCameraOptics( xml ) { + + for ( let i = 0; i < xml.childNodes.length; i ++ ) { + + const child = xml.childNodes[ i ]; + + switch ( child.nodeName ) { + + case 'technique_common': + return parseCameraTechnique( child ); + + } + + } + + return {}; + + } + + function parseCameraTechnique( xml ) { + + const data = {}; + + for ( let i = 0; i < xml.childNodes.length; i ++ ) { + + const child = xml.childNodes[ i ]; + + switch ( child.nodeName ) { + + case 'perspective': + case 'orthographic': + data.technique = child.nodeName; + data.parameters = parseCameraParameters( child ); + break; + + } + + } + + return data; + + } + + function parseCameraParameters( xml ) { + + const data = {}; + + for ( let i = 0; i < xml.childNodes.length; i ++ ) { + + const child = xml.childNodes[ i ]; + + switch ( child.nodeName ) { + + case 'xfov': + case 'yfov': + case 'xmag': + case 'ymag': + case 'znear': + case 'zfar': + case 'aspect_ratio': + data[ child.nodeName ] = parseFloat( child.textContent ); + break; + + } + + } + + return data; + + } + + function buildCamera( data ) { + + let camera; + + switch ( data.optics.technique ) { + + case 'perspective': + camera = new THREE.PerspectiveCamera( data.optics.parameters.yfov, data.optics.parameters.aspect_ratio, data.optics.parameters.znear, data.optics.parameters.zfar ); + break; + + case 'orthographic': + let ymag = data.optics.parameters.ymag; + let xmag = data.optics.parameters.xmag; + const aspectRatio = data.optics.parameters.aspect_ratio; + xmag = xmag === undefined ? ymag * aspectRatio : xmag; + ymag = ymag === undefined ? xmag / aspectRatio : ymag; + xmag *= 0.5; + ymag *= 0.5; + camera = new THREE.OrthographicCamera( - xmag, xmag, ymag, - ymag, // left, right, top, bottom + data.optics.parameters.znear, data.optics.parameters.zfar ); + break; + + default: + camera = new THREE.PerspectiveCamera(); + break; + + } + + camera.name = data.name || ''; + return camera; + + } + + function getCamera( id ) { + + const data = library.cameras[ id ]; + + if ( data !== undefined ) { + + return getBuild( data, buildCamera ); + + } + + console.warn( 'THREE.ColladaLoader: Couldn\'t find camera with ID:', id ); + return null; + + } // light + + + function parseLight( xml ) { + + let data = {}; + + for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'technique_common': + data = parseLightTechnique( child ); + break; + + } + + } + + library.lights[ xml.getAttribute( 'id' ) ] = data; + + } + + function parseLightTechnique( xml ) { + + const data = {}; + + for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'directional': + case 'point': + case 'spot': + case 'ambient': + data.technique = child.nodeName; + data.parameters = parseLightParameters( child ); + + } + + } + + return data; + + } + + function parseLightParameters( xml ) { + + const data = {}; + + for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'color': + const array = parseFloats( child.textContent ); + data.color = new THREE.Color().fromArray( array ); + break; + + case 'falloff_angle': + data.falloffAngle = parseFloat( child.textContent ); + break; + + case 'quadratic_attenuation': + const f = parseFloat( child.textContent ); + data.distance = f ? Math.sqrt( 1 / f ) : 0; + break; + + } + + } + + return data; + + } + + function buildLight( data ) { + + let light; + + switch ( data.technique ) { + + case 'directional': + light = new THREE.DirectionalLight(); + break; + + case 'point': + light = new THREE.PointLight(); + break; + + case 'spot': + light = new THREE.SpotLight(); + break; + + case 'ambient': + light = new THREE.AmbientLight(); + break; + + } + + if ( data.parameters.color ) light.color.copy( data.parameters.color ); + if ( data.parameters.distance ) light.distance = data.parameters.distance; + return light; + + } + + function getLight( id ) { + + const data = library.lights[ id ]; + + if ( data !== undefined ) { + + return getBuild( data, buildLight ); + + } + + console.warn( 'THREE.ColladaLoader: Couldn\'t find light with ID:', id ); + return null; + + } // geometry + + + function parseGeometry( xml ) { + + const data = { + name: xml.getAttribute( 'name' ), + sources: {}, + vertices: {}, + primitives: [] + }; + const mesh = getElementsByTagName( xml, 'mesh' )[ 0 ]; // the following tags inside geometry are not supported yet (see https://github.com/mrdoob/three.js/pull/12606): convex_mesh, spline, brep + + if ( mesh === undefined ) return; + + for ( let i = 0; i < mesh.childNodes.length; i ++ ) { + + const child = mesh.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + const id = child.getAttribute( 'id' ); + + switch ( child.nodeName ) { + + case 'source': + data.sources[ id ] = parseSource( child ); + break; + + case 'vertices': + // data.sources[ id ] = data.sources[ parseId( getElementsByTagName( child, 'input' )[ 0 ].getAttribute( 'source' ) ) ]; + data.vertices = parseGeometryVertices( child ); + break; + + case 'polygons': + console.warn( 'THREE.ColladaLoader: Unsupported primitive type: ', child.nodeName ); + break; + + case 'lines': + case 'linestrips': + case 'polylist': + case 'triangles': + data.primitives.push( parseGeometryPrimitive( child ) ); + break; + + default: + console.log( child ); + + } + + } + + library.geometries[ xml.getAttribute( 'id' ) ] = data; + + } + + function parseSource( xml ) { + + const data = { + array: [], + stride: 3 + }; + + for ( let i = 0; i < xml.childNodes.length; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'float_array': + data.array = parseFloats( child.textContent ); + break; + + case 'Name_array': + data.array = parseStrings( child.textContent ); + break; + + case 'technique_common': + const accessor = getElementsByTagName( child, 'accessor' )[ 0 ]; + + if ( accessor !== undefined ) { + + data.stride = parseInt( accessor.getAttribute( 'stride' ) ); + + } + + break; + + } + + } + + return data; + + } + + function parseGeometryVertices( xml ) { + + const data = {}; + + for ( let i = 0; i < xml.childNodes.length; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + data[ child.getAttribute( 'semantic' ) ] = parseId( child.getAttribute( 'source' ) ); + + } + + return data; + + } + + function parseGeometryPrimitive( xml ) { + + const primitive = { + type: xml.nodeName, + material: xml.getAttribute( 'material' ), + count: parseInt( xml.getAttribute( 'count' ) ), + inputs: {}, + stride: 0, + hasUV: false + }; + + for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'input': + const id = parseId( child.getAttribute( 'source' ) ); + const semantic = child.getAttribute( 'semantic' ); + const offset = parseInt( child.getAttribute( 'offset' ) ); + const set = parseInt( child.getAttribute( 'set' ) ); + const inputname = set > 0 ? semantic + set : semantic; + primitive.inputs[ inputname ] = { + id: id, + offset: offset + }; + primitive.stride = Math.max( primitive.stride, offset + 1 ); + if ( semantic === 'TEXCOORD' ) primitive.hasUV = true; + break; + + case 'vcount': + primitive.vcount = parseInts( child.textContent ); + break; + + case 'p': + primitive.p = parseInts( child.textContent ); + break; + + } + + } + + return primitive; + + } + + function groupPrimitives( primitives ) { + + const build = {}; + + for ( let i = 0; i < primitives.length; i ++ ) { + + const primitive = primitives[ i ]; + if ( build[ primitive.type ] === undefined ) build[ primitive.type ] = []; + build[ primitive.type ].push( primitive ); + + } + + return build; + + } + + function checkUVCoordinates( primitives ) { + + let count = 0; + + for ( let i = 0, l = primitives.length; i < l; i ++ ) { + + const primitive = primitives[ i ]; + + if ( primitive.hasUV === true ) { + + count ++; + + } + + } + + if ( count > 0 && count < primitives.length ) { + + primitives.uvsNeedsFix = true; + + } + + } + + function buildGeometry( data ) { + + const build = {}; + const sources = data.sources; + const vertices = data.vertices; + const primitives = data.primitives; + if ( primitives.length === 0 ) return {}; // our goal is to create one buffer geometry for a single type of primitives + // first, we group all primitives by their type + + const groupedPrimitives = groupPrimitives( primitives ); + + for ( const type in groupedPrimitives ) { + + const primitiveType = groupedPrimitives[ type ]; // second, ensure consistent uv coordinates for each type of primitives (polylist,triangles or lines) + + checkUVCoordinates( primitiveType ); // third, create a buffer geometry for each type of primitives + + build[ type ] = buildGeometryType( primitiveType, sources, vertices ); + + } + + return build; + + } + + function buildGeometryType( primitives, sources, vertices ) { + + const build = {}; + const position = { + array: [], + stride: 0 + }; + const normal = { + array: [], + stride: 0 + }; + const uv = { + array: [], + stride: 0 + }; + const uv2 = { + array: [], + stride: 0 + }; + const color = { + array: [], + stride: 0 + }; + const skinIndex = { + array: [], + stride: 4 + }; + const skinWeight = { + array: [], + stride: 4 + }; + const geometry = new THREE.BufferGeometry(); + const materialKeys = []; + let start = 0; + + for ( let p = 0; p < primitives.length; p ++ ) { + + const primitive = primitives[ p ]; + const inputs = primitive.inputs; // groups + + let count = 0; + + switch ( primitive.type ) { + + case 'lines': + case 'linestrips': + count = primitive.count * 2; + break; + + case 'triangles': + count = primitive.count * 3; + break; + + case 'polylist': + for ( let g = 0; g < primitive.count; g ++ ) { + + const vc = primitive.vcount[ g ]; + + switch ( vc ) { + + case 3: + count += 3; // single triangle + + break; + + case 4: + count += 6; // quad, subdivided into two triangles + + break; + + default: + count += ( vc - 2 ) * 3; // polylist with more than four vertices + + break; + + } + + } + + break; + + default: + console.warn( 'THREE.ColladaLoader: Unknow primitive type:', primitive.type ); + + } + + geometry.addGroup( start, count, p ); + start += count; // material + + if ( primitive.material ) { + + materialKeys.push( primitive.material ); + + } // geometry data + + + for ( const name in inputs ) { + + const input = inputs[ name ]; + + switch ( name ) { + + case 'VERTEX': + for ( const key in vertices ) { + + const id = vertices[ key ]; + + switch ( key ) { + + case 'POSITION': + const prevLength = position.array.length; + buildGeometryData( primitive, sources[ id ], input.offset, position.array ); + position.stride = sources[ id ].stride; + + if ( sources.skinWeights && sources.skinIndices ) { + + buildGeometryData( primitive, sources.skinIndices, input.offset, skinIndex.array ); + buildGeometryData( primitive, sources.skinWeights, input.offset, skinWeight.array ); + + } // see #3803 + + + if ( primitive.hasUV === false && primitives.uvsNeedsFix === true ) { + + const count = ( position.array.length - prevLength ) / position.stride; + + for ( let i = 0; i < count; i ++ ) { + + // fill missing uv coordinates + uv.array.push( 0, 0 ); + + } + + } + + break; + + case 'NORMAL': + buildGeometryData( primitive, sources[ id ], input.offset, normal.array ); + normal.stride = sources[ id ].stride; + break; + + case 'COLOR': + buildGeometryData( primitive, sources[ id ], input.offset, color.array ); + color.stride = sources[ id ].stride; + break; + + case 'TEXCOORD': + buildGeometryData( primitive, sources[ id ], input.offset, uv.array ); + uv.stride = sources[ id ].stride; + break; + + case 'TEXCOORD1': + buildGeometryData( primitive, sources[ id ], input.offset, uv2.array ); + uv.stride = sources[ id ].stride; + break; + + default: + console.warn( 'THREE.ColladaLoader: Semantic "%s" not handled in geometry build process.', key ); + + } + + } + + break; + + case 'NORMAL': + buildGeometryData( primitive, sources[ input.id ], input.offset, normal.array ); + normal.stride = sources[ input.id ].stride; + break; + + case 'COLOR': + buildGeometryData( primitive, sources[ input.id ], input.offset, color.array ); + color.stride = sources[ input.id ].stride; + break; + + case 'TEXCOORD': + buildGeometryData( primitive, sources[ input.id ], input.offset, uv.array ); + uv.stride = sources[ input.id ].stride; + break; + + case 'TEXCOORD1': + buildGeometryData( primitive, sources[ input.id ], input.offset, uv2.array ); + uv2.stride = sources[ input.id ].stride; + break; + + } + + } + + } // build geometry + + + if ( position.array.length > 0 ) geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( position.array, position.stride ) ); + if ( normal.array.length > 0 ) geometry.setAttribute( 'normal', new THREE.Float32BufferAttribute( normal.array, normal.stride ) ); + if ( color.array.length > 0 ) geometry.setAttribute( 'color', new THREE.Float32BufferAttribute( color.array, color.stride ) ); + if ( uv.array.length > 0 ) geometry.setAttribute( 'uv', new THREE.Float32BufferAttribute( uv.array, uv.stride ) ); + if ( uv2.array.length > 0 ) geometry.setAttribute( 'uv2', new THREE.Float32BufferAttribute( uv2.array, uv2.stride ) ); + if ( skinIndex.array.length > 0 ) geometry.setAttribute( 'skinIndex', new THREE.Float32BufferAttribute( skinIndex.array, skinIndex.stride ) ); + if ( skinWeight.array.length > 0 ) geometry.setAttribute( 'skinWeight', new THREE.Float32BufferAttribute( skinWeight.array, skinWeight.stride ) ); + build.data = geometry; + build.type = primitives[ 0 ].type; + build.materialKeys = materialKeys; + return build; + + } + + function buildGeometryData( primitive, source, offset, array ) { + + const indices = primitive.p; + const stride = primitive.stride; + const vcount = primitive.vcount; + + function pushVector( i ) { + + let index = indices[ i + offset ] * sourceStride; + const length = index + sourceStride; + + for ( ; index < length; index ++ ) { + + array.push( sourceArray[ index ] ); + + } + + } + + const sourceArray = source.array; + const sourceStride = source.stride; + + if ( primitive.vcount !== undefined ) { + + let index = 0; + + for ( let i = 0, l = vcount.length; i < l; i ++ ) { + + const count = vcount[ i ]; + + if ( count === 4 ) { + + const a = index + stride * 0; + const b = index + stride * 1; + const c = index + stride * 2; + const d = index + stride * 3; + pushVector( a ); + pushVector( b ); + pushVector( d ); + pushVector( b ); + pushVector( c ); + pushVector( d ); + + } else if ( count === 3 ) { + + const a = index + stride * 0; + const b = index + stride * 1; + const c = index + stride * 2; + pushVector( a ); + pushVector( b ); + pushVector( c ); + + } else if ( count > 4 ) { + + for ( let k = 1, kl = count - 2; k <= kl; k ++ ) { + + const a = index + stride * 0; + const b = index + stride * k; + const c = index + stride * ( k + 1 ); + pushVector( a ); + pushVector( b ); + pushVector( c ); + + } + + } + + index += stride * count; + + } + + } else { + + for ( let i = 0, l = indices.length; i < l; i += stride ) { + + pushVector( i ); + + } + + } + + } + + function getGeometry( id ) { + + return getBuild( library.geometries[ id ], buildGeometry ); + + } // kinematics + + + function parseKinematicsModel( xml ) { + + const data = { + name: xml.getAttribute( 'name' ) || '', + joints: {}, + links: [] + }; + + for ( let i = 0; i < xml.childNodes.length; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'technique_common': + parseKinematicsTechniqueCommon( child, data ); + break; + + } + + } + + library.kinematicsModels[ xml.getAttribute( 'id' ) ] = data; + + } + + function buildKinematicsModel( data ) { + + if ( data.build !== undefined ) return data.build; + return data; + + } + + function getKinematicsModel( id ) { + + return getBuild( library.kinematicsModels[ id ], buildKinematicsModel ); + + } + + function parseKinematicsTechniqueCommon( xml, data ) { + + for ( let i = 0; i < xml.childNodes.length; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'joint': + data.joints[ child.getAttribute( 'sid' ) ] = parseKinematicsJoint( child ); + break; + + case 'link': + data.links.push( parseKinematicsLink( child ) ); + break; + + } + + } + + } + + function parseKinematicsJoint( xml ) { + + let data; + + for ( let i = 0; i < xml.childNodes.length; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'prismatic': + case 'revolute': + data = parseKinematicsJointParameter( child ); + break; + + } + + } + + return data; + + } + + function parseKinematicsJointParameter( xml ) { + + const data = { + sid: xml.getAttribute( 'sid' ), + name: xml.getAttribute( 'name' ) || '', + axis: new THREE.Vector3(), + limits: { + min: 0, + max: 0 + }, + type: xml.nodeName, + static: false, + zeroPosition: 0, + middlePosition: 0 + }; + + for ( let i = 0; i < xml.childNodes.length; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'axis': + const array = parseFloats( child.textContent ); + data.axis.fromArray( array ); + break; + + case 'limits': + const max = child.getElementsByTagName( 'max' )[ 0 ]; + const min = child.getElementsByTagName( 'min' )[ 0 ]; + data.limits.max = parseFloat( max.textContent ); + data.limits.min = parseFloat( min.textContent ); + break; + + } + + } // if min is equal to or greater than max, consider the joint static + + + if ( data.limits.min >= data.limits.max ) { + + data.static = true; + + } // calculate middle position + + + data.middlePosition = ( data.limits.min + data.limits.max ) / 2.0; + return data; + + } + + function parseKinematicsLink( xml ) { + + const data = { + sid: xml.getAttribute( 'sid' ), + name: xml.getAttribute( 'name' ) || '', + attachments: [], + transforms: [] + }; + + for ( let i = 0; i < xml.childNodes.length; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'attachment_full': + data.attachments.push( parseKinematicsAttachment( child ) ); + break; + + case 'matrix': + case 'translate': + case 'rotate': + data.transforms.push( parseKinematicsTransform( child ) ); + break; + + } + + } + + return data; + + } + + function parseKinematicsAttachment( xml ) { + + const data = { + joint: xml.getAttribute( 'joint' ).split( '/' ).pop(), + transforms: [], + links: [] + }; + + for ( let i = 0; i < xml.childNodes.length; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'link': + data.links.push( parseKinematicsLink( child ) ); + break; + + case 'matrix': + case 'translate': + case 'rotate': + data.transforms.push( parseKinematicsTransform( child ) ); + break; + + } + + } + + return data; + + } + + function parseKinematicsTransform( xml ) { + + const data = { + type: xml.nodeName + }; + const array = parseFloats( xml.textContent ); + + switch ( data.type ) { + + case 'matrix': + data.obj = new THREE.Matrix4(); + data.obj.fromArray( array ).transpose(); + break; + + case 'translate': + data.obj = new THREE.Vector3(); + data.obj.fromArray( array ); + break; + + case 'rotate': + data.obj = new THREE.Vector3(); + data.obj.fromArray( array ); + data.angle = THREE.MathUtils.degToRad( array[ 3 ] ); + break; + + } + + return data; + + } // physics + + + function parsePhysicsModel( xml ) { + + const data = { + name: xml.getAttribute( 'name' ) || '', + rigidBodies: {} + }; + + for ( let i = 0; i < xml.childNodes.length; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'rigid_body': + data.rigidBodies[ child.getAttribute( 'name' ) ] = {}; + parsePhysicsRigidBody( child, data.rigidBodies[ child.getAttribute( 'name' ) ] ); + break; + + } + + } + + library.physicsModels[ xml.getAttribute( 'id' ) ] = data; + + } + + function parsePhysicsRigidBody( xml, data ) { + + for ( let i = 0; i < xml.childNodes.length; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'technique_common': + parsePhysicsTechniqueCommon( child, data ); + break; + + } + + } + + } + + function parsePhysicsTechniqueCommon( xml, data ) { + + for ( let i = 0; i < xml.childNodes.length; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'inertia': + data.inertia = parseFloats( child.textContent ); + break; + + case 'mass': + data.mass = parseFloats( child.textContent )[ 0 ]; + break; + + } + + } + + } // scene + + + function parseKinematicsScene( xml ) { + + const data = { + bindJointAxis: [] + }; + + for ( let i = 0; i < xml.childNodes.length; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'bind_joint_axis': + data.bindJointAxis.push( parseKinematicsBindJointAxis( child ) ); + break; + + } + + } + + library.kinematicsScenes[ parseId( xml.getAttribute( 'url' ) ) ] = data; + + } + + function parseKinematicsBindJointAxis( xml ) { + + const data = { + target: xml.getAttribute( 'target' ).split( '/' ).pop() + }; + + for ( let i = 0; i < xml.childNodes.length; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + + switch ( child.nodeName ) { + + case 'axis': + const param = child.getElementsByTagName( 'param' )[ 0 ]; + data.axis = param.textContent; + const tmpJointIndex = data.axis.split( 'inst_' ).pop().split( 'axis' )[ 0 ]; + data.jointIndex = tmpJointIndex.substr( 0, tmpJointIndex.length - 1 ); + break; + + } + + } + + return data; + + } + + function buildKinematicsScene( data ) { + + if ( data.build !== undefined ) return data.build; + return data; + + } + + function getKinematicsScene( id ) { + + return getBuild( library.kinematicsScenes[ id ], buildKinematicsScene ); + + } + + function setupKinematics() { + + const kinematicsModelId = Object.keys( library.kinematicsModels )[ 0 ]; + const kinematicsSceneId = Object.keys( library.kinematicsScenes )[ 0 ]; + const visualSceneId = Object.keys( library.visualScenes )[ 0 ]; + if ( kinematicsModelId === undefined || kinematicsSceneId === undefined ) return; + const kinematicsModel = getKinematicsModel( kinematicsModelId ); + const kinematicsScene = getKinematicsScene( kinematicsSceneId ); + const visualScene = getVisualScene( visualSceneId ); + const bindJointAxis = kinematicsScene.bindJointAxis; + const jointMap = {}; + + for ( let i = 0, l = bindJointAxis.length; i < l; i ++ ) { + + const axis = bindJointAxis[ i ]; // the result of the following query is an element of type 'translate', 'rotate','scale' or 'matrix' + + const targetElement = collada.querySelector( '[sid="' + axis.target + '"]' ); + + if ( targetElement ) { + + // get the parent of the transform element + const parentVisualElement = targetElement.parentElement; // connect the joint of the kinematics model with the element in the visual scene + + connect( axis.jointIndex, parentVisualElement ); + + } + + } + + function connect( jointIndex, visualElement ) { + + const visualElementName = visualElement.getAttribute( 'name' ); + const joint = kinematicsModel.joints[ jointIndex ]; + visualScene.traverse( function ( object ) { + + if ( object.name === visualElementName ) { + + jointMap[ jointIndex ] = { + object: object, + transforms: buildTransformList( visualElement ), + joint: joint, + position: joint.zeroPosition + }; + + } + + } ); + + } + + const m0 = new THREE.Matrix4(); + kinematics = { + joints: kinematicsModel && kinematicsModel.joints, + getJointValue: function ( jointIndex ) { + + const jointData = jointMap[ jointIndex ]; + + if ( jointData ) { + + return jointData.position; + + } else { + + console.warn( 'THREE.ColladaLoader: Joint ' + jointIndex + ' doesn\'t exist.' ); + + } + + }, + setJointValue: function ( jointIndex, value ) { + + const jointData = jointMap[ jointIndex ]; + + if ( jointData ) { + + const joint = jointData.joint; + + if ( value > joint.limits.max || value < joint.limits.min ) { + + console.warn( 'THREE.ColladaLoader: Joint ' + jointIndex + ' value ' + value + ' outside of limits (min: ' + joint.limits.min + ', max: ' + joint.limits.max + ').' ); + + } else if ( joint.static ) { + + console.warn( 'THREE.ColladaLoader: Joint ' + jointIndex + ' is static.' ); + + } else { + + const object = jointData.object; + const axis = joint.axis; + const transforms = jointData.transforms; + matrix.identity(); // each update, we have to apply all transforms in the correct order + + for ( let i = 0; i < transforms.length; i ++ ) { + + const transform = transforms[ i ]; // if there is a connection of the transform node with a joint, apply the joint value + + if ( transform.sid && transform.sid.indexOf( jointIndex ) !== - 1 ) { + + switch ( joint.type ) { + + case 'revolute': + matrix.multiply( m0.makeRotationAxis( axis, THREE.MathUtils.degToRad( value ) ) ); + break; + + case 'prismatic': + matrix.multiply( m0.makeTranslation( axis.x * value, axis.y * value, axis.z * value ) ); + break; + + default: + console.warn( 'THREE.ColladaLoader: Unknown joint type: ' + joint.type ); + break; + + } + + } else { + + switch ( transform.type ) { + + case 'matrix': + matrix.multiply( transform.obj ); + break; + + case 'translate': + matrix.multiply( m0.makeTranslation( transform.obj.x, transform.obj.y, transform.obj.z ) ); + break; + + case 'scale': + matrix.scale( transform.obj ); + break; + + case 'rotate': + matrix.multiply( m0.makeRotationAxis( transform.obj, transform.angle ) ); + break; + + } + + } + + } + + object.matrix.copy( matrix ); + object.matrix.decompose( object.position, object.quaternion, object.scale ); + jointMap[ jointIndex ].position = value; + + } + + } else { + + console.log( 'THREE.ColladaLoader: ' + jointIndex + ' does not exist.' ); + + } + + } + }; + + } + + function buildTransformList( node ) { + + const transforms = []; + const xml = collada.querySelector( '[id="' + node.id + '"]' ); + + for ( let i = 0; i < xml.childNodes.length; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + let array, vector; + + switch ( child.nodeName ) { + + case 'matrix': + array = parseFloats( child.textContent ); + const matrix = new THREE.Matrix4().fromArray( array ).transpose(); + transforms.push( { + sid: child.getAttribute( 'sid' ), + type: child.nodeName, + obj: matrix + } ); + break; + + case 'translate': + case 'scale': + array = parseFloats( child.textContent ); + vector = new THREE.Vector3().fromArray( array ); + transforms.push( { + sid: child.getAttribute( 'sid' ), + type: child.nodeName, + obj: vector + } ); + break; + + case 'rotate': + array = parseFloats( child.textContent ); + vector = new THREE.Vector3().fromArray( array ); + const angle = THREE.MathUtils.degToRad( array[ 3 ] ); + transforms.push( { + sid: child.getAttribute( 'sid' ), + type: child.nodeName, + obj: vector, + angle: angle + } ); + break; + + } + + } + + return transforms; + + } // nodes + + + function prepareNodes( xml ) { + + const elements = xml.getElementsByTagName( 'node' ); // ensure all node elements have id attributes + + for ( let i = 0; i < elements.length; i ++ ) { + + const element = elements[ i ]; + + if ( element.hasAttribute( 'id' ) === false ) { + + element.setAttribute( 'id', generateId() ); + + } + + } + + } + + const matrix = new THREE.Matrix4(); + const vector = new THREE.Vector3(); + + function parseNode( xml ) { + + const data = { + name: xml.getAttribute( 'name' ) || '', + type: xml.getAttribute( 'type' ), + id: xml.getAttribute( 'id' ), + sid: xml.getAttribute( 'sid' ), + matrix: new THREE.Matrix4(), + nodes: [], + instanceCameras: [], + instanceControllers: [], + instanceLights: [], + instanceGeometries: [], + instanceNodes: [], + transforms: {} + }; + + for ( let i = 0; i < xml.childNodes.length; i ++ ) { + + const child = xml.childNodes[ i ]; + if ( child.nodeType !== 1 ) continue; + let array; + + switch ( child.nodeName ) { + + case 'node': + data.nodes.push( child.getAttribute( 'id' ) ); + parseNode( child ); + break; + + case 'instance_camera': + data.instanceCameras.push( parseId( child.getAttribute( 'url' ) ) ); + break; + + case 'instance_controller': + data.instanceControllers.push( parseNodeInstance( child ) ); + break; + + case 'instance_light': + data.instanceLights.push( parseId( child.getAttribute( 'url' ) ) ); + break; + + case 'instance_geometry': + data.instanceGeometries.push( parseNodeInstance( child ) ); + break; + + case 'instance_node': + data.instanceNodes.push( parseId( child.getAttribute( 'url' ) ) ); + break; + + case 'matrix': + array = parseFloats( child.textContent ); + data.matrix.multiply( matrix.fromArray( array ).transpose() ); + data.transforms[ child.getAttribute( 'sid' ) ] = child.nodeName; + break; + + case 'translate': + array = parseFloats( child.textContent ); + vector.fromArray( array ); + data.matrix.multiply( matrix.makeTranslation( vector.x, vector.y, vector.z ) ); + data.transforms[ child.getAttribute( 'sid' ) ] = child.nodeName; + break; + + case 'rotate': + array = parseFloats( child.textContent ); + const angle = THREE.MathUtils.degToRad( array[ 3 ] ); + data.matrix.multiply( matrix.makeRotationAxis( vector.fromArray( array ), angle ) ); + data.transforms[ child.getAttribute( 'sid' ) ] = child.nodeName; + break; + + case 'scale': + array = parseFloats( child.textContent ); + data.matrix.scale( vector.fromArray( array ) ); + data.transforms[ child.getAttribute( 'sid' ) ] = child.nodeName; + break; + + case 'extra': + break; + + default: + console.log( child ); + + } + + } + + if ( hasNode( data.id ) ) { + + console.warn( 'THREE.ColladaLoader: There is already a node with ID %s. Exclude current node from further processing.', data.id ); + + } else { + + library.nodes[ data.id ] = data; + + } + + return data; + + } + + function parseNodeInstance( xml ) { + + const data = { + id: parseId( xml.getAttribute( 'url' ) ), + materials: {}, + skeletons: [] + }; + + for ( let i = 0; i < xml.childNodes.length; i ++ ) { + + const child = xml.childNodes[ i ]; + + switch ( child.nodeName ) { + + case 'bind_material': + const instances = child.getElementsByTagName( 'instance_material' ); + + for ( let j = 0; j < instances.length; j ++ ) { + + const instance = instances[ j ]; + const symbol = instance.getAttribute( 'symbol' ); + const target = instance.getAttribute( 'target' ); + data.materials[ symbol ] = parseId( target ); + + } + + break; + + case 'skeleton': + data.skeletons.push( parseId( child.textContent ) ); + break; + + default: + break; + + } + + } + + return data; + + } + + function buildSkeleton( skeletons, joints ) { + + const boneData = []; + const sortedBoneData = []; + let i, j, data; // a skeleton can have multiple root bones. collada expresses this + // situtation with multiple "skeleton" tags per controller instance + + for ( i = 0; i < skeletons.length; i ++ ) { + + const skeleton = skeletons[ i ]; + let root; + + if ( hasNode( skeleton ) ) { + + root = getNode( skeleton ); + buildBoneHierarchy( root, joints, boneData ); + + } else if ( hasVisualScene( skeleton ) ) { + + // handle case where the skeleton refers to the visual scene (#13335) + const visualScene = library.visualScenes[ skeleton ]; + const children = visualScene.children; + + for ( let j = 0; j < children.length; j ++ ) { + + const child = children[ j ]; + + if ( child.type === 'JOINT' ) { + + const root = getNode( child.id ); + buildBoneHierarchy( root, joints, boneData ); + + } + + } + + } else { + + console.error( 'THREE.ColladaLoader: Unable to find root bone of skeleton with ID:', skeleton ); + + } + + } // sort bone data (the order is defined in the corresponding controller) + + + for ( i = 0; i < joints.length; i ++ ) { + + for ( j = 0; j < boneData.length; j ++ ) { + + data = boneData[ j ]; + + if ( data.bone.name === joints[ i ].name ) { + + sortedBoneData[ i ] = data; + data.processed = true; + break; + + } + + } + + } // add unprocessed bone data at the end of the list + + + for ( i = 0; i < boneData.length; i ++ ) { + + data = boneData[ i ]; + + if ( data.processed === false ) { + + sortedBoneData.push( data ); + data.processed = true; + + } + + } // setup arrays for skeleton creation + + + const bones = []; + const boneInverses = []; + + for ( i = 0; i < sortedBoneData.length; i ++ ) { + + data = sortedBoneData[ i ]; + bones.push( data.bone ); + boneInverses.push( data.boneInverse ); + + } + + return new THREE.Skeleton( bones, boneInverses ); + + } + + function buildBoneHierarchy( root, joints, boneData ) { + + // setup bone data from visual scene + root.traverse( function ( object ) { + + if ( object.isBone === true ) { + + let boneInverse; // retrieve the boneInverse from the controller data + + for ( let i = 0; i < joints.length; i ++ ) { + + const joint = joints[ i ]; + + if ( joint.name === object.name ) { + + boneInverse = joint.boneInverse; + break; + + } + + } + + if ( boneInverse === undefined ) { + + // Unfortunately, there can be joints in the visual scene that are not part of the + // corresponding controller. In this case, we have to create a dummy boneInverse matrix + // for the respective bone. This bone won't affect any vertices, because there are no skin indices + // and weights defined for it. But we still have to add the bone to the sorted bone list in order to + // ensure a correct animation of the model. + boneInverse = new THREE.Matrix4(); + + } + + boneData.push( { + bone: object, + boneInverse: boneInverse, + processed: false + } ); + + } + + } ); + + } + + function buildNode( data ) { + + const objects = []; + const matrix = data.matrix; + const nodes = data.nodes; + const type = data.type; + const instanceCameras = data.instanceCameras; + const instanceControllers = data.instanceControllers; + const instanceLights = data.instanceLights; + const instanceGeometries = data.instanceGeometries; + const instanceNodes = data.instanceNodes; // nodes + + for ( let i = 0, l = nodes.length; i < l; i ++ ) { + + objects.push( getNode( nodes[ i ] ) ); + + } // instance cameras + + + for ( let i = 0, l = instanceCameras.length; i < l; i ++ ) { + + const instanceCamera = getCamera( instanceCameras[ i ] ); + + if ( instanceCamera !== null ) { + + objects.push( instanceCamera.clone() ); + + } + + } // instance controllers + + + for ( let i = 0, l = instanceControllers.length; i < l; i ++ ) { + + const instance = instanceControllers[ i ]; + const controller = getController( instance.id ); + const geometries = getGeometry( controller.id ); + const newObjects = buildObjects( geometries, instance.materials ); + const skeletons = instance.skeletons; + const joints = controller.skin.joints; + const skeleton = buildSkeleton( skeletons, joints ); + + for ( let j = 0, jl = newObjects.length; j < jl; j ++ ) { + + const object = newObjects[ j ]; + + if ( object.isSkinnedMesh ) { + + object.bind( skeleton, controller.skin.bindMatrix ); + object.normalizeSkinWeights(); + + } + + objects.push( object ); + + } + + } // instance lights + + + for ( let i = 0, l = instanceLights.length; i < l; i ++ ) { + + const instanceLight = getLight( instanceLights[ i ] ); + + if ( instanceLight !== null ) { + + objects.push( instanceLight.clone() ); + + } + + } // instance geometries + + + for ( let i = 0, l = instanceGeometries.length; i < l; i ++ ) { + + const instance = instanceGeometries[ i ]; // a single geometry instance in collada can lead to multiple object3Ds. + // this is the case when primitives are combined like triangles and lines + + const geometries = getGeometry( instance.id ); + const newObjects = buildObjects( geometries, instance.materials ); + + for ( let j = 0, jl = newObjects.length; j < jl; j ++ ) { + + objects.push( newObjects[ j ] ); + + } + + } // instance nodes + + + for ( let i = 0, l = instanceNodes.length; i < l; i ++ ) { + + objects.push( getNode( instanceNodes[ i ] ).clone() ); + + } + + let object; + + if ( nodes.length === 0 && objects.length === 1 ) { + + object = objects[ 0 ]; + + } else { + + object = type === 'JOINT' ? new THREE.Bone() : new THREE.Group(); + + for ( let i = 0; i < objects.length; i ++ ) { + + object.add( objects[ i ] ); + + } + + } + + object.name = type === 'JOINT' ? data.sid : data.name; + object.matrix.copy( matrix ); + object.matrix.decompose( object.position, object.quaternion, object.scale ); + return object; + + } + + const fallbackMaterial = new THREE.MeshBasicMaterial( { + color: 0xff00ff + } ); + + function resolveMaterialBinding( keys, instanceMaterials ) { + + const materials = []; + + for ( let i = 0, l = keys.length; i < l; i ++ ) { + + const id = instanceMaterials[ keys[ i ] ]; + + if ( id === undefined ) { + + console.warn( 'THREE.ColladaLoader: Material with key %s not found. Apply fallback material.', keys[ i ] ); + materials.push( fallbackMaterial ); + + } else { + + materials.push( getMaterial( id ) ); + + } + + } + + return materials; + + } + + function buildObjects( geometries, instanceMaterials ) { + + const objects = []; + + for ( const type in geometries ) { + + const geometry = geometries[ type ]; + const materials = resolveMaterialBinding( geometry.materialKeys, instanceMaterials ); // handle case if no materials are defined + + if ( materials.length === 0 ) { + + if ( type === 'lines' || type === 'linestrips' ) { + + materials.push( new THREE.LineBasicMaterial() ); + + } else { + + materials.push( new THREE.MeshPhongMaterial() ); + + } + + } // regard skinning + + + const skinning = geometry.data.attributes.skinIndex !== undefined; // choose between a single or multi materials (material array) + + const material = materials.length === 1 ? materials[ 0 ] : materials; // now create a specific 3D object + + let object; + + switch ( type ) { + + case 'lines': + object = new THREE.LineSegments( geometry.data, material ); + break; + + case 'linestrips': + object = new THREE.Line( geometry.data, material ); + break; + + case 'triangles': + case 'polylist': + if ( skinning ) { + + object = new THREE.SkinnedMesh( geometry.data, material ); + + } else { + + object = new THREE.Mesh( geometry.data, material ); + + } + + break; + + } + + objects.push( object ); + + } + + return objects; + + } + + function hasNode( id ) { + + return library.nodes[ id ] !== undefined; + + } + + function getNode( id ) { + + return getBuild( library.nodes[ id ], buildNode ); + + } // visual scenes + + + function parseVisualScene( xml ) { + + const data = { + name: xml.getAttribute( 'name' ), + children: [] + }; + prepareNodes( xml ); + const elements = getElementsByTagName( xml, 'node' ); + + for ( let i = 0; i < elements.length; i ++ ) { + + data.children.push( parseNode( elements[ i ] ) ); + + } + + library.visualScenes[ xml.getAttribute( 'id' ) ] = data; + + } + + function buildVisualScene( data ) { + + const group = new THREE.Group(); + group.name = data.name; + const children = data.children; + + for ( let i = 0; i < children.length; i ++ ) { + + const child = children[ i ]; + group.add( getNode( child.id ) ); + + } + + return group; + + } + + function hasVisualScene( id ) { + + return library.visualScenes[ id ] !== undefined; + + } + + function getVisualScene( id ) { + + return getBuild( library.visualScenes[ id ], buildVisualScene ); + + } // scenes + + + function parseScene( xml ) { + + const instance = getElementsByTagName( xml, 'instance_visual_scene' )[ 0 ]; + return getVisualScene( parseId( instance.getAttribute( 'url' ) ) ); + + } + + function setupAnimations() { + + const clips = library.clips; + + if ( isEmpty( clips ) === true ) { + + if ( isEmpty( library.animations ) === false ) { + + // if there are animations but no clips, we create a default clip for playback + const tracks = []; + + for ( const id in library.animations ) { + + const animationTracks = getAnimation( id ); + + for ( let i = 0, l = animationTracks.length; i < l; i ++ ) { + + tracks.push( animationTracks[ i ] ); + + } + + } + + animations.push( new THREE.AnimationClip( 'default', - 1, tracks ) ); + + } + + } else { + + for ( const id in clips ) { + + animations.push( getAnimationClip( id ) ); + + } + + } + + } // convert the parser error element into text with each child elements text + // separated by new lines. + + + function parserErrorToText( parserError ) { + + let result = ''; + const stack = [ parserError ]; + + while ( stack.length ) { + + const node = stack.shift(); + + if ( node.nodeType === Node.TEXT_NODE ) { + + result += node.textContent; + + } else { + + result += '\n'; + stack.push.apply( stack, node.childNodes ); + + } + + } + + return result.trim(); + + } + + if ( text.length === 0 ) { + + return { + scene: new THREE.Scene() + }; + + } + + const xml = new DOMParser().parseFromString( text, 'application/xml' ); + const collada = getElementsByTagName( xml, 'COLLADA' )[ 0 ]; + const parserError = xml.getElementsByTagName( 'parsererror' )[ 0 ]; + + if ( parserError !== undefined ) { + + // Chrome will return parser error with a div in it + const errorElement = getElementsByTagName( parserError, 'div' )[ 0 ]; + let errorText; + + if ( errorElement ) { + + errorText = errorElement.textContent; + + } else { + + errorText = parserErrorToText( parserError ); + + } + + console.error( 'THREE.ColladaLoader: Failed to parse collada file.\n', errorText ); + return null; + + } // metadata + + + const version = collada.getAttribute( 'version' ); + console.log( 'THREE.ColladaLoader: File version', version ); + const asset = parseAsset( getElementsByTagName( collada, 'asset' )[ 0 ] ); + const textureLoader = new THREE.TextureLoader( this.manager ); + textureLoader.setPath( this.resourcePath || path ).setCrossOrigin( this.crossOrigin ); + let tgaLoader; + + if ( THREE.TGALoader ) { + + tgaLoader = new THREE.TGALoader( this.manager ); + tgaLoader.setPath( this.resourcePath || path ); + + } // + + + const animations = []; + let kinematics = {}; + let count = 0; // + + const library = { + animations: {}, + clips: {}, + controllers: {}, + images: {}, + effects: {}, + materials: {}, + cameras: {}, + lights: {}, + geometries: {}, + nodes: {}, + visualScenes: {}, + kinematicsModels: {}, + physicsModels: {}, + kinematicsScenes: {} + }; + parseLibrary( collada, 'library_animations', 'animation', parseAnimation ); + parseLibrary( collada, 'library_animation_clips', 'animation_clip', parseAnimationClip ); + parseLibrary( collada, 'library_controllers', 'controller', parseController ); + parseLibrary( collada, 'library_images', 'image', parseImage ); + parseLibrary( collada, 'library_effects', 'effect', parseEffect ); + parseLibrary( collada, 'library_materials', 'material', parseMaterial ); + parseLibrary( collada, 'library_cameras', 'camera', parseCamera ); + parseLibrary( collada, 'library_lights', 'light', parseLight ); + parseLibrary( collada, 'library_geometries', 'geometry', parseGeometry ); + parseLibrary( collada, 'library_nodes', 'node', parseNode ); + parseLibrary( collada, 'library_visual_scenes', 'visual_scene', parseVisualScene ); + parseLibrary( collada, 'library_kinematics_models', 'kinematics_model', parseKinematicsModel ); + parseLibrary( collada, 'library_physics_models', 'physics_model', parsePhysicsModel ); + parseLibrary( collada, 'scene', 'instance_kinematics_scene', parseKinematicsScene ); + buildLibrary( library.animations, buildAnimation ); + buildLibrary( library.clips, buildAnimationClip ); + buildLibrary( library.controllers, buildController ); + buildLibrary( library.images, buildImage ); + buildLibrary( library.effects, buildEffect ); + buildLibrary( library.materials, buildMaterial ); + buildLibrary( library.cameras, buildCamera ); + buildLibrary( library.lights, buildLight ); + buildLibrary( library.geometries, buildGeometry ); + buildLibrary( library.visualScenes, buildVisualScene ); + setupAnimations(); + setupKinematics(); + const scene = parseScene( getElementsByTagName( collada, 'scene' )[ 0 ] ); + scene.animations = animations; + + if ( asset.upAxis === 'Z_UP' ) { + + scene.quaternion.setFromEuler( new THREE.Euler( - Math.PI / 2, 0, 0 ) ); + + } + + scene.scale.multiplyScalar( asset.unit ); + return { + get animations() { + + console.warn( 'THREE.ColladaLoader: Please access animations over scene.animations now.' ); + return animations; + + }, + + kinematics: kinematics, + library: library, + scene: scene + }; + + } + + } + + THREE.ColladaLoader = ColladaLoader; + +} )(); diff --git a/source/threejs/threeimporter.js b/source/threejs/threeimporter.js index e580ae4..4200aea 100644 --- a/source/threejs/threeimporter.js +++ b/source/threejs/threeimporter.js @@ -1,21 +1,130 @@ -OV.ThreeImporter = class extends OV.ImporterBase +OV.ThreeLoader = class +{ + constructor () + { + + } + + GetExtension () + { + return null; + } + + GetExternalLibraries () + { + return null; + } + + CreateLoader (manager) + { + return null; + } + + EnumerateMeshes (loadedObject, processor) + { + + } +}; + +OV.ThreeLoaderFbx = class extends OV.ThreeLoader { constructor () { super (); } + GetExtension () + { + return 'fbx'; + } + + GetExternalLibraries () + { + return [ + 'three_loaders/fflate.min.js', + 'three_loaders/FBXLoader.js' + ]; + } + + CreateLoader (manager) + { + return new THREE.FBXLoader (manager); + } + + EnumerateMeshes (loadedObject, processor) + { + loadedObject.traverse ((child) => { + if (child.isMesh) { + processor (child); + } + }); + } +}; + +OV.ThreeLoaderDae = class extends OV.ThreeLoader +{ + constructor () + { + super (); + } + + GetExtension () + { + return 'dae'; + } + + GetExternalLibraries () + { + return [ + 'three_loaders/ColladaLoader.js' + ]; + } + + CreateLoader (manager) + { + return new THREE.ColladaLoader (manager); + } + + EnumerateMeshes (loadedObject, processor) + { + loadedObject.scene.traverse ((child) => { + if (child.isMesh) { + processor (child); + } + }); + } +}; + +OV.ThreeImporter = class extends OV.ImporterBase +{ + constructor () + { + super (); + this.loaders = [ + new OV.ThreeLoaderFbx (), + new OV.ThreeLoaderDae () + ]; + } + CanImportExtension (extension) { - const knownExtensions = ['fbx']; - return knownExtensions.indexOf (extension) !== -1; + for (let i = 0; i < this.loaders.length; i++) { + let loader = this.loaders[i]; + if (loader.GetExtension () === extension) { + return true; + } + } + return false; } GetKnownFileFormats () { - return { - 'fbx' : OV.FileFormat.Binary - }; + let result = {}; + for (let i = 0; i < this.loaders.length; i++) { + let loader = this.loaders[i]; + result[loader.GetExtension ()] = OV.FileFormat.Binary; + } + return result; } GetUpDirection () @@ -25,19 +134,12 @@ OV.ThreeImporter = class extends OV.ImporterBase ClearContent () { - if (this.loadedScene !== undefined && this.loadedScene !== null) { - this.loadedScene.traverse ((child) => { - if (child.isMesh) { - child.geometry.dispose (); - } - }); - this.loadedScene = null; - } + } ResetContent () { - this.loadedScene = null; + } ImportContent (fileContent, onFinish) @@ -54,50 +156,54 @@ OV.ThreeImporter = class extends OV.ImporterBase onFinish (); } - const libraries = this.GetExternalLibraries (); + const loader = this.FindLoader (); + if (loader === null) { + onFinish (); + return; + } + + const libraries = this.GetExternalLibraries (loader); if (libraries === null) { onFinish (); return; } LoadLibraries (libraries, () => { - this.LoadModel (fileContent, onFinish); + this.LoadModel (loader, fileContent, onFinish); }, () => { onFinish (); }); } - GetExternalLibraries () + FindLoader () + { + for (let i = 0; i < this.loaders.length; i++) { + let loader = this.loaders[i]; + if (loader.GetExtension () === this.extension) { + return loader; + } + } + return null; + } + + GetExternalLibraries (loader) { let libraries = [ 'three_loaders/TGALoader.js' ]; - if (this.extension === 'fbx') { - libraries.push ( - 'three_loaders/fflate.min.js', - 'three_loaders/FBXLoader.js' - ); - } else { - return null; + let loaderLibraries = loader.GetExternalLibraries (); + for (let i = 0; i < loaderLibraries.length; i++) { + libraries.push (loaderLibraries[i]); } return libraries; } - CreateLoader (manager) + LoadModel (loader, fileContent, onFinish) { - let loader = null; - if (this.extension === 'fbx') { - loader = new THREE.FBXLoader (manager); - } - return loader; - } - - LoadModel (fileContent, onFinish) - { - let loadedScene = null; + let loadedObject = null; let externalFileNames = {}; let loadingManager = new THREE.LoadingManager (() => { - if (loadedScene !== null) { - this.OnThreeObjectsLoaded (loadedScene, externalFileNames, onFinish); + if (loadedObject !== null) { + this.OnThreeObjectsLoaded (loader, loadedObject, externalFileNames, onFinish); } }); // TODO @@ -123,15 +229,15 @@ OV.ThreeImporter = class extends OV.ImporterBase return url; }); - const loader = this.CreateLoader (loadingManager); - if (loader === null) { + const threeLoader = loader.CreateLoader (loadingManager); + if (threeLoader === null) { onFinish (); return; } - loader.load (mainFileUrl, + threeLoader.load (mainFileUrl, (object) => { - loadedScene = object; + loadedObject = object; }, () => { }, @@ -143,7 +249,7 @@ OV.ThreeImporter = class extends OV.ImporterBase ); } - OnThreeObjectsLoaded (loadedScene, externalFileNames, onFinish) + OnThreeObjectsLoaded (loader, loadedObject, externalFileNames, onFinish) { function ConvertThreeMaterialToMaterial (threeMaterial, externalFileNames) { @@ -227,47 +333,46 @@ OV.ThreeImporter = class extends OV.ImporterBase } let materialIdToIndex = {}; - this.loadedScene = loadedScene; - loadedScene.traverse ((child) => { - if (child.isMesh) { - let materialIndex = null; - let mesh = null; - if (Array.isArray (child.material)) { - mesh = OV.ConvertThreeGeometryToMesh (child.geometry, null); - if (child.geometry.attributes.color === undefined || child.geometry.attributes.color === null) { - let materialIndices = []; - for (let i = 0; i < child.material.length; i++) { - let material = child.material[i]; - materialIndices.push (FindMatchingMaterialIndex (this.model, material, materialIdToIndex, externalFileNames)); - } - for (let i = 0; i < child.geometry.groups.length; i++) { - let group = child.geometry.groups[i]; - for (let j = group.start / 3; j < group.start / 3 + group.count / 3; j++) { - let triangle = mesh.GetTriangle (j); - triangle.SetMaterial (materialIndices[group.materialIndex]); - } + loader.EnumerateMeshes (loadedObject, (child) => { + let materialIndex = null; + let mesh = null; + if (Array.isArray (child.material)) { + mesh = OV.ConvertThreeGeometryToMesh (child.geometry, null); + if (child.geometry.attributes.color === undefined || child.geometry.attributes.color === null) { + let materialIndices = []; + for (let i = 0; i < child.material.length; i++) { + let material = child.material[i]; + materialIndices.push (FindMatchingMaterialIndex (this.model, material, materialIdToIndex, externalFileNames)); + } + for (let i = 0; i < child.geometry.groups.length; i++) { + let group = child.geometry.groups[i]; + for (let j = group.start / 3; j < group.start / 3 + group.count / 3; j++) { + let triangle = mesh.GetTriangle (j); + triangle.SetMaterial (materialIndices[group.materialIndex]); } } - } else { - materialIndex = FindMatchingMaterialIndex (this.model, child.material, materialIdToIndex, externalFileNames); - mesh = OV.ConvertThreeGeometryToMesh (child.geometry, materialIndex); } - if (child.name !== undefined && child.name !== null) { - mesh.SetName (child.name); - } - if (child.matrixWorld !== undefined && child.matrixWorld !== null) { - const matrix = new OV.Matrix (child.matrixWorld.elements); - const transformation = new OV.Transformation (matrix); - const determinant = matrix.Determinant (); - const mirrorByX = OV.IsNegative (determinant); - OV.TransformMesh (mesh, transformation); - if (mirrorByX) { - OV.FlipMeshTrianglesOrientation (mesh); - } - } - this.model.AddMesh (mesh); + } else { + materialIndex = FindMatchingMaterialIndex (this.model, child.material, materialIdToIndex, externalFileNames); + mesh = OV.ConvertThreeGeometryToMesh (child.geometry, materialIndex); } + if (child.name !== undefined && child.name !== null) { + mesh.SetName (child.name); + } + if (child.matrixWorld !== undefined && child.matrixWorld !== null) { + const matrix = new OV.Matrix (child.matrixWorld.elements); + const transformation = new OV.Transformation (matrix); + const determinant = matrix.Determinant (); + const mirrorByX = OV.IsNegative (determinant); + OV.TransformMesh (mesh, transformation); + if (mirrorByX) { + OV.FlipMeshTrianglesOrientation (mesh); + } + } + this.model.AddMesh (mesh); + child.geometry.dispose (); }); + onFinish (); } };