02.three官方示例+编辑器+AI快速学习webgl_animation_skinning_blending

发布于:2025-05-12 ⋅ 阅读:(19) ⋅ 点赞:(0)

本实例主要讲解内容

这个示例展示了Three.js中骨骼动画混合(Skeletal Animation Blending)的实现方法,通过加载一个士兵模型,演示了如何在不同动画状态(如站立、行走、跑步)之间进行平滑过渡。核心技术包括动画混合器(AnimationMixer)的使用、动画权重控制、动画同步淡入淡出,以及通过GUI控制面板实时调整动画参数。
在这里插入图片描述

完整代码注释

<!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">
		<link type="text/css" rel="stylesheet" href="main.css">
		<style>
			a {
				color: #f00;
			}
		</style>
	</head>
	<body>
		<!-- 渲染场景的容器 -->
		<div id="container"></div>
		<!-- 信息面板,显示项目信息和注意事项 -->
		<div id="info">
			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - Skeletal Animation Blending
			(model from <a href="https://www.mixamo.com/" target="_blank" rel="noopener">mixamo.com</a>)<br/>
			Note: crossfades are possible with blend weights being set to (1,0,0), (0,1,0) or (0,0,1)
		</div>

		<!-- 导入映射,指定模块导入路径 -->
		<script type="importmap">
			{
				"imports": {
					"three": "../build/three.module.js",
					"three/addons/": "./jsm/"
				}
			}
		</script>

		<script type="module">

			// 导入Three.js核心库和辅助工具
			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';  // GUI控制面板

			import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';  // GLTF模型加载器

			// 全局变量定义
			let scene, renderer, camera, stats;  // 场景、渲染器、相机和性能统计
			let model, skeleton, mixer, clock;  // 模型、骨骼辅助工具、动画混合器和时钟

			const crossFadeControls = [];  // 存储淡入淡出控制按钮的数组

			let idleAction, walkAction, runAction;  // 不同动画状态的动作
			let idleWeight, walkWeight, runWeight;  // 各动画的权重值
			let actions, settings;  // 动作数组和控制面板设置

			let singleStepMode = false;  // 单步模式标志
			let sizeOfNextStep = 0;  // 单步模式下一步的大小

			// 初始化函数
			init();

			function init() {

				// 获取渲染容器
				const container = document.getElementById( 'container' );

				// 创建透视相机,设置位置和朝向
				camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 100 );
				camera.position.set( 1, 2, - 3 );
				camera.lookAt( 0, 1, 0 );

				// 创建时钟,用于计算动画时间增量
				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, 0x8d8d8d, 3 );
				hemiLight.position.set( 0, 20, 0 );
				scene.add( hemiLight );

				// 添加方向光,用于产生阴影
				const dirLight = new THREE.DirectionalLight( 0xffffff, 3 );
				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 );

				// 可选:显示阴影相机辅助线,用于调试阴影
				// scene.add( new THREE.CameraHelper( dirLight.shadow.camera ) );

				// 创建地面平面
				const mesh = new THREE.Mesh( new THREE.PlaneGeometry( 100, 100 ), new THREE.MeshPhongMaterial( { color: 0xcbcbcb, depthWrite: false } ) );
				mesh.rotation.x = - Math.PI / 2;  // 旋转平面使其水平
				mesh.receiveShadow = true;  // 地面接收阴影
				scene.add( mesh );

				// 加载GLTF格式模型
				const loader = new GLTFLoader();
				loader.load( 'models/gltf/Soldier.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 );

					// 创建控制面板
					createPanel();

					// 获取模型中的所有动画
					const animations = gltf.animations;

					// 创建动画混合器,用于管理模型的所有动画
					mixer = new THREE.AnimationMixer( model );

					// 提取特定动画片段并创建动作
					idleAction = mixer.clipAction( animations[ 0 ] );  // 站立动画
					walkAction = mixer.clipAction( animations[ 3 ] );  // 行走动画
					runAction = mixer.clipAction( animations[ 1 ] );  // 跑步动画

					// 将所有动作存储到数组中
					actions = [ idleAction, walkAction, runAction ];

					// 激活所有动画动作
					activateAllActions();

					// 设置渲染循环,使用requestAnimationFrame持续更新和渲染场景
					renderer.setAnimationLoop( animate );

				} );

				// 初始化WebGL渲染器
				renderer = new THREE.WebGLRenderer( { antialias: true } );
				renderer.setPixelRatio( window.devicePixelRatio );  // 设置像素比,适配高DPI屏幕
				renderer.setSize( window.innerWidth, window.innerHeight );  // 设置渲染尺寸
				renderer.shadowMap.enabled = true;  // 启用阴影渲染
				container.appendChild( renderer.domElement );  // 将渲染器DOM元素添加到容器中

				// 添加性能统计面板
				stats = new Stats();
				container.appendChild( stats.dom );

				// 添加窗口大小变化事件监听,调整相机和渲染器
				window.addEventListener( 'resize', onWindowResize );

			}

			// 创建控制面板函数
			function createPanel() {

				// 创建GUI面板
				const panel = new GUI( { width: 310 } );

				// 创建不同功能的折叠面板
				const folder1 = panel.addFolder( 'Visibility' );  // 可见性控制
				const folder2 = panel.addFolder( 'Activation/Deactivation' );  // 动画激活/停用
				const folder3 = panel.addFolder( 'Pausing/Stepping' );  // 暂停/单步控制
				const folder4 = panel.addFolder( 'Crossfading' );  // 动画淡入淡出
				const folder5 = panel.addFolder( 'Blend Weights' );  // 混合权重
				const folder6 = panel.addFolder( 'General Speed' );  // 全局速度

				// 控制面板设置对象
				settings = {
					'show model': true,  // 是否显示模型
					'show skeleton': false,  // 是否显示骨骼
					'deactivate all': deactivateAllActions,  // 停用所有动画函数
					'activate all': activateAllActions,  // 激活所有动画函数
					'pause/continue': pauseContinue,  // 暂停/继续动画函数
					'make single step': toSingleStepMode,  // 切换到单步模式函数
					'modify step size': 0.05,  // 单步大小
					'from walk to idle': function () {  // 从行走到站立的过渡函数
						prepareCrossFade( walkAction, idleAction, 1.0 );
					},
					'from idle to walk': function () {  // 从站立到行走的过渡函数
						prepareCrossFade( idleAction, walkAction, 0.5 );
					},
					'from walk to run': function () {  // 从行走到跑步的过渡函数
						prepareCrossFade( walkAction, runAction, 2.5 );
					},
					'from run to walk': function () {  // 从跑步到行走的过渡函数
						prepareCrossFade( runAction, walkAction, 5.0 );
					},
					'use default duration': true,  // 是否使用默认过渡时长
					'set custom duration': 3.5,  // 自定义过渡时长
					'modify idle weight': 0.0,  // 站立动画权重
					'modify walk weight': 1.0,  // 行走动画权重
					'modify run weight': 0.0,  // 跑步动画权重
					'modify time scale': 1.0  // 动画全局速度
				};

				// 为各折叠面板添加控制项
				folder1.add( settings, 'show model' ).onChange( showModel );  // 模型可见性控制
				folder1.add( settings, 'show skeleton' ).onChange( showSkeleton );  // 骨骼可见性控制
				folder2.add( settings, 'deactivate all' );  // 停用所有动画按钮
				folder2.add( settings, 'activate all' );  // 激活所有动画按钮
				folder3.add( settings, 'pause/continue' );  // 暂停/继续按钮
				folder3.add( settings, 'make single step' );  // 单步模式按钮
				folder3.add( settings, 'modify step size', 0.01, 0.1, 0.001 );  // 单步大小滑块
				crossFadeControls.push( folder4.add( settings, 'from walk to idle' ) );  // 添加淡入淡出控制按钮
				crossFadeControls.push( folder4.add( settings, 'from idle to walk' ) );
				crossFadeControls.push( folder4.add( settings, 'from walk to run' ) );
				crossFadeControls.push( folder4.add( settings, 'from run to walk' ) );
				folder4.add( settings, 'use default duration' );  // 是否使用默认时长复选框
				folder4.add( settings, 'set custom duration', 0, 10, 0.01 );  // 自定义时长滑块
				// 添加动画权重滑块,并监听变化以更新动画权重
				folder5.add( settings, 'modify idle weight', 0.0, 1.0, 0.01 ).listen().onChange( function ( weight ) {
					setWeight( idleAction, weight );
				} );
				folder5.add( settings, 'modify walk weight', 0.0, 1.0, 0.01 ).listen().onChange( function ( weight ) {
					setWeight( walkAction, weight );
				} );
				folder5.add( settings, 'modify run weight', 0.0, 1.0, 0.01 ).listen().onChange( function ( weight ) {
					setWeight( runAction, weight );
				} );
				folder6.add( settings, 'modify time scale', 0.0, 1.5, 0.01 ).onChange( modifyTimeScale );  // 全局速度滑块

				// 默认打开所有折叠面板
				folder1.open();
				folder2.open();
				folder3.open();
				folder4.open();
				folder5.open();
				folder6.open();

			}

			// 控制模型可见性的函数
			function showModel( visibility ) {
				model.visible = visibility;
			}

			// 控制骨骼可见性的函数
			function showSkeleton( visibility ) {
				skeleton.visible = visibility;
			}

			// 修改动画全局速度的函数
			function modifyTimeScale( speed ) {
				mixer.timeScale = speed;
			}

			// 停用所有动画动作的函数
			function deactivateAllActions() {
				actions.forEach( function ( action ) {
					action.stop();  // 停止动画
				} );
			}

			// 激活所有动画动作的函数
			function activateAllActions() {
				// 设置各动画初始权重
				setWeight( idleAction, settings[ 'modify idle weight' ] );
				setWeight( walkAction, settings[ 'modify walk weight' ] );
				setWeight( runAction, settings[ 'modify run weight' ] );

				// 播放所有动画
				actions.forEach( function ( action ) {
					action.play();
				} );
			}

			// 暂停/继续动画的函数
			function pauseContinue() {
				if ( singleStepMode ) {  // 如果处于单步模式
					singleStepMode = false;  // 退出单步模式
					unPauseAllActions();  // 恢复所有动画
				} else {
					if ( idleAction.paused ) {  // 如果当前动画已暂停
						unPauseAllActions();  // 恢复所有动画
					} else {
						pauseAllActions();  // 暂停所有动画
					}
				}
			}

			// 暂停所有动画的函数
			function pauseAllActions() {
				actions.forEach( function ( action ) {
					action.paused = true;  // 设置动画暂停状态
				} );
			}

			// 恢复所有动画的函数
			function unPauseAllActions() {
				actions.forEach( function ( action ) {
					action.paused = false;  // 设置动画恢复状态
				} );
			}

			// 切换到单步模式的函数
			function toSingleStepMode() {
				unPauseAllActions();  // 先恢复所有动画
				singleStepMode = true;  // 启用单步模式
				sizeOfNextStep = settings[ 'modify step size' ];  // 设置下一步的大小
			}

			// 准备动画淡入淡出过渡的函数
			function prepareCrossFade( startAction, endAction, defaultDuration ) {
				// 根据用户选择设置过渡时长
				const duration = setCrossFadeDuration( defaultDuration );

				// 确保不处于单步模式,并恢复所有动画
				singleStepMode = false;
				unPauseAllActions();

				// 如果起始动画是站立动画(持续时间较长),立即执行过渡
				// 否则等待当前动画完成当前循环后再执行过渡
				if ( startAction === idleAction ) {
					executeCrossFade( startAction, endAction, duration );
				} else {
					synchronizeCrossFade( startAction, endAction, duration );
				}
			}

			// 设置动画过渡时长的函数
			function setCrossFadeDuration( defaultDuration ) {
				// 根据用户选择决定使用默认时长还是自定义时长
				if ( settings[ 'use default duration' ] ) {
					return defaultDuration;
				} else {
					return settings[ 'set custom duration' ];
				}
			}

			// 同步动画过渡的函数,确保在动画循环结束时进行过渡
			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 ) {
				// 在过渡前确保目标动画权重为1,并重置时间
				setWeight( endAction, 1 );
				endAction.time = 0;

				// 使用warping进行过渡(第三个参数为true),可以尝试设置为false不使用warping
				startAction.crossFadeTo( endAction, duration, true );
			}

			// 设置动画权重的函数
			function setWeight( action, weight ) {
				action.enabled = true;  // 启用动画
				action.setEffectiveTimeScale( 1 );  // 设置时间缩放为1
				action.setEffectiveWeight( weight );  // 设置动画权重
			}

			// 更新权重滑块显示的函数
			function updateWeightSliders() {
				settings[ 'modify idle weight' ] = idleWeight;
				settings[ 'modify walk weight' ] = walkWeight;
				settings[ 'modify run weight' ] = runWeight;
			}

			// 更新淡入淡出控制按钮状态的函数
			function updateCrossFadeControls() {
				// 根据当前动画权重状态启用/禁用相应的过渡按钮
				if ( idleWeight === 1 && walkWeight === 0 && runWeight === 0 ) {
					crossFadeControls[ 0 ].disable();  // 从行走到站立(禁用)
					crossFadeControls[ 1 ].enable();   // 从站立到行走(启用)
					crossFadeControls[ 2 ].disable();  // 从行走到跑步(禁用)
					crossFadeControls[ 3 ].disable();  // 从跑步到行走(禁用)
				}

				if ( idleWeight === 0 && walkWeight === 1 && runWeight === 0 ) {
					crossFadeControls[ 0 ].enable();   // 从行走到站立(启用)
					crossFadeControls[ 1 ].disable();  // 从站立到行走(禁用)
					crossFadeControls[ 2 ].enable();   // 从行走到跑步(启用)
					crossFadeControls[ 3 ].disable();  // 从跑步到行走(禁用)
				}

				if ( idleWeight === 0 && walkWeight === 0 && runWeight === 1 ) {
					crossFadeControls[ 0 ].disable();  // 从行走到站立(禁用)
					crossFadeControls[ 1 ].disable();  // 从站立到行走(禁用)
					crossFadeControls[ 2 ].disable();  // 从行走到跑步(禁用)
					crossFadeControls[ 3 ].enable();   // 从跑步到行走(启用)
				}
			}

			// 窗口大小变化事件处理函数
			function onWindowResize() {
				camera.aspect = window.innerWidth / window.innerHeight;  // 更新相机宽高比
				camera.updateProjectionMatrix();  // 更新相机投影矩阵
				renderer.setSize( window.innerWidth, window.innerHeight );  // 更新渲染器尺寸
			}

			// 动画循环函数,每一帧都会被调用
			function animate() {
				// 获取当前各动画的有效权重
				idleWeight = idleAction.getEffectiveWeight();
				walkWeight = walkAction.getEffectiveWeight();
				runWeight = runAction.getEffectiveWeight();

				// 如果权重被"外部"(如淡入淡出)修改,更新面板显示
				updateWeightSliders();

				// 根据当前权重状态启用/禁用相应的过渡按钮
				updateCrossFadeControls();

				// 获取自上一帧以来的时间增量,用于更新动画混合器
				let mixerUpdateDelta = clock.getDelta();

				// 如果处于单步模式,执行一步并暂停
				if ( singleStepMode ) {
					mixerUpdateDelta = sizeOfNextStep;
					sizeOfNextStep = 0;
				}

				// 更新动画混合器,渲染场景,并更新性能统计
				mixer.update( mixerUpdateDelta );
				renderer.render( scene, camera );
				stats.update();
			}

		</script>

	</body>
</html>

整体总结

这个Three.js示例展示了骨骼动画混合的实现方法,主要内容包括:

  1. 核心技术

    • 使用AnimationMixer管理多个动画
    • 通过clipAction获取特定动画片段
    • 控制动画权重实现动画混合
    • 使用crossFadeTo方法实现平滑过渡
  2. 动画控制方式

    • 直接控制:通过调整各动画权重实现混合
    • 过渡控制:在不同动画状态间实现平滑过渡
    • 同步过渡:确保动画在循环结束时进行过渡,避免动作中断
  3. 用户交互

    • 通过GUI面板提供直观控制
    • 可调整动画权重、过渡时长、全局速度等参数
    • 支持暂停、单步模式等特殊控制方式

交流学习: Three.js 场景编辑器 (Vue3 + TypeScript
实现)

https://threelab.cn/threejs-edit/


网站公告

今日签到

点亮在社区的每一天
去签到