瀏覽代碼

Added an example to showcase skeletal animation blending.

Michael Guerrero 11 年之前
父節點
當前提交
3522d8fc73

+ 285 - 0
examples/js/BlendCharacter.js

@@ -0,0 +1,285 @@
+/**
+ * @author Michael Guerrero / http://realitymeltdown.com
+ */
+
+//==============================================================================
+THREE.BlendCharacter = function () {
+
+	var self = this;
+
+	this.animations = {};
+	this.boneHelpers = [];
+	this.weightSchedule = [];
+	this.warpSchedule = [];
+
+	// ---------------------------------------------------------------------------
+	this.load = function(url, loadedCB) {
+
+		var loader = new THREE.JSONLoader();
+		loader.load( url, function( geometry, materials ) {
+
+			var originalMaterial = materials[ 0 ];
+
+			originalMaterial.skinning = true;
+			originalMaterial.transparent = true;
+			originalMaterial.alphaTest = 0.75;
+
+			THREE.SkinnedMesh.call( self, geometry, originalMaterial );
+
+			for ( var i = 0; i < geometry.animations.length; ++i ) {
+
+				THREE.AnimationHandler.add( geometry.animations[ i ] );
+
+				// Create the animation object and set a default weight
+				var animName = geometry.animations[ i ].name;
+				self.animations[ animName ] = new THREE.Animation( self, animName );
+
+			}
+
+			for ( var i = 0; i < self.skeleton.bones.length; ++i ) {
+
+				var helper = new THREE.BoneAxisHelper( self.skeleton.bones[i], 2, 1 );
+				helper.update();
+				self.add( helper );
+				self.boneHelpers.push( helper );
+
+			}
+
+			self.toggleShowBones( false );
+
+			// Loading is complete, fire the callback
+			loadedCB && loadedCB();
+
+		} );
+
+	};
+
+	// ---------------------------------------------------------------------------
+	this.updateWeights = function( dt ) {
+
+		for ( var i = this.weightSchedule.length - 1; i >= 0; --i ) {
+
+			var data = this.weightSchedule[ i ];
+			data.timeElapsed += dt;
+
+			if ( data.timeElapsed > data.duration ) {
+
+				data.anim.weight = data.endWeight;
+				this.weightSchedule.splice( i, 1 );
+
+				if ( data.anim.weight == 0 ) {
+					data.anim.stop( 0 );
+				}
+
+			} else {
+
+				data.anim.weight = data.startWeight + (data.endWeight - data.startWeight) * data.timeElapsed / data.duration;
+
+			}
+
+		}
+
+		this.updateWarps( dt );
+
+	};
+
+	// ---------------------------------------------------------------------------
+	this.updateWarps = function( dt ) {
+
+		for ( var i = this.warpSchedule.length - 1; i >= 0; --i ) {
+
+			var data = this.warpSchedule[ i ];
+			data.timeElapsed += dt;
+
+			if ( data.timeElapsed > data.duration ) {
+
+				data.to.weight = 1;
+				data.to.timeScale = 1;
+				data.from.weight = 0;
+				data.from.timeScale = 1;
+				data.from.stop( 0 );
+
+				this.warpSchedule.splice( i, 1 );
+
+			} else {
+
+				var alpha = data.timeElapsed / data.duration;
+
+				var fromLength = data.from.data.length;
+				var toLength = data.to.data.length;
+
+				var fromToRatio = fromLength / toLength;
+				var toFromRatio = toLength / fromLength;
+
+				// scale from each time proportionally to the other animation
+				data.from.timeScale = ( 1 - alpha ) + fromToRatio * alpha;
+				data.to.timeScale = alpha + toFromRatio * ( 1 - alpha );
+
+				data.from.weight = 1 - alpha;
+				data.to.weight = alpha;
+
+			}
+
+		}
+
+	}
+
+	// ---------------------------------------------------------------------------
+	this.updateBoneHelpers = function() {
+
+		for ( var i = 0; i < this.boneHelpers.length; ++i ) {
+			this.boneHelpers[ i ].update();
+		}
+	};
+
+	// ---------------------------------------------------------------------------
+	this.play = function(animName, weight) {
+		this.animations[ animName ].play( 0, weight );
+	};
+
+	// ---------------------------------------------------------------------------
+	this.crossfade = function( fromAnimName, toAnimName, duration ) {
+
+		var fromAnim = this.animations[ fromAnimName ];
+		var toAnim = this.animations[ toAnimName ];
+
+		fromAnim.play( 0, 1 );
+		toAnim.play( 0, 0 );
+
+		this.weightSchedule.push( {
+
+			anim: fromAnim,
+			startWeight: 1,
+			endWeight: 0,
+			timeElapsed: 0,
+			duration: duration
+
+		} );
+
+		this.weightSchedule.push( {
+
+			anim: toAnim,
+			startWeight: 0,
+			endWeight: 1,
+			timeElapsed: 0,
+			duration: duration
+
+		} );
+
+	};
+
+	// ---------------------------------------------------------------------------
+	this.warp = function( fromAnimName, toAnimName, duration ) {
+
+		var fromAnim = this.animations[ fromAnimName ];
+		var toAnim = this.animations[ toAnimName ];
+
+		fromAnim.play( 0, 1 );
+		toAnim.play( 0, 0 );
+
+		this.warpSchedule.push( {
+
+			from: fromAnim,
+			to: toAnim,
+			timeElapsed: 0,
+			duration: duration
+
+		} );
+
+	};
+
+	// ---------------------------------------------------------------------------
+	this.applyWeight = function(animName, weight) {
+
+		this.animations[animName].weight = weight;
+
+	};
+
+	// ---------------------------------------------------------------------------
+	this.pauseAll = function() {
+
+		for ( var a in this.animations ) {
+
+			if ( this.animations[ a ].isPlaying ) {
+
+				this.animations[ a ].pause();
+
+			}
+
+		}
+
+	};
+
+	// ---------------------------------------------------------------------------
+	this.unPauseAll = function() {
+
+		for ( var a in this.animations ) {
+
+			if ( this.animations[ a ].isPaused ) {
+
+				this.animations[ a ].pause();
+
+			}
+
+		}
+
+	};
+
+
+	// ---------------------------------------------------------------------------
+	this.stopAll = function() {
+
+		for (a in this.animations) {
+
+			if ( this.animations[ a ].isPlaying ) {
+				this.animations[ a ].stop(0);
+			}
+
+			this.animations[ a ].weight = 0;
+
+		}
+
+		this.weightSchedule.length = 0;
+		this.warpSchedule.length = 0;
+
+	}
+
+	// ---------------------------------------------------------------------------
+	this.toggleShowBones = function( shouldShow ) {
+
+		this.visible = !shouldShow;
+
+		for ( var i = 0; i < self.boneHelpers.length; ++i ) {
+
+			self.boneHelpers[ i ].traverse( function( obj ) {
+
+				obj.visible = shouldShow;
+
+			} );
+
+		}
+
+	}
+
+};
+
+//==============================================================================
+THREE.BlendCharacter.prototype = Object.create( THREE.SkinnedMesh.prototype );
+
+THREE.BlendCharacter.prototype.getForward = function() {
+
+	var forward = new THREE.Vector3();
+
+	return function() {
+
+		// pull the character's forward basis vector out of the matrix
+		forward.set(
+			-this.matrix.elements[ 8 ],
+			-this.matrix.elements[ 9 ],
+			-this.matrix.elements[ 10 ]
+		);
+
+		return forward;
+	}
+}
+

+ 194 - 0
examples/js/BlendCharacterGui.js

@@ -0,0 +1,194 @@
+/**
+ * @author Michael Guerrero / http://realitymeltdown.com
+ */
+
+function BlendCharacterGui(animations) {
+
+	var controls = {
+
+		gui: null,
+		"Lock Camera": false,
+		"Show Bones": false,
+		"Time Scale": 1.0,
+		"Step Size": 0.016,
+		"Crossfade Time": 3.5,
+		"idle": 0.33,
+		"walk": 0.33,
+		"run": 0.33
+
+	};
+
+	var animations = animations;
+
+	this.shouldShowBones = function() {
+
+		return controls['Show Bones'];
+
+	};
+
+	this.getTimeScale = function() {
+
+		return controls['Time Scale'];
+
+	};
+
+	this.update = function() {
+
+		controls[ 'idle'] = animations[ 'idle' ].weight;
+		controls[ 'walk'] = animations[ 'walk' ].weight;
+		controls[ 'run'] = animations[ 'run' ].weight;
+
+	};
+
+	var init = function() {
+
+		controls.gui = new dat.GUI();
+
+		var settings = controls.gui.addFolder( 'Settings' );
+		var playback = controls.gui.addFolder( 'Playback' );
+		var blending = controls.gui.addFolder( 'Blend Tuning' );
+
+		settings.add( controls, "Lock Camera" ).onChange( controls.lockCameraChanged );
+		settings.add( controls, "Show Bones" ).onChange( controls.showBonesChanged );
+		settings.add( controls, "Time Scale", 0, 1, 0.01 );
+		settings.add( controls, "Step Size", 0.01, 0.1, 0.01 );
+		settings.add( controls, "Crossfade Time", 0.1, 6.0, 0.05 );
+
+		// These controls execute functions
+		playback.add( controls, "start" );
+		playback.add( controls, "pause" );
+		playback.add( controls, "step" );
+		playback.add( controls, "idle to walk" );
+		playback.add( controls, "walk to run" );
+		playback.add( controls, "warp walk to run" );
+
+		blending.add( controls, "idle", 0, 1, 0.01).listen().onChange( controls.weight );
+		blending.add( controls, "walk", 0, 1, 0.01).listen().onChange( controls.weight );
+		blending.add( controls, "run", 0, 1, 0.01).listen().onChange( controls.weight );
+
+		settings.open();
+		playback.open();
+		blending.open();
+
+	}
+
+	var getAnimationData = function() {
+
+		return {
+
+			detail: {
+
+				anims: [ "idle", "walk", "run" ],
+
+				weights: [ controls['idle'],
+						   controls['walk'],
+						   controls['run'] ]
+			}
+
+		};
+	}
+
+	controls.start = function() {
+
+		var startEvent = new CustomEvent( 'start-animation', getAnimationData() );
+		window.dispatchEvent(startEvent);
+
+	};
+
+	controls.stop = function() {
+
+		var stopEvent = new CustomEvent( 'stop-animation' );
+		window.dispatchEvent( stopEvent );
+
+	};
+
+	controls.pause = function() {
+
+		var pauseEvent = new CustomEvent( 'pause-animation' );
+		window.dispatchEvent( pauseEvent );
+
+	};
+
+	controls.step = function() {
+
+		var stepData = { detail: { stepSize: controls['Step Size'] } };
+		window.dispatchEvent( new CustomEvent('step-animation', stepData ));
+
+	};
+
+	controls.weight = function() {
+
+		// renormalize
+		var sum = controls['idle'] + controls['walk'] + controls['run'];
+		controls['idle'] /= sum;
+		controls['walk'] /= sum;
+		controls['run'] /= sum;
+
+		var weightEvent = new CustomEvent( 'weight-animation', getAnimationData() );
+		window.dispatchEvent(weightEvent);
+	};
+
+	controls.crossfade = function( from, to ) {
+
+		var fadeData = getAnimationData();
+		fadeData.detail.from = from;
+		fadeData.detail.to = to;
+		fadeData.detail.time = controls[ "Crossfade Time" ];
+
+		window.dispatchEvent( new CustomEvent( 'crossfade', fadeData ) );
+	}
+
+	controls.warp = function( from, to ) {
+
+		var warpData = getAnimationData();
+		warpData.detail.from = 'walk';
+		warpData.detail.to = 'run';
+		warpData.detail.time = controls[ "Crossfade Time" ];
+
+		window.dispatchEvent( new CustomEvent( 'warp', warpData ) );
+	}
+
+	controls['idle to walk'] = function() {
+
+		controls.crossfade( 'idle', 'walk' );
+
+	};
+
+	controls['walk to run'] = function() {
+
+		controls.crossfade( 'walk', 'run' );
+
+	};
+
+	controls['warp walk to run'] = function() {
+
+		controls.warp( 'walk', 'run' );
+
+	};
+
+	controls.lockCameraChanged = function() {
+
+		var data = {
+			detail: {
+				shouldLock: controls['Lock Camera']
+			}
+		}
+
+		window.dispatchEvent( new CustomEvent( 'toggle-lock-camera', data ) );
+	}
+
+	controls.showBonesChanged = function() {
+
+		var data = {
+			detail: {
+				shouldShow: controls['Show Bones']
+			}
+		}
+
+		window.dispatchEvent( new CustomEvent( 'toggle-show-bones', data ) );
+	}
+
+
+	init.call(this);
+
+}

二進制
examples/models/skinned/marine/M4.png


二進制
examples/models/skinned/marine/MarineCv2_color.jpg


二進制
examples/models/skinned/marine/m4.blend


文件差異過大導致無法顯示
+ 37 - 0
examples/models/skinned/marine/m4.js


文件差異過大導致無法顯示
+ 37 - 0
examples/models/skinned/marine/marine.js


二進制
examples/models/skinned/marine/marine_anims.blend


文件差異過大導致無法顯示
+ 37 - 0
examples/models/skinned/marine/marine_anims.js


二進制
examples/models/skinned/marine/marine_fbx.blend


二進制
examples/models/skinned/marine/marine_fk.blend


二進制
examples/models/skinned/marine/marine_ikrig.blend


文件差異過大導致無法顯示
+ 37 - 0
examples/models/skinned/marine/marine_ikrig.js


+ 233 - 0
examples/webgl_animation_skinning_blending.html

@@ -0,0 +1,233 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <title>three.js webgl - animation - skinning</title>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+    <style>
+      body {
+        color: #000;
+        font-family:Monospace;
+        font-size:13px;
+        text-align:center;
+
+        background-color: #fff;
+        margin: 0px;
+        overflow: hidden;
+      }
+
+      #info {
+        position: absolute;
+        top: 0px; width: 100%;
+        padding: 5px;
+      }
+    </style>
+  </head>
+  <body>
+    <div id="container"></div>
+
+    <script src="../build/Three.js"></script>
+    <script src="js/Detector.js"></script>
+    <script src="js/Controls/OrbitControls.js"></script>
+    <script src="js/BlendCharacter.js"></script>
+    <script src="js/BlendCharacterGui.js"></script>
+    <script src="js/libs/dat.gui.min.js"></script>
+
+    <script>
+
+      if ( ! Detector.webgl ) Detector.addGetWebGLMessage();
+
+      var container;
+
+      var camera, scene, renderer, controls;
+
+      var light;
+      var blendMesh = null;
+
+      var clock = new THREE.Clock();
+      var gui = null;
+
+      var isFrameStepping = false;
+      var timeToStep = 0;
+
+      init();
+
+      function init() {
+
+        container = document.getElementById( 'container' );
+
+        scene = new THREE.Scene();
+        scene.add ( new THREE.AmbientLight( 0xaaaaaa ) );
+
+        light = new THREE.DirectionalLight( 0xffffff, 1.5 );
+        light.position = new THREE.Vector3( 0, 0, 1000.0 );
+        scene.add( light );
+
+        renderer = new THREE.WebGLRenderer( { antialias: true, alpha: false } );
+        renderer.setClearColor( '#777777', 1 );
+        renderer.setSize( window.innerWidth, window.innerHeight );
+        renderer.autoClear = false;
+
+        container.appendChild( renderer.domElement );
+
+        window.addEventListener( 'resize', onWindowResize, false );
+        window.addEventListener( 'start-animation', onStartAnimation );
+        window.addEventListener( 'stop-animation', onStopAnimation );
+        window.addEventListener( 'pause-animation', onPauseAnimation );
+        window.addEventListener( 'step-animation', onStepAnimation );
+        window.addEventListener( 'weight-animation', onWeightAnimation );
+        window.addEventListener( 'crossfade', onCrossfade );
+        window.addEventListener( 'warp', onWarp );
+        window.addEventListener( 'toggle-lock-camera', onLockCameraToggle );
+        window.addEventListener( 'toggle-show-bones', onShowBonesToggle );
+
+        blendMesh = new THREE.BlendCharacter();
+        blendMesh.load( "models/skinned/marine/marine_anims.js", start );
+
+      }
+
+      function onWindowResize() {
+
+        camera.aspect = window.innerWidth / window.innerHeight;
+        camera.updateProjectionMatrix();
+
+        renderer.setSize( window.innerWidth, window.innerHeight );
+
+      }
+
+      function onStartAnimation( event ) {
+
+        var data = event.detail;
+
+        blendMesh.stopAll();
+
+        // the blend mesh will combine 1 or more animations
+        for ( var i = 0; i < data.anims.length; ++i ) {
+          blendMesh.play(data.anims[i], data.weights[i]);
+        }
+
+        isFrameStepping = false;
+
+      }
+
+      function onStopAnimation( event ) {
+
+        blendMesh.stopAll();
+        isFrameStepping = false;
+
+      }
+
+      function onPauseAnimation( event ) {
+
+        ( isFrameStepping ) ? blendMesh.unPauseAll(): blendMesh.pauseAll();
+
+        isFrameStepping = false;
+
+      }
+
+      function onStepAnimation( event ) {
+
+        blendMesh.unPauseAll();
+        isFrameStepping = true;
+        timeToStep = event.detail.stepSize;
+      }
+
+      function onWeightAnimation(event) {
+
+        var data = event.detail;
+        for ( var i = 0; i < data.anims.length; ++i ) {
+
+          blendMesh.applyWeight(data.anims[i], data.weights[i]);
+
+        }
+
+      }
+
+      function onCrossfade(event) {
+
+        var data = event.detail;
+
+        blendMesh.stopAll();
+        blendMesh.crossfade( data.from, data.to, data.time );
+
+        isFrameStepping = false;
+
+      }
+
+      function onWarp( event ) {
+
+        var data = event.detail;
+
+        blendMesh.stopAll();
+        blendMesh.warp( data.from, data.to, data.time );
+
+        isFrameStepping = false;
+
+      }
+
+
+      function onLockCameraToggle( event ) {
+
+        var shouldLock = event.detail.shouldLock;
+        controls.enabled = !shouldLock;
+
+      }
+
+      function onShowBonesToggle( event ) {
+
+        var shouldShow = event.detail.shouldShow;
+        blendMesh.toggleShowBones( shouldShow );
+
+      }
+
+      function start() {
+
+        blendMesh.rotation.y = Math.PI * -135 / 180;
+        scene.add( blendMesh );
+
+        var aspect = window.innerWidth / window.innerHeight;
+        var radius = blendMesh.geometry.boundingSphere.radius;
+
+        camera = new THREE.PerspectiveCamera( 45, aspect, 1, 10000 );
+        camera.position.set( 0.0, radius, radius * 3.5 );
+
+        controls = new THREE.OrbitControls( camera );
+        controls.target = new THREE.Vector3( 0, radius, 0 );
+        controls.update();
+
+        // Set default weights
+        blendMesh.animations[ 'idle' ].weight = 1 / 3;
+        blendMesh.animations[ 'walk' ].weight = 1 / 3;
+        blendMesh.animations[ 'run' ].weight = 1 / 3;
+
+        gui = new BlendCharacterGui(blendMesh.animations);
+
+        animate();
+      }
+
+      function animate() {
+
+        requestAnimationFrame( animate, renderer.domElement );
+
+        var scale = gui.getTimeScale();
+        var delta = clock.getDelta();
+        var stepSize = (!isFrameStepping) ? delta * scale: timeToStep;
+
+        blendMesh.updateWeights( stepSize );
+        gui.update();
+
+        THREE.AnimationHandler.update( stepSize );
+        blendMesh.updateBoneHelpers();
+
+        timeToStep = 0;
+
+        renderer.clear();
+        renderer.render( scene, camera );
+
+      }
+
+    </script>
+
+  </body>
+</html>
+

部分文件因文件數量過多而無法顯示