|
@@ -0,0 +1,420 @@
|
|
|
+<!DOCTYPE html>
|
|
|
+<html lang="en">
|
|
|
+ <head>
|
|
|
+ <title>three.js webgl - additive 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">
|
|
|
+ <link type="text/css" rel="stylesheet" href="main.css">
|
|
|
+ <style>
|
|
|
+ a {
|
|
|
+ color: blue;
|
|
|
+ }
|
|
|
+ .ac { /* prevent dat-gui from being selected */
|
|
|
+ -webkit-user-select: none;
|
|
|
+ -moz-user-select: none;
|
|
|
+ -ms-user-select: none;
|
|
|
+ user-select: none;
|
|
|
+ }
|
|
|
+ .control-inactive {
|
|
|
+ color: #888;
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+ </head>
|
|
|
+ <body>
|
|
|
+ <div id="container"></div>
|
|
|
+ <div id="info">
|
|
|
+ <a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - Skeletal Additive Animation Blending
|
|
|
+ (model from <a href="https://www.mixamo.com/" target="_blank" rel="noopener">mixamo.com</a>)<br/>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <script type="module">
|
|
|
+
|
|
|
+ import * as THREE from '../build/three.module.js';
|
|
|
+
|
|
|
+ import Stats from './jsm/libs/stats.module.js';
|
|
|
+ import { GUI } from './jsm/libs/dat.gui.module.js';
|
|
|
+ import { OrbitControls } from './jsm/controls/OrbitControls.js';
|
|
|
+ import { GLTFLoader } from './jsm/loaders/GLTFLoader.js';
|
|
|
+
|
|
|
+ var scene, renderer, camera, stats;
|
|
|
+ var model, skeleton, mixer, clock;
|
|
|
+
|
|
|
+ var crossFadeControls = [];
|
|
|
+
|
|
|
+ var currentBaseAction = 'idle';
|
|
|
+ const allActions = [];
|
|
|
+ const baseActions = {
|
|
|
+ idle: { weight: 1 },
|
|
|
+ walk: { weight: 0 },
|
|
|
+ run: { weight: 0 }
|
|
|
+ };
|
|
|
+ const additiveActions = {
|
|
|
+ sneak_pose: { weight: 0 },
|
|
|
+ sad_pose: { weight: 0 },
|
|
|
+ agree: { weight: 0 },
|
|
|
+ headShake: { weight: 0 }
|
|
|
+ }
|
|
|
+ var panelSettings, numAnimations;
|
|
|
+
|
|
|
+ init();
|
|
|
+
|
|
|
+ function init() {
|
|
|
+
|
|
|
+ var container = document.getElementById( 'container' );
|
|
|
+ clock = new THREE.Clock();
|
|
|
+
|
|
|
+ renderer = new THREE.WebGLRenderer( { antialias: true } );
|
|
|
+ renderer.setPixelRatio( window.devicePixelRatio );
|
|
|
+ renderer.setSize( window.innerWidth, window.innerHeight );
|
|
|
+ renderer.outputEncoding = THREE.sRGBEncoding;
|
|
|
+ renderer.shadowMap.enabled = true;
|
|
|
+ container.appendChild( renderer.domElement );
|
|
|
+
|
|
|
+ // camera
|
|
|
+ camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 1000 );
|
|
|
+ var controls = new OrbitControls( camera, renderer.domElement );
|
|
|
+ camera.position.set( -1, 2, 3 );
|
|
|
+ camera.lookAt( 0, 1, 0 );
|
|
|
+ controls.target = new THREE.Vector3( 0, 1, 0 );
|
|
|
+ controls.update();
|
|
|
+
|
|
|
+ stats = new Stats();
|
|
|
+ container.appendChild( stats.dom );
|
|
|
+
|
|
|
+ window.addEventListener( 'resize', onWindowResize, false );
|
|
|
+
|
|
|
+ scene = new THREE.Scene();
|
|
|
+ scene.background = new THREE.Color( 0xa0a0a0 );
|
|
|
+ scene.fog = new THREE.Fog( 0xa0a0a0, 10, 50 );
|
|
|
+
|
|
|
+ var hemiLight = new THREE.HemisphereLight( 0xffffff, 0x444444 );
|
|
|
+ hemiLight.position.set( 0, 20, 0 );
|
|
|
+ scene.add( hemiLight );
|
|
|
+
|
|
|
+ var dirLight = new THREE.DirectionalLight( 0xffffff );
|
|
|
+ dirLight.position.set( - 3, 10, - 10 );
|
|
|
+ dirLight.castShadow = true;
|
|
|
+ dirLight.shadow.camera.top = 2;
|
|
|
+ dirLight.shadow.camera.bottom = - 2;
|
|
|
+ dirLight.shadow.camera.left = - 2;
|
|
|
+ dirLight.shadow.camera.right = 2;
|
|
|
+ dirLight.shadow.camera.near = 0.1;
|
|
|
+ dirLight.shadow.camera.far = 40;
|
|
|
+ scene.add( dirLight );
|
|
|
+
|
|
|
+ // ground
|
|
|
+
|
|
|
+ var mesh = new THREE.Mesh( new THREE.PlaneBufferGeometry( 100, 100 ), new THREE.MeshPhongMaterial( { color: 0x999999, depthWrite: false } ) );
|
|
|
+ mesh.rotation.x = - Math.PI / 2;
|
|
|
+ mesh.receiveShadow = true;
|
|
|
+ scene.add( mesh );
|
|
|
+
|
|
|
+ var loader = new GLTFLoader();
|
|
|
+ loader.load( 'models/gltf/Xbot.glb', function ( gltf ) {
|
|
|
+
|
|
|
+ model = gltf.scene;
|
|
|
+ scene.add( model );
|
|
|
+
|
|
|
+ model.traverse( function ( object ) {
|
|
|
+
|
|
|
+ if ( object.isMesh ) object.castShadow = true;
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ skeleton = new THREE.SkeletonHelper( model );
|
|
|
+ skeleton.visible = false;
|
|
|
+ scene.add( skeleton );
|
|
|
+
|
|
|
+ var animations = gltf.animations;
|
|
|
+ mixer = new THREE.AnimationMixer( model );
|
|
|
+
|
|
|
+ numAnimations = animations.length;
|
|
|
+
|
|
|
+ for ( let i = 0; i !== numAnimations; ++ i ) {
|
|
|
+
|
|
|
+ let clip = animations[ i ];
|
|
|
+ const name = clip.name;
|
|
|
+
|
|
|
+ if ( baseActions[ name ] ) {
|
|
|
+
|
|
|
+ const action = mixer.clipAction( clip, undefined, false );
|
|
|
+ activateAction( action );
|
|
|
+ baseActions[ name ].action = action;
|
|
|
+ allActions.push( action );
|
|
|
+
|
|
|
+ } else if ( additiveActions[ name ] ) {
|
|
|
+
|
|
|
+ // Make the clip additive and remove the reference frame
|
|
|
+
|
|
|
+ THREE.AnimationUtils.makeClipAdditive( clip );
|
|
|
+
|
|
|
+ if ( clip.name.endsWith( '_pose' ) ) {
|
|
|
+
|
|
|
+ clip = THREE.AnimationUtils.subclip( clip, clip.name, 2, 3, 30 );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ const action = mixer.clipAction( clip, undefined, true );
|
|
|
+ activateAction( action );
|
|
|
+ additiveActions[ name ].action = action;
|
|
|
+ allActions.push( action );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ createPanel();
|
|
|
+
|
|
|
+ animate();
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ function createPanel() {
|
|
|
+
|
|
|
+ var panel = new GUI( { width: 310 } );
|
|
|
+
|
|
|
+ var folder1 = panel.addFolder( 'Base Actions' );
|
|
|
+ var folder2 = panel.addFolder( 'Additive Action Weights' );
|
|
|
+ var folder3 = panel.addFolder( 'General Speed' );
|
|
|
+
|
|
|
+ panelSettings = {
|
|
|
+ 'modify time scale': 1.0
|
|
|
+ };
|
|
|
+
|
|
|
+ const baseNames = [ 'None', ...Object.keys( baseActions ) ];
|
|
|
+
|
|
|
+ for ( let i = 0, l = baseNames.length; i !== l; ++ i ) {
|
|
|
+
|
|
|
+ const name = baseNames[ i ];
|
|
|
+ const settings = baseActions[ name ];
|
|
|
+ panelSettings[ name ] = function () {
|
|
|
+
|
|
|
+ const currentSettings = baseActions[ currentBaseAction ];
|
|
|
+ const currentAction = currentSettings ? currentSettings.action : null;
|
|
|
+ const action = settings ? settings.action : null;
|
|
|
+
|
|
|
+ prepareCrossFade( currentAction, action, 0.35 );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ crossFadeControls.push( folder1.add( panelSettings, name ) );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ for ( const name of Object.keys( additiveActions ) ) {
|
|
|
+
|
|
|
+ const settings = additiveActions[ name ];
|
|
|
+
|
|
|
+ const panelName = `modify ${name} weight`;
|
|
|
+ panelSettings[ name ] = settings.weight;
|
|
|
+ folder2.add( panelSettings, name, 0.0, 1.0, 0.01 ).listen().onChange( function ( weight ) {
|
|
|
+
|
|
|
+ setWeight( settings.action, weight );
|
|
|
+ settings.weight = weight;
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ folder3.add( panelSettings, 'modify time scale', 0.0, 1.5, 0.01 ).onChange( modifyTimeScale );
|
|
|
+
|
|
|
+ folder1.open();
|
|
|
+ folder2.open();
|
|
|
+ folder3.open();
|
|
|
+
|
|
|
+ crossFadeControls.forEach( function ( control ) {
|
|
|
+
|
|
|
+ control.classList1 = control.domElement.parentElement.parentElement.classList;
|
|
|
+ control.classList2 = control.domElement.previousElementSibling.classList;
|
|
|
+
|
|
|
+ control.setInactive = function () {
|
|
|
+
|
|
|
+ control.classList2.add( 'control-inactive' );
|
|
|
+
|
|
|
+ };
|
|
|
+
|
|
|
+ control.setActive = function () {
|
|
|
+
|
|
|
+ control.classList2.remove( 'control-inactive' );
|
|
|
+
|
|
|
+ };
|
|
|
+
|
|
|
+ const settings = baseActions[ control.property ];
|
|
|
+
|
|
|
+ if ( !settings || !settings.weight ) {
|
|
|
+
|
|
|
+ control.setInactive();
|
|
|
+ }
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ function activateAction( action ) {
|
|
|
+
|
|
|
+ const clip = action.getClip();
|
|
|
+ const settings = baseActions[ clip.name ] || additiveActions[ clip.name ];
|
|
|
+ setWeight( action, settings.weight );
|
|
|
+ action.play();
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ function modifyTimeScale( speed ) {
|
|
|
+
|
|
|
+ mixer.timeScale = speed;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ function prepareCrossFade( startAction, endAction, duration ) {
|
|
|
+
|
|
|
+ // If the current action is 'idle', execute the crossfade immediately;
|
|
|
+ // else wait until the current action has finished its current loop
|
|
|
+
|
|
|
+ if ( currentBaseAction === 'idle' || ! startAction || ! endAction ) {
|
|
|
+
|
|
|
+ executeCrossFade( startAction, endAction, duration );
|
|
|
+
|
|
|
+ } else {
|
|
|
+
|
|
|
+ synchronizeCrossFade( startAction, endAction, duration );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // Update control colors
|
|
|
+
|
|
|
+ if ( endAction ) {
|
|
|
+
|
|
|
+ const clip = endAction.getClip();
|
|
|
+ currentBaseAction = clip.name;
|
|
|
+
|
|
|
+ } else {
|
|
|
+
|
|
|
+ currentBaseAction = 'None';
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ crossFadeControls.forEach( function ( control ) {
|
|
|
+
|
|
|
+ const name = control.property;
|
|
|
+
|
|
|
+ if ( name === currentBaseAction ) {
|
|
|
+
|
|
|
+ control.setActive();
|
|
|
+
|
|
|
+ } else {
|
|
|
+
|
|
|
+ control.setInactive();
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ function synchronizeCrossFade( startAction, endAction, duration ) {
|
|
|
+
|
|
|
+ mixer.addEventListener( 'loop', onLoopFinished );
|
|
|
+
|
|
|
+ function onLoopFinished( event ) {
|
|
|
+
|
|
|
+ if ( event.action === startAction ) {
|
|
|
+
|
|
|
+ mixer.removeEventListener( 'loop', onLoopFinished );
|
|
|
+
|
|
|
+ executeCrossFade( startAction, endAction, duration );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ function executeCrossFade( startAction, endAction, duration ) {
|
|
|
+
|
|
|
+ // Not only the start action, but also the end action must get a weight of 1 before fading
|
|
|
+ // (concerning the start action this is already guaranteed in this place)
|
|
|
+
|
|
|
+ if ( endAction ) {
|
|
|
+
|
|
|
+ setWeight( endAction, 1 );
|
|
|
+ endAction.time = 0;
|
|
|
+
|
|
|
+ if ( startAction ) {
|
|
|
+
|
|
|
+ // Crossfade with warping
|
|
|
+
|
|
|
+ startAction.crossFadeTo( endAction, duration, true );
|
|
|
+
|
|
|
+ } else {
|
|
|
+
|
|
|
+ // Fade in
|
|
|
+
|
|
|
+ endAction.fadeIn( duration );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ } else {
|
|
|
+
|
|
|
+ // Fade out
|
|
|
+
|
|
|
+ startAction.fadeOut( duration );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // This function is needed, since animationAction.crossFadeTo() disables its start action and sets
|
|
|
+ // the start action's timeScale to ((start animation's duration) / (end animation's duration))
|
|
|
+
|
|
|
+ function setWeight( action, weight ) {
|
|
|
+
|
|
|
+ action.enabled = true;
|
|
|
+ action.setEffectiveTimeScale( 1 );
|
|
|
+ action.setEffectiveWeight( weight );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ function onWindowResize() {
|
|
|
+
|
|
|
+ camera.aspect = window.innerWidth / window.innerHeight;
|
|
|
+ camera.updateProjectionMatrix();
|
|
|
+
|
|
|
+ renderer.setSize( window.innerWidth, window.innerHeight );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ function animate() {
|
|
|
+
|
|
|
+ // Render loop
|
|
|
+
|
|
|
+ requestAnimationFrame( animate );
|
|
|
+
|
|
|
+ for ( let i = 0; i !== numAnimations; ++ i ) {
|
|
|
+
|
|
|
+ const action = allActions[ i ];
|
|
|
+ const clip = action.getClip();
|
|
|
+ const settings = baseActions[ clip.name ] || additiveActions[ clip.name ];
|
|
|
+ settings.weight = action.getEffectiveWeight();
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // Get the time elapsed since the last frame, used for mixer update
|
|
|
+
|
|
|
+ var mixerUpdateDelta = clock.getDelta();
|
|
|
+
|
|
|
+ // Update the animation mixer, the stats panel, and render this frame
|
|
|
+
|
|
|
+ mixer.update( mixerUpdateDelta );
|
|
|
+
|
|
|
+ stats.update();
|
|
|
+
|
|
|
+ renderer.render( scene, camera );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ </script>
|
|
|
+
|
|
|
+ </body>
|
|
|
+</html>
|