04.three官方示例+编辑器+AI快速学习webgl_animation_skinning_additive_blending
本实例主要讲解内容
这个示例展示了Three.js中**骨骼动画的叠加混合(Additive Animation Blending)**技术。通过加载一个机器人模型,演示了如何在基础动画(如站立、行走、跑步)之上叠加额外的动画效果(如潜行姿态、悲伤表情、点头同意等),实现更丰富的角色表现。
核心技术包括:
- 如何将动画设置为叠加模式
- 基础动画与叠加动画的权重控制
- 动画之间的平滑过渡
- 叠加动画参考帧的处理
完整代码注释
<!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><!-- 导入映射,指定模块导入路径 --><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 { OrbitControls } from 'three/addons/controls/OrbitControls.js'; // 轨道控制器import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; // GLTF模型加载器// 全局变量定义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, 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 );// 创建地面平面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/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 );// 获取模型中的所有动画const 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 );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();// 设置渲染循环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元素添加到容器中// 设置相机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 );}// 创建控制面板函数function createPanel() {// 创建GUI面板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 // 动画全局速度设置};// 获取所有基础动画名称,并添加"None"选项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 {// 否则设置为"None"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 ) {if ( endAction ) { // 如果有目标动画// 设置目标动画权重为1,并重置时间setWeight( endAction, 1 );endAction.time = 0;if ( startAction ) {// 从起始动画过渡到目标动画(带warping效果)startAction.crossFadeTo( endAction, duration, true );} else {// 淡入目标动画(没有起始动画)endAction.fadeIn( duration );}} else { // 如果没有目标动画// 淡出起始动画startAction.fadeOut( duration );}}// 设置动画权重的函数function setWeight( action, weight ) {action.enabled = true; // 启用动画action.setEffectiveTimeScale( 1 ); // 设置时间缩放为1action.setEffectiveWeight( weight ); // 设置动画权重}// 窗口大小变化事件处理函数function onWindowResize() {camera.aspect = window.innerWidth / window.innerHeight; // 更新相机宽高比camera.updateProjectionMatrix(); // 更新相机投影矩阵renderer.setSize( window.innerWidth, window.innerHeight ); // 更新渲染器尺寸}// 动画循环函数,每一帧都会被调用function 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();}// 获取自上一帧以来的时间增量,用于更新动画混合器const mixerUpdateDelta = clock.getDelta();// 更新动画混合器,渲染场景,并更新性能统计mixer.update( mixerUpdateDelta );renderer.render( scene, camera );stats.update();}</script></body>
</html>
整体总结
这个Three.js示例展示了骨骼动画的叠加混合技术,主要内容包括:
-
叠加动画原理:
- 叠加动画基于参考帧计算偏移量,可在基础动画之上叠加额外效果
- 使用
THREE.AnimationUtils.makeClipAdditive()
将动画设置为叠加模式 - 对于姿态类动画,使用
subclip()
提取特定帧作为参考姿态
-
动画控制系统:
- 基础动画系统:站立、行走、跑步等主要动作
- 叠加动画系统:潜行姿态、悲伤表情、点头等次要动作
- 动画过渡:使用
crossFadeTo()
和fadeIn/fadeOut()
实现平滑过渡
-
用户交互:
- 基础动画切换:通过按钮在不同基础动画间切换
- 叠加动画权重控制:通过滑块调整各叠加动画的影响程度
- 全局速度控制:调整所有动画的播放速度
这种叠加动画技术在游戏、虚拟角色动画等场景中非常有用,可以让角色表现更加丰富多样,同时保持动画系统的灵活性和可维护性。