Selaa lähdekoodia

Merge pull request #11486 from Mugen87/dev

ColladaLoader2: Basic skeleton animation support
Mr.doob 8 vuotta sitten
vanhempi
commit
9e74883f6c
1 muutettua tiedostoa jossa 447 lisäystä ja 8 poistoa
  1. 447 8
      examples/js/loaders/ColladaLoader2.js

+ 447 - 8
examples/js/loaders/ColladaLoader2.js

@@ -128,6 +128,12 @@ THREE.ColladaLoader.prototype = {
 
 		}
 
+		function isEmpty( object ) {
+
+			return  Object.keys( object ).length === 0;
+
+		}
+
 		// asset
 
 		function parseAsset( xml ) {
@@ -323,6 +329,387 @@ THREE.ColladaLoader.prototype = {
 
 		}
 
+		function buildAnimation( data ) {
+
+			var tracks = [];
+
+			var channels = data.channels;
+			var samplers = data.samplers;
+			var sources = data.sources;
+
+			for ( var target in channels ) {
+
+				if ( channels.hasOwnProperty( target ) ) {
+
+					var channel = channels[ target ];
+					var sampler = samplers[ channel.sampler ];
+
+					var inputId = sampler.inputs.INPUT;
+					var outputId = sampler.inputs.OUTPUT;
+					var interpolationId = sampler.inputs.INTERPOLATION;
+
+					var inputSource = sources[ inputId ];
+					var outputSource = sources[ outputId ];
+					var interpolationSource = sources[ interpolationId ];
+
+					var animation = buildAnimationChannel( channel, inputSource, outputSource, interpolationSource );
+
+					createKeyframeTracks( animation, tracks );
+
+				}
+
+			}
+
+			return tracks;
+
+		}
+
+		function getAnimation( id ) {
+
+			return getBuild( library.animations[ id ], buildAnimation );
+
+		}
+
+		function buildAnimationChannel( channel, inputSource, outputSource, interpolationSource ) {
+
+			var node = library.nodes[ channel.id ];
+			var transform = node.transforms[ channel.sid ];
+			var defaultMatrix = node.matrix.clone().transpose();
+
+			var time, stride;
+			var i, il, j, jl;
+
+			var 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 ) {
+
+							var value = outputSource.array[ stride ];
+							var 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 "%s" not yet implemented.', transform );
+					break;
+
+				case 'scale':
+					console.warn( 'THREE.ColladaLoader: Animation transform "%s" not yet implemented.', transform );
+					break;
+
+			}
+
+			keyframes = prepareAnimationData( data, defaultMatrix );
+
+			var animation = {
+				name: '.bones[' + node.sid + ']',
+				keyframes: keyframes
+			}
+
+			return animation;
+
+		}
+
+		function prepareAnimationData( data, defaultMatrix ) {
+
+			var keyframes = [];
+
+			// transfer data into a sortable array
+
+			for ( var 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 ( var 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;
+
+			}
+
+		}
+
+		var position = new THREE.Vector3();
+		var scale = new THREE.Vector3();
+		var quaternion = new THREE.Quaternion();
+
+		function createKeyframeTracks( animation, tracks ) {
+
+			var keyframes = animation.keyframes;
+			var name = animation.name;
+
+			var times = [];
+			var positionData = [];
+			var quaternionData = [];
+			var scaleData = [];
+
+			for ( var i = 0, l = keyframes.length; i < l; i ++ ) {
+
+				var keyframe = keyframes[ i ];
+
+				var time = keyframe.time;
+				var value = keyframe.value;
+
+				matrix.set(
+					value[ 0 ], value[ 1 ], value[ 2 ], value[ 3 ],
+					value[ 4 ], value[ 5 ], value[ 6 ], value[ 7 ],
+					value[ 8 ], value[ 9 ], value[ 10 ], value[ 11 ],
+					value[ 12 ], value[ 13 ], value[ 14 ], value[ 15 ]
+				);
+
+				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 ) {
+
+			var keyframe;
+
+			var empty = true;
+			var 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 ) {
+
+			var prev, next;
+
+			for ( var i = 0, l = keyframes.length; i < l; i ++ ) {
+
+				var 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 ) {
+
+				var keyframe = keyframes[ i ];
+
+				if ( keyframe.value[ property ] !== null ) return keyframe;
+
+				i --;
+
+			}
+
+			return null;
+
+		}
+
+		function getNext( keyframes, i, property ) {
+
+			while ( i < keyframes.length ) {
+
+				var 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 ) {
+
+			var data = {
+				name: xml.getAttribute( 'id' ) || 'default',
+				start: parseFloat( xml.getAttribute( 'start' ) || 0 ),
+				end: parseFloat( xml.getAttribute( 'end' ) || 0 ),
+				animations: []
+			};
+
+			for ( var i = 0, l = xml.childNodes.length; i < l; i ++ ) {
+
+				var 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 ) {
+
+			var tracks = [];
+
+			var name = data.name;
+			var duration = ( data.end - data.start ) || - 1;
+			var animations = data.animations;
+
+			for ( var i = 0, il = animations.length; i < il; i ++ ) {
+
+				var animationTracks = getAnimation( animations[ i ] );
+
+				for ( var 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 ) {
@@ -1681,6 +2068,7 @@ THREE.ColladaLoader.prototype = {
 			var data = {
 				name: xml.getAttribute( 'name' ),
 				type: xml.getAttribute( 'type' ),
+				id: xml.getAttribute( 'id' ),
 				sid: xml.getAttribute( 'sid' ),
 				matrix: new THREE.Matrix4(),
 				nodes: [],
@@ -1688,7 +2076,8 @@ THREE.ColladaLoader.prototype = {
 				instanceControllers: [],
 				instanceLights: [],
 				instanceGeometries: [],
-				instanceNodes: []
+				instanceNodes: [],
+				transforms: {}
 			};
 
 			for ( var i = 0; i < xml.childNodes.length; i ++ ) {
@@ -1732,24 +2121,28 @@ THREE.ColladaLoader.prototype = {
 
 					case 'matrix':
 						var array = parseFloats( child.textContent );
-						data.matrix.multiply( matrix.fromArray( array ).transpose() ); // .transpose() when Z_UP?
+						data.matrix.multiply( matrix.fromArray( array ).transpose() );
+						data.transforms[ child.getAttribute( 'sid' ) ] = child.nodeName;
 						break;
 
 					case 'translate':
 						var 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':
 						var array = parseFloats( child.textContent );
 						var angle = THREE.Math.degToRad( array[ 3 ] );
 						data.matrix.multiply( matrix.makeRotationAxis( vector.fromArray( array ), angle ) );
+						data.transforms[ child.getAttribute( 'sid' ) ] = child.nodeName;
 						break;
 
 					case 'scale':
 						var array = parseFloats( child.textContent );
 						data.matrix.scale( vector.fromArray( array ) );
+						data.transforms[ child.getAttribute( 'sid' ) ] = child.nodeName;
 						break;
 
 					case 'extra':
@@ -1877,6 +2270,7 @@ THREE.ColladaLoader.prototype = {
 
 						object.bind( skeleton, controller.skin.bindMatrix );
 						object.normalizeSkinWeights();
+						object.bones = skeleton.bones; // this is necessary for property binding
 						object.add( node ); // bone hierarchy is a child of the skinned mesh
 
 						object.material = getMaterial( instance.materials[ key ] );
@@ -2008,6 +2402,46 @@ THREE.ColladaLoader.prototype = {
 
 		}
 
+		function setupAnimations() {
+
+			var 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
+
+					var tracks = [];
+
+					for ( var id in library.animations ) {
+
+						var animationTracks = getAnimation( id );
+
+						for ( var i = 0, l = animationTracks.length; i < l; i ++ ) {
+
+							tracks.push( animationTracks[ i ] );
+
+						}
+
+					}
+
+					animations.push( new THREE.AnimationClip( 'default', - 1, tracks ) );
+
+				}
+
+			} else {
+
+				for ( var id in clips ) {
+
+					animations.push( getAnimationClip( id ) );
+
+				}
+
+			}
+
+		}
+
 		console.time( 'THREE.ColladaLoader' );
 
 		if ( text.length === 0 ) {
@@ -2034,8 +2468,13 @@ THREE.ColladaLoader.prototype = {
 
 		//
 
+		var animations = [];
+
+		//
+
 		var library = {
 			animations: {},
+			clips: {},
 			controllers: {},
 			images: {},
 			effects: {},
@@ -2049,7 +2488,8 @@ THREE.ColladaLoader.prototype = {
 
 		console.time( 'THREE.ColladaLoader: Parse' );
 
-		// parseLibrary( collada, 'library_animations', 'animation', parseAnimation );
+		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 );
@@ -2064,6 +2504,8 @@ THREE.ColladaLoader.prototype = {
 
 		console.time( 'THREE.ColladaLoader: Build' );
 
+		buildLibrary( library.animations, buildAnimation );
+		buildLibrary( library.clips, buildAnimationClip );
 		buildLibrary( library.controllers, buildController );
 		buildLibrary( library.images, buildImage );
 		buildLibrary( library.effects, buildEffect );
@@ -2075,7 +2517,7 @@ THREE.ColladaLoader.prototype = {
 
 		console.timeEnd( 'THREE.ColladaLoader: Build' );
 
-		// console.log( library );
+		setupAnimations();
 
 		var scene = parseScene( getElementsByTagName( collada, 'scene' )[ 0 ] );
 
@@ -2089,11 +2531,8 @@ THREE.ColladaLoader.prototype = {
 
 		console.timeEnd( 'THREE.ColladaLoader' );
 
-		// console.log( scene );
-
 		return {
-			animations: [],
-			kinematics: { joints: [] },
+			animations: animations,
 			library: library,
 			scene: scene
 		};