4 <title>three.js webgl - animation - skinning</title>
6 <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
10 font-family:Monospace;
13 background-color: #fff;
19 top: 0px; width: 100%;
25 .ac { /* prevent dat-gui from being selected */
26 -webkit-user-select: none;
27 -moz-user-select: none;
28 -ms-user-select: none;
36 text-decoration: line-through;
41 <div id="container"></div>
43 <a href="http://threejs.org" target="_blank" rel="noopener">three.js</a> - Skeletal Animation Blending
44 (model from <a href="http://realitymeltdown.com" target="_blank" rel="noopener">realitymeltdown.com</a>)
45 <br><br>camera orbit/zoom/pan with left/middle/right mouse button
46 <br>Note: crossfades are possible with blend weights being set to (1,0,0), (0,1,0) or (0,0,1)
49 <script src="../build/three.js"></script>
51 <script src="js/Detector.js"></script>
52 <script src="js/libs/stats.min.js"></script>
53 <script src="js/controls/OrbitControls.js"></script>
54 <script src="js/libs/dat.gui.min.js"></script>
58 if ( ! Detector.webgl ) Detector.addGetWebGLMessage();
60 var container = document.getElementById( 'container' );
62 var scene, renderer, camera, controls, stats;
63 var mesh, skeleton, mixer;
65 var crossFadeControls = [];
67 var idleAction, walkAction, runAction;
68 var idleWeight, walkWeight, runWeight;
72 var clock = new THREE.Clock();
74 var singleStepMode = false;
75 var sizeOfNextStep = 0;
77 var url = 'models/skinned/marine/marine_anims_core.json';
80 // Initialize stats (fps display)
83 container.appendChild( stats.dom );
86 // Initialize scene, light and renderer
88 scene = new THREE.Scene();
89 scene.background = new THREE.Color( 0x333333 );
90 scene.add( new THREE.AmbientLight( 0xffffff ) );
92 renderer = new THREE.WebGLRenderer( { antialias: true } );
93 renderer.setPixelRatio( window.devicePixelRatio );
94 renderer.setSize( window.innerWidth, window.innerHeight );
96 container.appendChild( renderer.domElement );
101 new THREE.ObjectLoader().load( url, function ( loadedObject ) {
103 loadedObject.traverse( function ( child ) {
105 if ( child instanceof THREE.SkinnedMesh ) {
113 if ( mesh === undefined ) {
115 alert( 'Unable to find a SkinnedMesh in this place:\n\n' + url + '\n\n' );
121 // Add mesh and skeleton helper to scene
123 mesh.rotation.y = - 135 * Math.PI / 180;
126 skeleton = new THREE.SkeletonHelper( mesh );
127 skeleton.visible = false;
128 scene.add( skeleton );
131 // Initialize camera and camera controls
133 var radius = mesh.geometry.boundingSphere.radius;
135 var aspect = window.innerWidth / window.innerHeight;
136 camera = new THREE.PerspectiveCamera( 45, aspect, 1, 10000 );
137 camera.position.set( 0.0, radius, radius * 3.5 );
139 controls = new THREE.OrbitControls( camera, renderer.domElement );
140 controls.target.set( 0, radius, 0 );
144 // Create the control panel
149 // Initialize mixer and clip actions
151 mixer = new THREE.AnimationMixer( mesh );
153 idleAction = mixer.clipAction( 'idle' );
154 walkAction = mixer.clipAction( 'walk' );
155 runAction = mixer.clipAction( 'run' );
156 actions = [ idleAction, walkAction, runAction ];
158 activateAllActions();
161 // Listen on window resizing and start the render loop
163 window.addEventListener( 'resize', onWindowResize, false );
170 function createPanel() {
172 var panel = new dat.GUI( { width: 310 } );
174 var folder1 = panel.addFolder( 'Visibility' );
175 var folder2 = panel.addFolder( 'Activation/Deactivation' );
176 var folder3 = panel.addFolder( 'Pausing/Stepping' );
177 var folder4 = panel.addFolder( 'Crossfading' );
178 var folder5 = panel.addFolder( 'Blend Weights' );
179 var folder6 = panel.addFolder( 'General Speed' );
183 'show skeleton': false,
184 'deactivate all': deactivateAllActions,
185 'activate all': activateAllActions,
186 'pause/continue': pauseContinue,
187 'make single step': toSingleStepMode,
188 'modify step size': 0.05,
189 'from walk to idle': function () { prepareCrossFade( walkAction, idleAction, 1.0 ) },
190 'from idle to walk': function () { prepareCrossFade( idleAction, walkAction, 0.5 ) },
191 'from walk to run': function () { prepareCrossFade( walkAction, runAction, 2.5 ) },
192 'from run to walk': function () { prepareCrossFade( runAction, walkAction, 5.0 ) },
193 'use default duration': true,
194 'set custom duration': 3.5,
195 'modify idle weight': 0.0,
196 'modify walk weight': 1.0,
197 'modify run weight': 0.0,
198 'modify time scale': 1.0
201 folder1.add( settings, 'show model' ).onChange( showModel );
202 folder1.add( settings, 'show skeleton' ).onChange( showSkeleton );
203 folder2.add( settings, 'deactivate all' );
204 folder2.add( settings, 'activate all' );
205 folder3.add( settings, 'pause/continue' );
206 folder3.add( settings, 'make single step' );
207 folder3.add( settings, 'modify step size', 0.01, 0.1, 0.001 );
208 crossFadeControls.push( folder4.add( settings, 'from walk to idle' ) );
209 crossFadeControls.push( folder4.add( settings, 'from idle to walk' ) );
210 crossFadeControls.push( folder4.add( settings, 'from walk to run' ) );
211 crossFadeControls.push( folder4.add( settings, 'from run to walk' ) );
212 folder4.add( settings, 'use default duration' );
213 folder4.add( settings, 'set custom duration', 0, 10, 0.01 );
214 folder5.add( settings, 'modify idle weight', 0.0, 1.0, 0.01 ).listen().onChange( function ( weight ) { setWeight( idleAction, weight ) } );
215 folder5.add( settings, 'modify walk weight', 0.0, 1.0, 0.01 ).listen().onChange( function ( weight ) { setWeight( walkAction, weight ) } );
216 folder5.add( settings, 'modify run weight', 0.0, 1.0, 0.01 ).listen().onChange( function ( weight ) { setWeight( runAction, weight ) } );
217 folder6.add( settings, 'modify time scale', 0.0, 1.5, 0.01 ).onChange( modifyTimeScale );
226 crossFadeControls.forEach( function ( control ) {
228 control.classList1 = control.domElement.parentElement.parentElement.classList;
229 control.classList2 = control.domElement.previousElementSibling.classList;
231 control.setDisabled = function () {
233 control.classList1.add( 'no-pointer-events' );
234 control.classList2.add( 'control-disabled' );
238 control.setEnabled = function () {
240 control.classList1.remove( 'no-pointer-events' );
241 control.classList2.remove( 'control-disabled' );
250 function showModel( visibility ) {
252 mesh.visible = visibility;
257 function showSkeleton( visibility ) {
259 skeleton.visible = visibility;
264 function modifyTimeScale( speed ) {
266 mixer.timeScale = speed;
271 function deactivateAllActions() {
273 actions.forEach( function ( action ) {
282 function activateAllActions() {
284 setWeight( idleAction, settings[ 'modify idle weight' ] );
285 setWeight( walkAction, settings[ 'modify walk weight' ] );
286 setWeight( runAction, settings[ 'modify run weight' ] );
288 actions.forEach( function ( action ) {
297 function pauseContinue() {
299 if ( singleStepMode ) {
301 singleStepMode = false;
306 if ( idleAction.paused ) {
321 function pauseAllActions() {
323 actions.forEach( function ( action ) {
325 action.paused = true;
332 function unPauseAllActions() {
334 actions.forEach( function ( action ) {
336 action.paused = false;
343 function toSingleStepMode() {
347 singleStepMode = true;
348 sizeOfNextStep = settings[ 'modify step size' ];
353 function prepareCrossFade( startAction, endAction, defaultDuration ) {
355 // Switch default / custom crossfade duration (according to the user's choice)
357 var duration = setCrossFadeDuration( defaultDuration );
359 // Make sure that we don't go on in singleStepMode, and that all actions are unpaused
361 singleStepMode = false;
364 // If the current action is 'idle' (duration 4 sec), execute the crossfade immediately;
365 // else wait until the current action has finished its current loop
367 if ( startAction === idleAction ) {
369 executeCrossFade( startAction, endAction, duration );
373 synchronizeCrossFade( startAction, endAction, duration );
380 function setCrossFadeDuration( defaultDuration ) {
382 // Switch default crossfade duration <-> custom crossfade duration
384 if ( settings[ 'use default duration' ] ) {
386 return defaultDuration;
390 return settings[ 'set custom duration' ];
397 function synchronizeCrossFade( startAction, endAction, duration ) {
399 mixer.addEventListener( 'loop', onLoopFinished );
401 function onLoopFinished( event ) {
403 if ( event.action === startAction ) {
405 mixer.removeEventListener( 'loop', onLoopFinished );
407 executeCrossFade( startAction, endAction, duration );
416 function executeCrossFade( startAction, endAction, duration ) {
418 // Not only the start action, but also the end action must get a weight of 1 before fading
419 // (concerning the start action this is already guaranteed in this place)
421 setWeight( endAction, 1 );
424 // Crossfade with warping - you can also try without warping by setting the third parameter to false
426 startAction.crossFadeTo( endAction, duration, true );
431 // This function is needed, since animationAction.crossFadeTo() disables its start action and sets
432 // the start action's timeScale to ((start animation's duration) / (end animation's duration))
434 function setWeight( action, weight ) {
436 action.enabled = true;
437 action.setEffectiveTimeScale( 1 );
438 action.setEffectiveWeight( weight );
443 // Called by the render loop
445 function updateWeightSliders() {
447 settings[ 'modify idle weight' ] = idleWeight;
448 settings[ 'modify walk weight' ] = walkWeight;
449 settings[ 'modify run weight' ] = runWeight;
454 // Called by the render loop
456 function updateCrossFadeControls() {
458 crossFadeControls.forEach( function ( control ) {
460 control.setDisabled();
464 if ( idleWeight === 1 && walkWeight === 0 && runWeight === 0 ) {
466 crossFadeControls[ 1 ].setEnabled();
470 if ( idleWeight === 0 && walkWeight === 1 && runWeight === 0 ) {
472 crossFadeControls[ 0 ].setEnabled();
473 crossFadeControls[ 2 ].setEnabled();
477 if ( idleWeight === 0 && walkWeight === 0 && runWeight === 1 ) {
479 crossFadeControls[ 3 ].setEnabled();
486 function onWindowResize() {
488 camera.aspect = window.innerWidth / window.innerHeight;
489 camera.updateProjectionMatrix();
491 renderer.setSize( window.innerWidth, window.innerHeight );
500 requestAnimationFrame( animate );
502 idleWeight = idleAction.getEffectiveWeight();
503 walkWeight = walkAction.getEffectiveWeight();
504 runWeight = runAction.getEffectiveWeight();
506 // Update the panel values if weights are modified from "outside" (by crossfadings)
508 updateWeightSliders();
510 // Enable/disable crossfade controls according to current weight values
512 updateCrossFadeControls();
514 // Get the time elapsed since the last frame, used for mixer update (if not in single step mode)
516 var mixerUpdateDelta = clock.getDelta();
518 // If in single step mode, make one step and then do nothing (until the user clicks again)
520 if ( singleStepMode ) {
522 mixerUpdateDelta = sizeOfNextStep;
527 // Update the animation mixer, the stats panel, and render this frame
529 mixer.update( mixerUpdateDelta );
533 renderer.render( scene, camera );