Просмотр исходного кода

ColladaLoader: Improve animation support. (#32830)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
mrdoob 1 месяц назад
Родитель
Сommit
16cbcb819d

+ 819 - 80
examples/jsm/loaders/collada/ColladaComposer.js

@@ -11,6 +11,8 @@ import {
 	Float32BufferAttribute,
 	FrontSide,
 	Group,
+	InterpolateBezier,
+	InterpolateDiscrete,
 	Line,
 	LineBasicMaterial,
 	LineSegments,
@@ -60,6 +62,15 @@ class ColladaComposer {
 		this.quaternion = new Quaternion();
 		this.matrix = new Matrix4();
 
+		// Storage for deferred pivot animation data
+		// Nodes with pivot transforms need all their animation channels collected
+		// before building tracks, as channels may be split across animation elements
+		this.deferredPivotAnimations = {};
+
+		// Storage for transform node hierarchy
+		// Maps nodeId -> transformSid -> Object3D for animation targeting
+		this.transformNodes = {};
+
 	}
 
 	compose() {
@@ -102,8 +113,6 @@ class ColladaComposer {
 
 	}
 
-	// get
-
 	getBuild( data, builder ) {
 
 		if ( data.build !== undefined ) return data.build;
@@ -120,8 +129,6 @@ class ColladaComposer {
 
 	}
 
-	// animation
-
 	buildAnimation( data ) {
 
 		const tracks = [];
@@ -130,22 +137,58 @@ class ColladaComposer {
 		const samplers = data.samplers;
 		const sources = data.sources;
 
-		for ( const target in channels ) {
+		const aggregated = this.aggregateAnimationChannels( channels, samplers, sources );
+
+		for ( const nodeId in aggregated ) {
+
+			const nodeData = this.library.nodes[ nodeId ];
+			if ( ! nodeData ) continue;
+
+			const nodeChannels = aggregated[ nodeId ];
+
+			if ( this.hasPivotTransforms( nodeData ) ) {
+
+				// Defer - nodes haven't been built yet
+				this.collectDeferredPivotAnimation( nodeId, nodeChannels );
+
+			} else {
+
+				const object3D = this.getNode( nodeId );
+				let rotationTrackBuilt = false;
 
-			if ( channels.hasOwnProperty( target ) ) {
+				for ( const sid in nodeChannels ) {
 
-				const channel = channels[ target ];
-				const sampler = samplers[ channel.sampler ];
+					const transformType = nodeData.transforms[ sid ];
+					const transformInfo = nodeData.transformData[ sid ];
+					const channelData = nodeChannels[ sid ];
 
-				const inputId = sampler.inputs.INPUT;
-				const outputId = sampler.inputs.OUTPUT;
+					switch ( transformType ) {
 
-				const inputSource = sources[ inputId ];
-				const outputSource = sources[ outputId ];
+						case 'matrix':
+							this.buildMatrixTracks( object3D, channelData, nodeData, tracks );
+							break;
 
-				const animation = this.buildAnimationChannel( channel, inputSource, outputSource );
+						case 'translate':
+							this.buildTranslateTrack( object3D, channelData, transformInfo, tracks );
+							break;
 
-				this.createKeyframeTracks( animation, tracks );
+						case 'rotate':
+							if ( ! rotationTrackBuilt ) {
+
+								this.buildRotateTrack( object3D, sid, channelData, transformInfo, nodeData, tracks );
+								rotationTrackBuilt = true;
+
+							}
+
+							break;
+
+						case 'scale':
+							this.buildScaleTrack( object3D, channelData, transformInfo, tracks );
+							break;
+
+					}
+
+				}
 
 			}
 
@@ -155,119 +198,559 @@ class ColladaComposer {
 
 	}
 
+	collectDeferredPivotAnimation( nodeId, nodeChannels ) {
+
+		if ( ! this.deferredPivotAnimations[ nodeId ] ) {
+
+			this.deferredPivotAnimations[ nodeId ] = {};
+
+		}
+
+		const deferred = this.deferredPivotAnimations[ nodeId ];
+
+		for ( const sid in nodeChannels ) {
+
+			if ( ! deferred[ sid ] ) {
+
+				deferred[ sid ] = {};
+
+			}
+
+			for ( const member in nodeChannels[ sid ] ) {
+
+				deferred[ sid ][ member ] = nodeChannels[ sid ][ member ];
+
+			}
+
+		}
+
+	}
+
+	hasPivotTransforms( nodeData ) {
+
+		const pivotSids = [
+			'rotatePivot', 'rotatePivotInverse', 'rotatePivotTranslation',
+			'scalePivot', 'scalePivotInverse', 'scalePivotTranslation'
+		];
+
+		for ( const sid of pivotSids ) {
+
+			if ( nodeData.transforms[ sid ] !== undefined ) {
+
+				return true;
+
+			}
+
+		}
+
+		return false;
+
+	}
+
 	getAnimation( id ) {
 
 		return this.getBuild( this.library.animations[ id ], this.buildAnimation.bind( this ) );
 
 	}
 
-	buildAnimationChannel( channel, inputSource, outputSource ) {
+	aggregateAnimationChannels( channels, samplers, sources ) {
 
-		const node = this.library.nodes[ channel.id ];
-		const object3D = this.getNode( node.id );
+		const aggregated = {};
 
-		const transform = node.transforms[ channel.sid ];
-		const defaultMatrix = node.matrix.clone().transpose();
+		for ( const target in channels ) {
 
-		let time, stride;
-		let i, il, j, jl;
+			if ( ! channels.hasOwnProperty( target ) ) continue;
 
-		const data = {};
+			const channel = channels[ target ];
+			const sampler = samplers[ channel.sampler ];
 
-		// the collada spec allows the animation of data in various ways.
-		// depending on the transform type (matrix, translate, rotate, scale), we execute different logic
+			const inputId = sampler.inputs.INPUT;
+			const outputId = sampler.inputs.OUTPUT;
 
-		switch ( transform ) {
+			const inputSource = sources[ inputId ];
+			const outputSource = sources[ outputId ];
 
-			case 'matrix':
+			const interpolationId = sampler.inputs.INTERPOLATION;
+			const inTangentId = sampler.inputs.IN_TANGENT;
+			const outTangentId = sampler.inputs.OUT_TANGENT;
 
-				for ( i = 0, il = inputSource.array.length; i < il; i ++ ) {
+			const interpolationSource = interpolationId ? sources[ interpolationId ] : null;
+			const inTangentSource = inTangentId ? sources[ inTangentId ] : null;
+			const outTangentSource = outTangentId ? sources[ outTangentId ] : null;
 
-					time = inputSource.array[ i ];
-					stride = i * outputSource.stride;
+			const nodeId = channel.id;
+			const sid = channel.sid;
+			const member = channel.member || 'default';
 
-					if ( data[ time ] === undefined ) data[ time ] = {};
+			if ( ! aggregated[ nodeId ] ) aggregated[ nodeId ] = {};
+			if ( ! aggregated[ nodeId ][ sid ] ) aggregated[ nodeId ][ sid ] = {};
 
-					if ( channel.arraySyntax === true ) {
+			aggregated[ nodeId ][ sid ][ member ] = {
+				times: inputSource.array,
+				values: outputSource.array,
+				stride: outputSource.stride,
+				arraySyntax: channel.arraySyntax,
+				indices: channel.indices,
+				interpolation: interpolationSource ? interpolationSource.array : null,
+				inTangent: inTangentSource ? inTangentSource.array : null,
+				outTangent: outTangentSource ? outTangentSource.array : null,
+				inTangentStride: inTangentSource ? inTangentSource.stride : 0,
+				outTangentStride: outTangentSource ? outTangentSource.stride : 0
+			};
 
-						const value = outputSource.array[ stride ];
-						const index = channel.indices[ 0 ] + 4 * channel.indices[ 1 ];
+		}
 
-						data[ time ][ index ] = value;
+		return aggregated;
 
-					} else {
+	}
 
-						for ( j = 0, jl = outputSource.stride; j < jl; j ++ ) {
+	buildMatrixTracks( object3D, channelData, nodeData, tracks ) {
 
-							data[ time ][ j ] = outputSource.array[ stride + j ];
+		const defaultMatrix = nodeData.matrix.clone().transpose();
+		const data = {};
 
-						}
+		for ( const member in channelData ) {
+
+			const component = channelData[ member ];
+			const times = component.times;
+			const values = component.values;
+			const stride = component.stride;
+
+			for ( let i = 0, il = times.length; i < il; i ++ ) {
+
+				const time = times[ i ];
+				const valueOffset = i * stride;
+
+				if ( data[ time ] === undefined ) data[ time ] = {};
+
+				if ( component.arraySyntax === true ) {
+
+					const value = values[ valueOffset ];
+					const index = component.indices[ 0 ] + 4 * component.indices[ 1 ];
+					data[ time ][ index ] = value;
+
+				} else {
+
+					for ( let j = 0; j < stride; j ++ ) {
+
+						data[ time ][ j ] = values[ valueOffset + 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;
+		const keyframes = this.prepareAnimationData( data, defaultMatrix );
+		const animation = { name: object3D.uuid, keyframes: keyframes };
+		this.createKeyframeTracks( animation, tracks );
 
-			case 'scale':
-				console.warn( 'THREE.ColladaLoader: Animation transform type "%s" not yet implemented.', transform );
-				break;
+	}
+
+	buildTranslateTrack( object3D, channelData, transformInfo, tracks ) {
+
+		if ( channelData.default && channelData.default.stride === 3 ) {
+
+			const data = channelData.default;
+			const times = Array.from( data.times );
+			const values = Array.from( data.values );
+
+			const track = new VectorKeyframeTrack(
+				object3D.uuid + '.position',
+				times,
+				values
+			);
+
+			const interpolationInfo = this.getInterpolationInfo( channelData );
+			this.applyInterpolation( track, interpolationInfo, channelData );
+			tracks.push( track );
+			return;
 
 		}
 
-		const keyframes = this.prepareAnimationData( data, defaultMatrix );
+		const times = this.getTimesForAllAxes( channelData );
+		if ( times.length === 0 ) return;
 
-		const animation = {
-			name: object3D.uuid,
-			keyframes: keyframes
-		};
+		const values = [];
+		const interpolationInfo = this.getInterpolationInfo( channelData );
+
+		for ( let i = 0; i < times.length; i ++ ) {
+
+			const time = times[ i ];
+
+			const x = this.getValueAtTime( channelData.X, time, transformInfo.x );
+			const y = this.getValueAtTime( channelData.Y, time, transformInfo.y );
+			const z = this.getValueAtTime( channelData.Z, time, transformInfo.z );
+
+			values.push( x, y, z );
 
-		return animation;
+		}
+
+		const track = new VectorKeyframeTrack(
+			object3D.uuid + '.position',
+			times,
+			values
+		);
+
+		this.applyInterpolation( track, interpolationInfo );
+		tracks.push( track );
 
 	}
 
-	prepareAnimationData( data, defaultMatrix ) {
+	buildRotateTrack( object3D, sid, channelData, transformInfo, nodeData, tracks ) {
 
-		const keyframes = [];
+		const angleData = channelData.ANGLE || channelData.default;
+		if ( ! angleData ) return;
 
-		// transfer data into a sortable array
+		const times = Array.from( angleData.times );
+		if ( times.length === 0 ) return;
 
-		for ( const time in data ) {
+		// Collect all rotations to compose them in order
+		const rotations = [];
 
-			keyframes.push( { time: parseFloat( time ), value: data[ time ] } );
+		for ( const transformSid of nodeData.transformOrder ) {
+
+			const transformType = nodeData.transforms[ transformSid ];
+
+			if ( transformType === 'rotate' ) {
+
+				const info = nodeData.transformData[ transformSid ];
+				rotations.push( {
+					sid: transformSid,
+					axis: new Vector3( info.axis[ 0 ], info.axis[ 1 ], info.axis[ 2 ] ),
+					defaultAngle: info.angle
+				} );
+
+			}
 
 		}
 
-		// ensure keyframes are sorted by time
+		const quaternion = new Quaternion();
+		const prevQuaternion = new Quaternion();
+		const tempQuat = new Quaternion();
+		const values = [];
+		const interpolationInfo = this.getInterpolationInfo( channelData );
 
-		keyframes.sort( ascending );
+		for ( let i = 0; i < times.length; i ++ ) {
 
-		// now we clean up all animation data, so we can use them for keyframe tracks
+			const time = times[ i ];
+			quaternion.identity();
 
-		for ( let i = 0; i < 16; i ++ ) {
+			for ( const rotation of rotations ) {
 
-			this.transformAnimationData( keyframes, i, defaultMatrix.elements[ i ] );
+				let angleDegrees;
+
+				if ( rotation.sid === sid ) {
+
+					angleDegrees = this.getValueAtTime( angleData, time, rotation.defaultAngle );
+
+				} else {
+
+					angleDegrees = rotation.defaultAngle;
+
+				}
+
+				const angleRadians = MathUtils.degToRad( angleDegrees );
+				tempQuat.setFromAxisAngle( rotation.axis, angleRadians );
+				quaternion.multiply( tempQuat );
+
+			}
+
+			// Ensure quaternion continuity
+			if ( i > 0 && prevQuaternion.dot( quaternion ) < 0 ) {
+
+				quaternion.x = - quaternion.x;
+				quaternion.y = - quaternion.y;
+				quaternion.z = - quaternion.z;
+				quaternion.w = - quaternion.w;
+
+			}
+
+			prevQuaternion.copy( quaternion );
+
+			values.push( quaternion.x, quaternion.y, quaternion.z, quaternion.w );
 
 		}
 
-		return keyframes;
+		const track = new QuaternionKeyframeTrack(
+			object3D.uuid + '.quaternion',
+			times,
+			values
+		);
 
-		// array sort function
+		this.applyInterpolation( track, interpolationInfo );
+		tracks.push( track );
+
+	}
+
+	buildScaleTrack( object3D, channelData, transformInfo, tracks ) {
+
+		if ( channelData.default && channelData.default.stride === 3 ) {
+
+			const data = channelData.default;
+			const times = Array.from( data.times );
+			const values = Array.from( data.values );
+
+			const track = new VectorKeyframeTrack(
+				object3D.uuid + '.scale',
+				times,
+				values
+			);
+
+			const interpolationInfo = this.getInterpolationInfo( channelData );
+			this.applyInterpolation( track, interpolationInfo, channelData );
+			tracks.push( track );
+			return;
+
+		}
+
+		const times = this.getTimesForAllAxes( channelData );
+		if ( times.length === 0 ) return;
+
+		const values = [];
+		const interpolationInfo = this.getInterpolationInfo( channelData );
+
+		for ( let i = 0; i < times.length; i ++ ) {
+
+			const time = times[ i ];
+
+			const x = this.getValueAtTime( channelData.X, time, transformInfo.x );
+			const y = this.getValueAtTime( channelData.Y, time, transformInfo.y );
+			const z = this.getValueAtTime( channelData.Z, time, transformInfo.z );
+
+			values.push( x, y, z );
+
+		}
+
+		const track = new VectorKeyframeTrack(
+			object3D.uuid + '.scale',
+			times,
+			values
+		);
+
+		this.applyInterpolation( track, interpolationInfo );
+		tracks.push( track );
+
+	}
+
+	getTimesForAllAxes( channelData ) {
+
+		let times = [];
+
+		if ( channelData.X ) times = times.concat( Array.from( channelData.X.times ) );
+		if ( channelData.Y ) times = times.concat( Array.from( channelData.Y.times ) );
+		if ( channelData.Z ) times = times.concat( Array.from( channelData.Z.times ) );
+		if ( channelData.ANGLE ) times = times.concat( Array.from( channelData.ANGLE.times ) );
+		if ( channelData.default ) times = times.concat( Array.from( channelData.default.times ) );
+
+		times = [ ...new Set( times ) ].sort( ( a, b ) => a - b );
+
+		return times;
+
+	}
+
+	getValueAtTime( componentData, time, defaultValue ) {
+
+		if ( ! componentData ) return defaultValue;
+
+		const times = componentData.times;
+		const values = componentData.values;
+		const interpolation = componentData.interpolation;
+
+		for ( let i = 0; i < times.length; i ++ ) {
+
+			if ( times[ i ] === time ) {
+
+				return values[ i ];
+
+			}
+
+			if ( times[ i ] > time ) {
+
+				if ( i === 0 ) {
+
+					return values[ 0 ];
+
+				}
+
+				const i0 = i - 1;
+				const i1 = i;
+				const t0 = times[ i0 ];
+				const t1 = times[ i1 ];
+				const v0 = values[ i0 ];
+				const v1 = values[ i1 ];
+
+				const interp = interpolation ? interpolation[ i0 ] : 'LINEAR';
+
+				if ( interp === 'STEP' ) {
+
+					return v0;
+
+				} else if ( interp === 'BEZIER' && componentData.inTangent && componentData.outTangent ) {
+
+					return this.evaluateBezierComponent( componentData, i0, i1, t0, t1, time );
+
+				} else {
+
+					const t = ( time - t0 ) / ( t1 - t0 );
+					return v0 + t * ( v1 - v0 );
+
+				}
+
+			}
+
+		}
+
+		return values[ values.length - 1 ];
+
+	}
+
+	evaluateBezierComponent( componentData, i0, i1, t0, t1, time ) {
+
+		const values = componentData.values;
+		const inTangent = componentData.inTangent;
+		const outTangent = componentData.outTangent;
+		const tangentStride = componentData.inTangentStride || 1;
+
+		const v0 = values[ i0 ];
+		const v1 = values[ i1 ];
+
+		let c0x, c0y, c1x, c1y;
+
+		if ( tangentStride === 2 ) {
+
+			c0x = outTangent[ i0 * 2 ];
+			c0y = outTangent[ i0 * 2 + 1 ];
+			c1x = inTangent[ i1 * 2 ];
+			c1y = inTangent[ i1 * 2 + 1 ];
+
+		} else {
+
+			c0x = t0 + ( t1 - t0 ) / 3;
+			c0y = outTangent[ i0 ];
+			c1x = t1 - ( t1 - t0 ) / 3;
+			c1y = inTangent[ i1 ];
+
+		}
+
+		// Newton-Raphson to solve Bx(s) = time
+		let s = ( time - t0 ) / ( t1 - t0 );
+
+		for ( let iter = 0; iter < 8; iter ++ ) {
+
+			const s2 = s * s;
+			const s3 = s2 * s;
+			const oneMinusS = 1 - s;
+			const oneMinusS2 = oneMinusS * oneMinusS;
+			const oneMinusS3 = oneMinusS2 * oneMinusS;
 
-		function ascending( a, b ) {
+			const bx = oneMinusS3 * t0 + 3 * oneMinusS2 * s * c0x + 3 * oneMinusS * s2 * c1x + s3 * t1;
+			const dbx = 3 * oneMinusS2 * ( c0x - t0 ) + 6 * oneMinusS * s * ( c1x - c0x ) + 3 * s2 * ( t1 - c1x );
 
-			return a.time - b.time;
+			if ( Math.abs( dbx ) < 1e-10 ) break;
+
+			const error = bx - time;
+			if ( Math.abs( error ) < 1e-10 ) break;
+
+			s = s - error / dbx;
+			s = Math.max( 0, Math.min( 1, s ) );
 
 		}
 
+		const s2 = s * s;
+		const s3 = s2 * s;
+		const oneMinusS = 1 - s;
+		const oneMinusS2 = oneMinusS * oneMinusS;
+		const oneMinusS3 = oneMinusS2 * oneMinusS;
+
+		return oneMinusS3 * v0 + 3 * oneMinusS2 * s * c0y + 3 * oneMinusS * s2 * c1y + s3 * v1;
+
+	}
+
+	getInterpolationInfo( channelData ) {
+
+		const components = [ 'X', 'Y', 'Z', 'ANGLE', 'default' ];
+		let interpolationType = null;
+		let isUniform = true;
+
+		for ( const comp of components ) {
+
+			const data = channelData[ comp ];
+			if ( ! data || ! data.interpolation ) continue;
+
+			const interpArray = data.interpolation;
+
+			for ( let i = 0; i < interpArray.length; i ++ ) {
+
+				const interp = interpArray[ i ];
+
+				if ( interpolationType === null ) {
+
+					interpolationType = interp;
+
+				} else if ( interp !== interpolationType ) {
+
+					isUniform = false;
+
+				}
+
+			}
+
+		}
+
+		return {
+			type: interpolationType || 'LINEAR',
+			uniform: isUniform
+		};
+
+	}
+
+	applyInterpolation( track, interpolationInfo, channelData = null ) {
+
+		if ( interpolationInfo.type === 'STEP' && interpolationInfo.uniform ) {
+
+			track.setInterpolation( InterpolateDiscrete );
+
+		} else if ( interpolationInfo.type === 'BEZIER' && interpolationInfo.uniform && channelData ) {
+
+			const data = channelData.default;
+
+			if ( data && data.inTangent && data.outTangent ) {
+
+				track.setInterpolation( InterpolateBezier );
+				track.settings = {
+					inTangents: new Float32Array( data.inTangent ),
+					outTangents: new Float32Array( data.outTangent )
+				};
+
+			}
+
+		}
+
+	}
+
+	prepareAnimationData( data, defaultMatrix ) {
+
+		const keyframes = [];
+
+		for ( const time in data ) {
+
+			keyframes.push( { time: parseFloat( time ), value: data[ time ] } );
+
+		}
+
+		keyframes.sort( ( a, b ) => a.time - b.time );
+
+		for ( let i = 0; i < 16; i ++ ) {
+
+			this.transformAnimationData( keyframes, i, defaultMatrix.elements[ i ] );
+
+		}
+
+		return keyframes;
+
 	}
 
 	createKeyframeTracks( animation, tracks ) {
@@ -437,7 +920,6 @@ class ColladaComposer {
 
 	}
 
-	// animation clips
 
 	buildAnimationClip( data ) {
 
@@ -469,7 +951,6 @@ class ColladaComposer {
 
 	}
 
-	// controller
 
 	buildController( data ) {
 
@@ -614,7 +1095,6 @@ class ColladaComposer {
 
 	}
 
-	// image
 
 	buildImage( data ) {
 
@@ -640,7 +1120,6 @@ class ColladaComposer {
 
 	}
 
-	// effect
 
 	buildEffect( data ) {
 
@@ -654,7 +1133,6 @@ class ColladaComposer {
 
 	}
 
-	// material
 
 	getTextureLoader( image ) {
 
@@ -922,7 +1400,6 @@ class ColladaComposer {
 
 	}
 
-	// camera
 
 	buildCamera( data ) {
 
@@ -985,7 +1462,6 @@ class ColladaComposer {
 
 	}
 
-	// light
 
 	buildLight( data ) {
 
@@ -1034,7 +1510,6 @@ class ColladaComposer {
 
 	}
 
-	// geometry
 
 	groupPrimitives( primitives ) {
 
@@ -1414,7 +1889,6 @@ class ColladaComposer {
 
 	}
 
-	// kinematics
 
 	buildKinematicsModel( data ) {
 
@@ -1682,7 +2156,6 @@ class ColladaComposer {
 
 	}
 
-	// nodes
 
 	buildSkeleton( skeletons, joints ) {
 
@@ -1958,6 +2431,13 @@ class ColladaComposer {
 		}
 
 		object.name = ( type === 'JOINT' ) ? data.sid : data.name;
+
+		if ( type !== 'JOINT' && this.hasPivotTransforms( data ) ) {
+
+			return this.wrapWithTransformHierarchy( object, data );
+
+		}
+
 		object.matrix.copy( matrix );
 		object.matrix.decompose( object.position, object.quaternion, object.scale );
 
@@ -1965,6 +2445,70 @@ class ColladaComposer {
 
 	}
 
+	wrapWithTransformHierarchy( contentObject, nodeData ) {
+
+		const nodeId = nodeData.id;
+		this.transformNodes[ nodeId ] = {};
+
+		const transformOrder = nodeData.transformOrder;
+		const transformData = nodeData.transformData;
+
+		const rootNode = new Group();
+		rootNode.name = nodeData.name;
+
+		let currentParent = rootNode;
+
+		for ( let i = 0; i < transformOrder.length; i ++ ) {
+
+			const sid = transformOrder[ i ];
+			const info = transformData[ sid ];
+
+			const transformNode = new Group();
+			transformNode.name = nodeData.name + '_' + sid;
+
+			switch ( info.type ) {
+
+				case 'translate':
+					transformNode.position.set( info.x, info.y, info.z );
+					break;
+
+				case 'rotate': {
+
+					const axis = new Vector3( info.axis[ 0 ], info.axis[ 1 ], info.axis[ 2 ] );
+					const angle = MathUtils.degToRad( info.angle );
+					transformNode.quaternion.setFromAxisAngle( axis, angle );
+					transformNode.userData.rotationAxis = axis;
+					break;
+
+				}
+
+				case 'scale':
+					transformNode.scale.set( info.x, info.y, info.z );
+					break;
+
+				case 'matrix': {
+
+					const matrix = new Matrix4().fromArray( info.array ).transpose();
+					matrix.decompose( transformNode.position, transformNode.quaternion, transformNode.scale );
+					break;
+
+				}
+
+			}
+
+			this.transformNodes[ nodeId ][ sid ] = transformNode;
+
+			currentParent.add( transformNode );
+			currentParent = transformNode;
+
+		}
+
+		currentParent.add( contentObject );
+
+		return rootNode;
+
+	}
+
 	resolveMaterialBinding( keys, instanceMaterials ) {
 
 		const materials = [];
@@ -2117,7 +2661,6 @@ class ColladaComposer {
 
 	}
 
-	// visual scenes
 
 	buildVisualScene( data ) {
 
@@ -2150,7 +2693,6 @@ class ColladaComposer {
 
 	}
 
-	// scenes
 
 	parseScene( xml ) {
 
@@ -2189,6 +2731,8 @@ class ColladaComposer {
 
 				}
 
+				this.buildDeferredPivotAnimationTracks( tracks );
+
 				this.animations.push( new AnimationClip( 'default', - 1, tracks ) );
 
 			}
@@ -2205,6 +2749,201 @@ class ColladaComposer {
 
 	}
 
+	buildDeferredPivotAnimationTracks( tracks ) {
+
+		for ( const nodeId in this.deferredPivotAnimations ) {
+
+			const nodeData = this.library.nodes[ nodeId ];
+			if ( ! nodeData ) continue;
+
+			const mergedChannels = this.deferredPivotAnimations[ nodeId ];
+			this.buildTransformHierarchyTracks( nodeId, mergedChannels, nodeData, tracks );
+
+		}
+
+	}
+
+	buildTransformHierarchyTracks( nodeId, nodeChannels, nodeData, tracks ) {
+
+		const transformNodes = this.transformNodes[ nodeId ];
+
+		if ( ! transformNodes ) {
+
+			console.warn( 'THREE.ColladaLoader: Transform hierarchy not found for node:', nodeId );
+			return;
+
+		}
+
+		for ( const sid in nodeChannels ) {
+
+			const transformNode = transformNodes[ sid ];
+			if ( ! transformNode ) continue;
+
+			const transformType = nodeData.transforms[ sid ];
+			const transformInfo = nodeData.transformData[ sid ];
+			const channelData = nodeChannels[ sid ];
+
+			switch ( transformType ) {
+
+				case 'translate':
+					this.buildHierarchyTranslateTrack( transformNode, channelData, transformInfo, tracks );
+					break;
+
+				case 'rotate':
+					this.buildHierarchyRotateTrack( transformNode, channelData, transformInfo, tracks );
+					break;
+
+				case 'scale':
+					this.buildHierarchyScaleTrack( transformNode, channelData, transformInfo, tracks );
+					break;
+
+			}
+
+		}
+
+	}
+
+	buildHierarchyTranslateTrack( transformNode, channelData, transformInfo, tracks ) {
+
+		if ( channelData.default && channelData.default.stride === 3 ) {
+
+			const data = channelData.default;
+			const track = new VectorKeyframeTrack(
+				transformNode.uuid + '.position',
+				Array.from( data.times ),
+				Array.from( data.values )
+			);
+
+			const interpolationInfo = this.getInterpolationInfo( channelData );
+			this.applyInterpolation( track, interpolationInfo, channelData );
+			tracks.push( track );
+			return;
+
+		}
+
+		const times = this.getTimesForAllAxes( channelData );
+		if ( times.length === 0 ) return;
+
+		const values = [];
+		const interpolationInfo = this.getInterpolationInfo( channelData );
+
+		for ( let i = 0; i < times.length; i ++ ) {
+
+			const time = times[ i ];
+			const x = this.getValueAtTime( channelData.X, time, transformInfo.x );
+			const y = this.getValueAtTime( channelData.Y, time, transformInfo.y );
+			const z = this.getValueAtTime( channelData.Z, time, transformInfo.z );
+			values.push( x, y, z );
+
+		}
+
+		const track = new VectorKeyframeTrack(
+			transformNode.uuid + '.position',
+			times,
+			values
+		);
+
+		this.applyInterpolation( track, interpolationInfo );
+		tracks.push( track );
+
+	}
+
+	buildHierarchyRotateTrack( transformNode, channelData, transformInfo, tracks ) {
+
+		const angleData = channelData.ANGLE || channelData.default;
+		if ( ! angleData ) return;
+
+		const times = Array.from( angleData.times );
+		if ( times.length === 0 ) return;
+
+		const axis = transformNode.userData.rotationAxis ||
+			new Vector3( transformInfo.axis[ 0 ], transformInfo.axis[ 1 ], transformInfo.axis[ 2 ] );
+
+		const quaternion = new Quaternion();
+		const prevQuaternion = new Quaternion();
+		const values = [];
+
+		const interpolationInfo = this.getInterpolationInfo( channelData );
+
+		for ( let i = 0; i < times.length; i ++ ) {
+
+			const time = times[ i ];
+			const angleDegrees = this.getValueAtTime( angleData, time, transformInfo.angle );
+			const angleRadians = MathUtils.degToRad( angleDegrees );
+
+			quaternion.setFromAxisAngle( axis, angleRadians );
+
+			// Ensure quaternion continuity
+			if ( i > 0 && prevQuaternion.dot( quaternion ) < 0 ) {
+
+				quaternion.x = - quaternion.x;
+				quaternion.y = - quaternion.y;
+				quaternion.z = - quaternion.z;
+				quaternion.w = - quaternion.w;
+
+			}
+
+			prevQuaternion.copy( quaternion );
+			values.push( quaternion.x, quaternion.y, quaternion.z, quaternion.w );
+
+		}
+
+		const track = new QuaternionKeyframeTrack(
+			transformNode.uuid + '.quaternion',
+			times,
+			values
+		);
+
+		this.applyInterpolation( track, interpolationInfo );
+		tracks.push( track );
+
+	}
+
+	buildHierarchyScaleTrack( transformNode, channelData, transformInfo, tracks ) {
+
+		if ( channelData.default && channelData.default.stride === 3 ) {
+
+			const data = channelData.default;
+			const track = new VectorKeyframeTrack(
+				transformNode.uuid + '.scale',
+				Array.from( data.times ),
+				Array.from( data.values )
+			);
+
+			const interpolationInfo = this.getInterpolationInfo( channelData );
+			this.applyInterpolation( track, interpolationInfo, channelData );
+			tracks.push( track );
+			return;
+
+		}
+
+		const times = this.getTimesForAllAxes( channelData );
+		if ( times.length === 0 ) return;
+
+		const values = [];
+		const interpolationInfo = this.getInterpolationInfo( channelData );
+
+		for ( let i = 0; i < times.length; i ++ ) {
+
+			const time = times[ i ];
+			const x = this.getValueAtTime( channelData.X, time, transformInfo.x );
+			const y = this.getValueAtTime( channelData.Y, time, transformInfo.y );
+			const z = this.getValueAtTime( channelData.Z, time, transformInfo.z );
+			values.push( x, y, z );
+
+		}
+
+		const track = new VectorKeyframeTrack(
+			transformNode.uuid + '.scale',
+			times,
+			values
+		);
+
+		this.applyInterpolation( track, interpolationInfo );
+		tracks.push( track );
+
+	}
+
 }
 
 export { ColladaComposer };

+ 41 - 7
examples/jsm/loaders/collada/ColladaParser.js

@@ -1763,7 +1763,9 @@ class ColladaParser {
 			instanceLights: [],
 			instanceGeometries: [],
 			instanceNodes: [],
-			transforms: {}
+			transforms: {},
+			transformData: {},
+			transformOrder: []
 		};
 
 		for ( let i = 0; i < xml.childNodes.length; i ++ ) {
@@ -1804,27 +1806,59 @@ class ColladaParser {
 				case 'matrix':
 					array = parseFloats( child.textContent );
 					data.matrix.multiply( matrix.fromArray( array ).transpose() );
-					data.transforms[ child.getAttribute( 'sid' ) ] = child.nodeName;
+					{
+
+						const sid = child.getAttribute( 'sid' );
+						data.transforms[ sid ] = child.nodeName;
+						data.transformData[ sid ] = { type: 'matrix', array: array };
+						data.transformOrder.push( sid );
+
+					}
+
 					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;
+					{
+
+						const sid = child.getAttribute( 'sid' );
+						data.transforms[ sid ] = child.nodeName;
+						data.transformData[ sid ] = { type: 'translate', x: array[ 0 ], y: array[ 1 ], z: array[ 2 ] };
+						data.transformOrder.push( sid );
+
+					}
+
 					break;
 
 				case 'rotate':
 					array = parseFloats( child.textContent );
-					const angle = MathUtils.degToRad( array[ 3 ] );
-					data.matrix.multiply( matrix.makeRotationAxis( vector.fromArray( array ), angle ) );
-					data.transforms[ child.getAttribute( 'sid' ) ] = child.nodeName;
+					{
+
+						const angle = MathUtils.degToRad( array[ 3 ] );
+						data.matrix.multiply( matrix.makeRotationAxis( vector.fromArray( array ), angle ) );
+						const sid = child.getAttribute( 'sid' );
+						data.transforms[ sid ] = child.nodeName;
+						data.transformData[ sid ] = { type: 'rotate', axis: [ array[ 0 ], array[ 1 ], array[ 2 ] ], angle: array[ 3 ] };
+						data.transformOrder.push( sid );
+
+					}
+
 					break;
 
 				case 'scale':
 					array = parseFloats( child.textContent );
 					data.matrix.scale( vector.fromArray( array ) );
-					data.transforms[ child.getAttribute( 'sid' ) ] = child.nodeName;
+					{
+
+						const sid = child.getAttribute( 'sid' );
+						data.transforms[ sid ] = child.nodeName;
+						data.transformData[ sid ] = { type: 'scale', x: array[ 0 ], y: array[ 1 ], z: array[ 2 ] };
+						data.transformOrder.push( sid );
+
+					}
+
 					break;
 
 				case 'extra':

粤ICP备19079148号