ThreeJS官方案例学习(3)webgl - animation-skinning

在这里插入图片描述

<!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;
			}
			.control-inactive button {
    
    
				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>

		<!-- Import maps polyfill -->
		<!-- Remove this when import maps will be widely supported -->
		<script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>

		<script type="importmap">
			{
    
    
				"imports": {
    
    
					"three": "../build/three.module.js",
					"three/addons/": "./jsm/"
				}
			}
		</script>

		<script type="module">

			import * as THREE from 'three';

			import Stats from 'three/addons/libs/stats.module.js';
			import {
    
     GUI } from 'three/addons/libs/lil-gui.module.min.js';
			import {
    
     OrbitControls } from 'three/addons/controls/OrbitControls.js';
			import {
    
     GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

			let scene, renderer, camera, stats;
			let model, skeleton, mixer, clock;

			const crossFadeControls = [];

			let 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 }
			};
			let panelSettings, numAnimations;
			//初始化
			init();

			function init() {
    
    

				const container = document.getElementById( 'container' );
				//时钟
				clock = new THREE.Clock();
				//新建一个场景
				scene = new THREE.Scene();
				//设置背景
				scene.background = new THREE.Color( 0xa0a0a0 );
				//设置颜色渐变
				scene.fog = new THREE.Fog( 0xa0a0a0, 10, 50 );
				//半球光 光源直接放置于场景之上,光照颜色从天空光线颜色渐变到地面光线颜色。
				const hemiLight = new THREE.HemisphereLight( 0xffffff, 0x444444 );
				hemiLight.position.set( 0, 20, 0 );
				scene.add( hemiLight );
				//平行光是沿着特定方向发射的光。这种光的表现像是无限远,从它发出的光线都是平行的
				const 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 );

				//组合一个用于生成平面几何体与一种用于具有镜面高光的光泽表面的材质
				const mesh = new THREE.Mesh( new THREE.PlaneGeometry( 100, 100 ), new THREE.MeshPhongMaterial( {
    
     color: 0x999999, depthWrite: false } ) );
				//物体的局部旋转,以弧度来表示
				mesh.rotation.x = - Math.PI / 2;
				//材质是否接收阴影
				mesh.receiveShadow = true;
				scene.add( mesh );
				//用于载入glTF 2.0资源的加载器。
				const loader = new GLTFLoader();
				loader.load( 'models/gltf/Xbot.glb', function ( gltf ) {
    
    

					model = gltf.scene;
					scene.add( model );
					//以一个object3D对象作为第一个参数的函数
					model.traverse( function ( object ) {
    
    
						//对象是否被渲染到阴影贴图中
						if ( object.isMesh ) object.castShadow = true;

					} );
					//用来模拟骨骼 Skeleton 的辅助对象
					skeleton = new THREE.SkeletonHelper( model );
					skeleton.visible = true;
					scene.add( skeleton );
					//console.log(gltf);
					const animations = gltf.animations;
					动画混合器是用于场景中特定对象的动画的播放器。当场景中的多个对象独立动画时,每个对象都可以使用同一个动画混合器。
					mixer = new THREE.AnimationMixer( model );

					numAnimations = animations.length;
					//console.log("numAnimations:"+numAnimations);
					//根据动作配置来添加到动画混合器
					for ( let i = 0; i !== numAnimations; ++ i ) {
    
    
						let clip = animations[ i ];
						const name = clip.name;
						if ( baseActions[ name ] ) {
    
    
							const action = mixer.clipAction( clip );
							activateAction( action );
							baseActions[ name ].action = action;
							allActions.push( action );
						} else if ( additiveActions[ name ] ) {
    
    
							//移除动画片段的实例
							THREE.AnimationUtils.makeClipAdditive( clip );
							if ( clip.name.endsWith( '_pose' ) ) {
    
    
								//创建一个新的片段,仅包含所给定帧之间的原始片段。
								clip = THREE.AnimationUtils.subclip( clip, clip.name, 2, 3, 30 );
							}
							//传入一个动画片段,如果不存在符合传入的片段和根对象这两个参数的动作, 该方法将会创建一个
							const action = mixer.clipAction( clip );
							activateAction( action );
							additiveActions[ name ].action = action;
							allActions.push( action );

						}

					}

					createPanel();

					animate();

				} );
				//用WebGL渲染出你精心制作的场景。
				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 = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 100 );
				//相机的位置
				camera.position.set( - 1, 2, 3 );
				//轨道控制器 可以使得相机围绕目标进行轨道运动
				const controls = new OrbitControls( camera, renderer.domElement );
				//禁用摄像机平移,鼠标右键操作
				controls.enablePan = false;
				//禁用摄像机的缩放。
				controls.enableZoom = false;
				controls.target.set( 0, 1, 0 );
				controls.update();
				//添加性能监视器
				stats = new Stats();
				container.appendChild( stats.dom );
				//监听窗口缩放
				window.addEventListener( 'resize', onWindowResize );

			}
			//创建调试器GUI
			function createPanel() {
    
    
				const panel = new GUI( {
    
     width: 310 } );
				const folder1 = panel.addFolder( 'Base Actions' );
				const folder2 = panel.addFolder( 'Additive Action Weights' );
				const 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;
						if ( currentAction !== action ) {
    
    
							prepareCrossFade( currentAction, action, 0.35 );
						}
					};
					crossFadeControls.push( folder1.add( panelSettings, name ) );
				}
				for ( const name of Object.keys( additiveActions ) ) {
    
    
					const settings = additiveActions[ name ];
					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.setInactive = function () {
    
    
						control.domElement.classList.add( 'control-inactive' );
					};
					control.setActive = function () {
    
    
						control.domElement.classList.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 ) {
    
    
				// 如果当前动作是'idle',立即执行渐隐;否则等待当前动作完成当前循环
				if ( currentBaseAction === 'idle' || ! startAction || ! endAction ) {
    
    
					executeCrossFade( startAction, endAction, duration );
				} else {
    
    
					synchronizeCrossFade( startAction, endAction, duration );
				}

				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 );
				}
			}

			//动作的影响程度 (取值范围[0, 1]). 0 (无影响)到1(完全影响)
			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
				const 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>

猜你喜欢

转载自blog.csdn.net/ren365880/article/details/129846841