第十八节:骨骼动画 - 角色动画控制
第十八节:骨骼动画 - 角色动画控制
GLTF动画剪辑与状态混合技术
1. 核心概念解析
1.1 骨骼动画基本原理
组件 | 作用 | Three.js对应类 |
---|---|---|
骨骼 | 构成角色骨架的关节层级 | Bone |
蒙皮 | 将网格顶点绑定到骨骼上 | SkinnedMesh |
权重 | 顶点受多个骨骼影响的程度 | skinWeight 属性 |
动画剪辑 | 包含骨骼关键帧动画数据 | AnimationClip |
动画混合器 | 控制动画播放和混合 | AnimationMixer |
1.2 动画系统工作流程
2. 基础动画控制
2.1 加载GLTF模型与动画
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader';let mixer, animations, model;// 加载带骨骼动画的GLTF模型
const loader = new GLTFLoader();
loader.load('/models/character.glb', (gltf) => {model = gltf.scene;scene.add(model);// 获取动画剪辑animations = gltf.animations;// 创建动画混合器mixer = new THREE.AnimationMixer(model);// 创建动画动作const walkAction = mixer.clipAction(animations[0]);const idleAction = mixer.clipAction(animations[1]);// 播放行走动画walkAction.play();// 调整动画播放速度walkAction.setEffectiveTimeScale(1.2);// 设置动画循环模式walkAction.setLoop(THREE.LoopRepeat, Infinity);
});// 在渲染循环中更新动画
function animate() {requestAnimationFrame(animate);if (mixer) {mixer.update(clock.getDelta()); // 使用时钟确保平滑更新}renderer.render(scene, camera);
}
2.2 动画剪辑管理
// 创建动画剪辑映射
const animationActions = {idle: null,walk: null,run: null,jump: null
};// 初始化所有动画动作
function setupAnimations() {animations.forEach((clip) => {const action = mixer.clipAction(clip);// 根据剪辑名称映射动作if (clip.name.toLowerCase().includes('idle')) {animationActions.idle = action;} else if (clip.name.toLowerCase().includes('walk')) {animationActions.walk = action;} else if (clip.name.toLowerCase().includes('run')) {animationActions.run = action;} else if (clip.name.toLowerCase().includes('jump')) {animationActions.jle = action;}});// 设置默认动画animationActions.idle.play();
}
3. 高级动画技术
3.1 动画混合与过渡
// 当前激活的动作
let currentAction = null;// 切换到新动画
function fadeToAction(actionName, duration = 0.3) {if (currentAction === animationActions[actionName]) return;const newAction = animationActions[actionName];const oldAction = currentAction;// 准备新动作newAction.reset();newAction.play();newAction.setEffectiveTimeScale(1.0);newAction.crossFadeFrom(oldAction, duration, true);currentAction = newAction;
}// 键盘控制示例
document.addEventListener('keydown', (event) => {switch(event.key) {case 'w':fadeToAction('walk');break;case 'r':fadeToAction('run');break;case ' ':fadeToAction('jump');break;case 's':fadeToAction('idle');break;}
});
3.2 动画遮罩技术
// 创建上半身遮罩(只影响上半身骨骼)
function createUpperBodyMask(skeleton) {const upperBodyBones = ['spine', 'spine1', 'spine2', 'neck', 'head', 'shoulder_L', 'arm_L', 'forearm_L', 'hand_L','shoulder_R', 'arm_R', 'forearm_R', 'hand_R'];const trackStates = [];skeleton.bones.forEach((bone, boneIndex) => {// 检查是否是上半身骨骼const isUpperBody = upperBodyBones.some(name => bone.name.toLowerCase().includes(name.toLowerCase()));trackStates[boneIndex] = isUpperBody;});return trackStates;
}// 应用动画遮罩
function setupUpperBodyAnimation() {const attackAction = mixer.clipAction(animations[2]);const walkAction = mixer.clipAction(animations[1]);// 创建遮罩const upperBodyMask = createUpperBodyMask(model.skeleton);// 应用遮罩到攻击动作(只影响上半身)attackAction.setEffectiveWeight(1);attackAction.enabled = true;// 设置骨骼遮罩attackAction.getClip().tracks.forEach((track) => {const boneName = track.name.split('.')[0];const boneIndex = model.skeleton.bones.findIndex(b => b.name === boneName);if (boneIndex !== -1 && !upperBodyMask[boneIndex]) {track.setEffectiveWeight(0); // 禁用非上半身骨骼的动画}});// 同时播放行走和攻击walkAction.play();attackAction.play();
}
3.3 根运动处理
// 存储原始位置
const originalPosition = new THREE.Vector3();// 启用根运动(将动画位移应用到模型)
function enableRootMotion(action) {action.getClip().tracks.forEach((track) => {if (track.name.endsWith('.position')) {// 监听位置变化track.addEventListener('change', (event) => {const position = event.target.getValue();model.position.add(new THREE.Vector3(position.x - originalPosition.x,position.y - originalPosition.y,position.z - originalPosition.z));originalPosition.copy(position);});}});
}// 初始化根运动
const walkAction = mixer.clipAction(animations[1]);
enableRootMotion(walkAction);
walkAction.play();
4. 性能优化与注意事项
4.1 动画性能优化
优化策略 | 实现方法 | 效果 |
---|---|---|
动画压缩 | 使用GLTF动画压缩扩展 | 文件大小减少60% |
LOD系统 | 根据距离简化骨骼数量 | 帧率提升30% |
GPU加速 | 使用GPUAnimation 扩展 | CPU占用降低70% |
缓存重用 | 复用动画混合器和动作 | 内存占用降低40% |
4.2 常见问题与解决方案
问题1:动画播放卡顿
- 原因:帧率不稳定导致混合器更新不均匀
- 解决方案:
// 使用固定时间步长 const clock = new THREE.Clock(); let previousTime = 0;function animate() {requestAnimationFrame(animate);const time = clock.getElapsedTime();const delta = time - previousTime;previousTime = time;if (mixer) {mixer.update(delta);}renderer.render(scene, camera); }
问题2:动画权重设置无效
- 排查步骤:
- 确认骨骼名称匹配
- 检查权重值范围(0-1)
- 验证动画剪辑包含目标骨骼
问题3:蒙皮变形异常
- 现象:模型扭曲或撕裂
- 解决方案:
- 检查权重分配是否正确
- 确认骨骼层级结构完整
- 验证骨骼数量限制(移动端最多60根骨骼)
4.3 调试技巧
// 1. 显示骨骼辅助器
const skeletonHelper = new THREE.SkeletonHelper(model);
scene.add(skeletonHelper);// 2. 打印骨骼结构
function printBoneHierarchy(bone, depth = 0) {console.log(' '.repeat(depth * 2) + bone.name);bone.children.forEach(child => {if (child.isBone) {printBoneHierarchy(child, depth + 1);}});
}// 3. 可视化权重
const helper = new THREE.SkeletonHelper(model);
helper.material = new THREE.LineBasicMaterial({ color: 0xff0000, linewidth: 2
});
scene.add(helper);
5. 完整案例:角色动画控制器
class CharacterAnimator {constructor(model, animations) {this.model = model;this.animations = animations;this.mixer = new THREE.AnimationMixer(model);this.actions = {};this.currentAction = null;this.setupAnimations();}setupAnimations() {// 创建所有动画动作this.animations.forEach((clip) => {this.actions[clip.name] = this.mixer.clipAction(clip);});// 设置默认动画this.currentAction = this.actions['Idle'];this.currentAction.play();}fadeToAction(actionName, duration = 0.2) {if (this.currentAction === this.actions[actionName]) return;const newAction = this.actions[actionName];const oldAction = this.currentAction;// 准备新动作newAction.reset();newAction.play();newAction.crossFadeFrom(oldAction, duration, true);this.currentAction = newAction;}update(delta) {if (this.mixer) {this.mixer.update(delta);}}// 键盘控制setupKeyboardControls() {document.addEventListener('keydown', (event) => {switch(event.key.toLowerCase()) {case 'w': this.fadeToAction('Walk'); break;case 'r': this.fadeToAction('Run'); break;case ' ': this.fadeToAction('Jump'); break;case 'e': this.fadeToAction('Attack'); break;case 's': this.fadeToAction('Idle'); break;}});}
}// 使用示例
let characterAnimator;loader.load('/models/character.glb', (gltf) => {const model = gltf.scene;scene.add(model);characterAnimator = new CharacterAnimator(model, gltf.animations);characterAnimator.setupKeyboardControls();
});// 在渲染循环中更新
function animate() {requestAnimationFrame(animate);const delta = clock.getDelta();if (characterAnimator) {characterAnimator.update(delta);}renderer.render(scene, camera);
}
下一节预告:阴影进阶
第十九节:软阴影与性能平衡技术
核心内容:
- PCF软阴影原理与实现
- 阴影贴图分辨率优化策略
- 级联阴影映射(CSM)技术
- 实时阴影性能调优技巧
重点掌握:实现高质量实时阴影的同时保持性能平衡,适用于各种复杂场景。