第07章-动画编辑器
第七章:动画编辑器
7.1 动画系统概述
Astral3D提供了功能完善的动画编辑器,支持关键帧动画、曲线编辑、骨骼动画、形变动画等多种动画类型,可以为3D场景创建丰富的动态效果。
7.1.1 动画类型
| 动画类型 | 说明 | 适用场景 |
|---|---|---|
| 变换动画 | 位置、旋转、缩放的变化 | 物体移动、摄像机漫游 |
| 属性动画 | 材质、光照等属性变化 | 颜色渐变、亮度变化 |
| 骨骼动画 | 骨骼驱动的变形动画 | 角色动画、机械臂 |
| 形变动画 | 顶点形变动画 | 表情动画、形态变化 |
| 路径动画 | 沿路径运动 | 车辆行驶、相机漫游 |
| 序列帧动画 | 贴图序列播放 | 2D特效、UI动画 |
7.1.2 动画编辑器界面
┌─────────────────────────────────────────────────────────────────────┐
│ 动画编辑器 │
├──────────────────────────────────────┬──────────────────────────────┤
│ 3D预览视口 │ 曲线编辑器 │
│ │ ┌─────────────────────┐ │
│ [播放] [暂停] [停止] [循环] │ │ 贝塞尔曲线 │ │
│ │ │ ╱ │ │
│ │ │ ╱ │ │
│ │ │╱ │ │
│ │ └─────────────────────┘ │
│ │ │
├──────────────────────────────────────┴──────────────────────────────┤
│ 时间轴 (Timeline) │
│ 0 1 2 3 4 5 6 7 8 9 10 (秒) │
│ ├────┼────┼────┼────┼────┼────┼────┼────┼────┼────┤ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 建筑 - 位置X [●]─────────────[●]────────[●] │ │
│ │ 建筑 - 位置Y [●]──────────────[●] │ │
│ │ 建筑 - 旋转Y [●]────────────────────────────[●] │ │
│ │ 摄像机 - 位置 [●]──[●]──[●]──[●]──[●]──[●]──[●] │ │
│ │ 灯光 - 强度 [●]─────────────────────[●] │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ [添加轨道] [删除轨道] [复制关键帧] [粘贴] [删除关键帧] │
└─────────────────────────────────────────────────────────────────────┘
7.2 关键帧动画
7.2.1 基础概念
import { AnimationClip, KeyframeTrack, AnimationMixer } from 'three';
import { AnimationEditor } from '@astral3d/engine';
// 关键帧数据结构
interface Keyframe {
time: number; // 时间(秒)
value: any; // 值
easing?: string; // 缓动函数
handle?: { // 贝塞尔控制点
in: { x: number, y: number };
out: { x: number, y: number };
};
}
// 动画轨道结构
interface AnimationTrack {
name: string; // 轨道名称
property: string; // 属性路径
type: string; // 值类型
keyframes: Keyframe[];
}
7.2.2 创建关键帧动画
// 创建动画编辑器
const animEditor = new AnimationEditor(viewer);
// 选择要动画的对象
const building = viewer.scene.getObjectByName('建筑');
// 添加位置动画轨道
animEditor.addTrack({
object: building,
property: 'position.y',
name: '建筑上升动画'
});
// 添加关键帧
animEditor.addKeyframe({
track: '建筑上升动画',
time: 0,
value: 0
});
animEditor.addKeyframe({
track: '建筑上升动画',
time: 2,
value: 10,
easing: 'easeOutBounce'
});
animEditor.addKeyframe({
track: '建筑上升动画',
time: 4,
value: 5
});
7.2.3 编程方式创建动画
// 使用Three.js原生API创建动画
const times = [0, 1, 2]; // 关键帧时间
const values = [0, 10, 5]; // 关键帧值
const positionTrack = new THREE.NumberKeyframeTrack(
'.position[y]', // 属性路径
times,
values
);
// 创建动画片段
const clip = new THREE.AnimationClip(
'建筑动画',
-1, // 自动计算时长
[positionTrack]
);
// 创建动画混合器
const mixer = new THREE.AnimationMixer(building);
// 创建动画动作
const action = mixer.clipAction(clip);
// 播放动画
action.play();
// 在渲染循环中更新
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
mixer.update(delta);
viewer.render();
}
7.2.4 多属性动画
// 位置关键帧
const positionKF = new THREE.VectorKeyframeTrack(
'.position',
[0, 2, 4],
[
0, 0, 0, // t=0
10, 5, 0, // t=2
0, 10, 0 // t=4
]
);
// 旋转关键帧(四元数)
const rotationKF = new THREE.QuaternionKeyframeTrack(
'.quaternion',
[0, 2, 4],
[
0, 0, 0, 1, // t=0
0, 0.707, 0, 0.707, // t=2 (Y轴旋转90度)
0, 1, 0, 0 // t=4 (Y轴旋转180度)
]
);
// 缩放关键帧
const scaleKF = new THREE.VectorKeyframeTrack(
'.scale',
[0, 2, 4],
[
1, 1, 1, // t=0
1.5, 1.5, 1.5, // t=2
1, 1, 1 // t=4
]
);
// 组合为动画片段
const clip = new THREE.AnimationClip(
'复合动画',
4,
[positionKF, rotationKF, scaleKF]
);
7.3 缓动函数与曲线编辑
7.3.1 内置缓动函数
// 线性
'linear'
// 二次方
'easeInQuad', 'easeOutQuad', 'easeInOutQuad'
// 三次方
'easeInCubic', 'easeOutCubic', 'easeInOutCubic'
// 四次方
'easeInQuart', 'easeOutQuart', 'easeInOutQuart'
// 五次方
'easeInQuint', 'easeOutQuint', 'easeInOutQuint'
// 正弦
'easeInSine', 'easeOutSine', 'easeInOutSine'
// 指数
'easeInExpo', 'easeOutExpo', 'easeInOutExpo'
// 圆形
'easeInCirc', 'easeOutCirc', 'easeInOutCirc'
// 弹性
'easeInElastic', 'easeOutElastic', 'easeInOutElastic'
// 回退
'easeInBack', 'easeOutBack', 'easeInOutBack'
// 弹跳
'easeInBounce', 'easeOutBounce', 'easeInOutBounce'
7.3.2 缓动函数可视化
线性 (linear):
│ ╱
│ ╱
│ ╱
│ ╱
└────────
easeOutQuad:
│ ╱─
│ ╱
│ ╱
│╱
└────────
easeInOutCubic:
│ ╱─
│ ╱
│ •
│ ╱
└─╱───────
easeOutBounce:
│ ╱─╮╭╮
│ ╱ ╰╯
│ ╱
│╱
└────────
easeOutElastic:
│ ╱╮
│ ╱ ╰╮╭╮
│ ╱ ╰╯
│╱
└────────
7.3.3 贝塞尔曲线控制
// 自定义贝塞尔曲线
animEditor.addKeyframe({
track: 'position.x',
time: 0,
value: 0,
handle: {
out: { x: 0.2, y: 0 } // 出控制点
}
});
animEditor.addKeyframe({
track: 'position.x',
time: 2,
value: 100,
handle: {
in: { x: -0.2, y: 0 }, // 入控制点
out: { x: 0.2, y: 0 }
}
});
// 使用CSS贝塞尔曲线格式
const customEasing = 'cubic-bezier(0.25, 0.1, 0.25, 1.0)';
7.3.4 曲线编辑器操作
// 曲线编辑器API
const curveEditor = animEditor.curveEditor;
// 选择关键帧
curveEditor.selectKeyframe(trackIndex, keyframeIndex);
// 移动关键帧
curveEditor.moveKeyframe(trackIndex, keyframeIndex, {
time: 1.5,
value: 50
});
// 调整手柄
curveEditor.setHandle(trackIndex, keyframeIndex, {
in: { x: -0.3, y: 0.5 },
out: { x: 0.3, y: -0.2 }
});
// 平滑曲线
curveEditor.smoothKeyframes(trackIndex);
// 线性化
curveEditor.linearizeKeyframes(trackIndex);
// 阶梯化
curveEditor.stepKeyframes(trackIndex);
7.4 动画控制
7.4.1 播放控制
// 获取动画动作
const action = mixer.clipAction(clip);
// 播放
action.play();
// 暂停
action.paused = true;
// 继续
action.paused = false;
// 停止
action.stop();
// 重置
action.reset();
// 设置播放速度
action.timeScale = 2; // 2倍速
// 设置权重(用于混合)
action.weight = 0.5;
// 设置循环模式
action.loop = THREE.LoopOnce; // 播放一次
action.loop = THREE.LoopRepeat; // 重复播放
action.loop = THREE.LoopPingPong; // 往返播放
// 设置钳制
action.clampWhenFinished = true; // 播放完成后保持最后一帧
// 跳转到指定时间
action.time = 2.5;
// 交叉淡入淡出
action1.crossFadeTo(action2, 1); // 1秒过渡
7.4.2 动画事件
// 监听动画事件
mixer.addEventListener('finished', (event) => {
console.log('动画完成:', event.action.getClip().name);
});
mixer.addEventListener('loop', (event) => {
console.log('动画循环:', event.action.getClip().name);
});
// 自定义动画事件(标记点)
animEditor.addMarker({
time: 2.5,
name: 'explosion',
callback: () => {
createExplosion();
}
});
animEditor.addMarker({
time: 5,
name: 'sound',
callback: () => {
playSound('impact.mp3');
}
});
7.4.3 动画混合
// 创建多个动画
const idleClip = loadAnimation('idle.fbx');
const walkClip = loadAnimation('walk.fbx');
const runClip = loadAnimation('run.fbx');
const idleAction = mixer.clipAction(idleClip);
const walkAction = mixer.clipAction(walkClip);
const runAction = mixer.clipAction(runClip);
// 播放idle
idleAction.play();
// 从idle过渡到walk
function startWalking() {
walkAction.reset();
walkAction.play();
idleAction.crossFadeTo(walkAction, 0.5);
}
// 从walk过渡到run
function startRunning() {
runAction.reset();
runAction.play();
walkAction.crossFadeTo(runAction, 0.3);
}
// 停止所有动画,回到idle
function stopMoving() {
const current = mixer.getRoot()._currentAction;
idleAction.reset();
idleAction.play();
current.crossFadeTo(idleAction, 0.5);
}
// 混合多个动画
function blendAnimations(weights) {
idleAction.weight = weights.idle;
walkAction.weight = weights.walk;
runAction.weight = weights.run;
}
7.5 路径动画
7.5.1 创建路径
import { CatmullRomCurve3 } from 'three';
// 创建路径点
const points = [
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(10, 5, 10),
new THREE.Vector3(20, 0, 20),
new THREE.Vector3(30, 5, 10),
new THREE.Vector3(40, 0, 0)
];
// 创建曲线
const curve = new THREE.CatmullRomCurve3(points);
// 闭合路径
curve.closed = true;
// 曲线类型
curve.curveType = 'catmullrom'; // centripetal, chordal, catmullrom
// 张力(仅catmullrom)
curve.tension = 0.5;
// 可视化路径
const pathGeometry = new THREE.TubeGeometry(curve, 100, 0.1, 8, false);
const pathMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const pathMesh = new THREE.Mesh(pathGeometry, pathMaterial);
scene.add(pathMesh);
7.5.2 沿路径运动
// 路径动画控制器
class PathAnimator {
curve: THREE.CatmullRomCurve3;
object: THREE.Object3D;
progress: number = 0;
speed: number = 1;
lookAhead: boolean = true;
constructor(curve, object) {
this.curve = curve;
this.object = object;
}
update(delta) {
// 更新进度
this.progress += delta * this.speed / this.curve.getLength();
if (this.progress > 1) this.progress -= 1;
// 获取位置
const position = this.curve.getPointAt(this.progress);
this.object.position.copy(position);
// 朝向运动方向
if (this.lookAhead) {
const lookAt = this.curve.getPointAt(
Math.min(this.progress + 0.01, 1)
);
this.object.lookAt(lookAt);
}
}
}
// 使用
const pathAnimator = new PathAnimator(curve, car);
pathAnimator.speed = 5; // 每秒移动5单位
function animate() {
requestAnimationFrame(animate);
pathAnimator.update(clock.getDelta());
viewer.render();
}
7.5.3 相机路径动画
// 相机漫游路径
const cameraPath = new THREE.CatmullRomCurve3([
new THREE.Vector3(0, 10, 50),
new THREE.Vector3(30, 15, 30),
new THREE.Vector3(50, 10, 0),
new THREE.Vector3(30, 15, -30),
new THREE.Vector3(0, 10, -50),
new THREE.Vector3(-30, 15, -30),
new THREE.Vector3(-50, 10, 0),
new THREE.Vector3(-30, 15, 30)
]);
cameraPath.closed = true;
// 观察目标路径
const targetPath = new THREE.CatmullRomCurve3([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(5, 0, 5),
new THREE.Vector3(0, 0, 10),
new THREE.Vector3(-5, 0, 5),
new THREE.Vector3(0, 0, 0)
]);
targetPath.closed = true;
// 相机动画
class CameraPathAnimator {
cameraPath: THREE.CatmullRomCurve3;
targetPath: THREE.CatmullRomCurve3;
camera: THREE.Camera;
progress: number = 0;
duration: number = 30; // 30秒完成一圈
update(delta) {
this.progress += delta / this.duration;
if (this.progress > 1) this.progress -= 1;
// 更新相机位置
const camPos = this.cameraPath.getPointAt(this.progress);
this.camera.position.copy(camPos);
// 更新观察目标
const targetPos = this.targetPath.getPointAt(this.progress);
this.camera.lookAt(targetPos);
}
}
7.6 骨骼动画
7.6.1 骨骼结构
// 骨骼层级结构示例
/*
Root
├── Hips
│ ├── Spine
│ │ ├── Spine1
│ │ │ ├── Spine2
│ │ │ │ ├── Neck
│ │ │ │ │ └── Head
│ │ │ │ ├── LeftShoulder
│ │ │ │ │ └── LeftArm
│ │ │ │ │ └── LeftForeArm
│ │ │ │ │ └── LeftHand
│ │ │ │ └── RightShoulder
│ │ │ │ └── RightArm
│ │ │ │ └── RightForeArm
│ │ │ │ └── RightHand
│ ├── LeftUpLeg
│ │ └── LeftLeg
│ │ └── LeftFoot
│ └── RightUpLeg
│ └── RightLeg
│ └── RightFoot
*/
// 访问骨骼
const skeleton = skinnedMesh.skeleton;
const bones = skeleton.bones;
// 获取特定骨骼
const headBone = skeleton.getBoneByName('Head');
const leftHandBone = skeleton.getBoneByName('LeftHand');
// 修改骨骼变换
headBone.rotation.y = Math.PI / 4; // 头部转向
leftHandBone.rotation.z = Math.PI / 2; // 手臂抬起
7.6.2 加载骨骼动画
import { FBXLoader, GLTFLoader } from '@astral3d/engine';
// 加载FBX模型和动画
const fbxLoader = new FBXLoader();
// 加载模型
const model = await fbxLoader.load('character.fbx');
scene.add(model);
// 创建混合器
const mixer = new THREE.AnimationMixer(model);
// 加载动画
const walkAnim = await fbxLoader.load('walk.fbx');
const idleAnim = await fbxLoader.load('idle.fbx');
const runAnim = await fbxLoader.load('run.fbx');
// 应用动画到模型
const walkAction = mixer.clipAction(walkAnim.animations[0]);
const idleAction = mixer.clipAction(idleAnim.animations[0]);
const runAction = mixer.clipAction(runAnim.animations[0]);
// 播放
idleAction.play();
7.6.3 IK(逆向运动学)
import { CCDIKSolver, IKS } from 'three/examples/jsm/animation/CCDIKSolver';
// IK配置
const ikConfig = {
target: targetBone, // 目标骨骼
effector: handBone, // 效果器骨骼
links: [ // 链接骨骼
{ index: foreArmIndex },
{ index: armIndex, rotationMin: new THREE.Vector3(-Math.PI/2, 0, 0),
rotationMax: new THREE.Vector3(Math.PI/2, 0, Math.PI) },
{ index: shoulderIndex }
],
iteration: 10, // 迭代次数
minAngle: 0,
maxAngle: 1
};
// 创建IK求解器
const ikSolver = new CCDIKSolver(mesh, [ikConfig]);
// 更新
function animate() {
ikSolver.update();
renderer.render(scene, camera);
}
// 设置目标位置
function setHandTarget(position) {
targetBone.position.copy(position);
}
7.7 形变动画
7.7.1 Morph Targets
// 创建带形变目标的几何体
const geometry = new THREE.BoxGeometry(1, 1, 1);
// 添加形变目标
const morphPositions = [];
morphPositions.push(createMorphTarget1(geometry));
morphPositions.push(createMorphTarget2(geometry));
geometry.morphAttributes.position = morphPositions;
// 创建材质
const material = new THREE.MeshStandardMaterial({
morphTargets: true
});
// 创建网格
const mesh = new THREE.Mesh(geometry, material);
// 设置形变权重
mesh.morphTargetInfluences[0] = 0.5; // 50%变形到目标1
mesh.morphTargetInfluences[1] = 0.3; // 30%变形到目标2
7.7.2 形变动画
// 创建形变动画轨道
const morphTrack = new THREE.NumberKeyframeTrack(
'.morphTargetInfluences[0]', // 形变目标0
[0, 1, 2], // 时间
[0, 1, 0] // 权重值
);
// 创建动画
const morphClip = new THREE.AnimationClip('MorphAnimation', 2, [morphTrack]);
const morphAction = mixer.clipAction(morphClip);
morphAction.play();
7.7.3 表情动画
// 表情形变目标
const expressions = {
neutral: 0,
smile: 1,
sad: 2,
angry: 3,
surprised: 4
};
// 设置表情
function setExpression(name, weight = 1) {
// 重置所有表情
Object.values(expressions).forEach(index => {
face.morphTargetInfluences[index] = 0;
});
// 设置目标表情
face.morphTargetInfluences[expressions[name]] = weight;
}
// 表情过渡动画
function transitionExpression(from, to, duration) {
const fromIndex = expressions[from];
const toIndex = expressions[to];
const times = [0, duration];
const fromValues = [1, 0];
const toValues = [0, 1];
const fromTrack = new THREE.NumberKeyframeTrack(
`.morphTargetInfluences[${fromIndex}]`,
times, fromValues
);
const toTrack = new THREE.NumberKeyframeTrack(
`.morphTargetInfluences[${toIndex}]`,
times, toValues
);
const clip = new THREE.AnimationClip('ExpressionTransition', duration, [fromTrack, toTrack]);
const action = mixer.clipAction(clip);
action.clampWhenFinished = true;
action.loop = THREE.LoopOnce;
action.play();
}
7.8 动画序列与时间轴
7.8.1 动画序列
// 动画序列管理器
class AnimationSequence {
animations: { clip: THREE.AnimationClip; startTime: number }[] = [];
currentTime: number = 0;
duration: number = 0;
// 添加动画到序列
add(clip: THREE.AnimationClip, startTime: number) {
this.animations.push({ clip, startTime });
const endTime = startTime + clip.duration;
this.duration = Math.max(this.duration, endTime);
}
// 播放序列
play(mixer: THREE.AnimationMixer) {
this.animations.forEach(({ clip, startTime }) => {
const action = mixer.clipAction(clip);
action.startAt(mixer.time + startTime);
action.play();
});
}
}
// 使用
const sequence = new AnimationSequence();
sequence.add(fadeInClip, 0); // 0秒开始淡入
sequence.add(moveClip, 1); // 1秒开始移动
sequence.add(rotateClip, 2); // 2秒开始旋转
sequence.add(fadeOutClip, 4); // 4秒开始淡出
sequence.play(mixer);
7.8.2 时间轴控制
// 时间轴控制器
class Timeline {
mixer: THREE.AnimationMixer;
tracks: Map<string, THREE.AnimationAction> = new Map();
duration: number = 0;
currentTime: number = 0;
playing: boolean = false;
constructor(mixer: THREE.AnimationMixer) {
this.mixer = mixer;
}
// 添加轨道
addTrack(name: string, clip: THREE.AnimationClip) {
const action = this.mixer.clipAction(clip);
action.clampWhenFinished = true;
this.tracks.set(name, action);
this.duration = Math.max(this.duration, clip.duration);
}
// 播放
play() {
this.tracks.forEach(action => action.play());
this.playing = true;
}
// 暂停
pause() {
this.tracks.forEach(action => action.paused = true);
this.playing = false;
}
// 停止
stop() {
this.tracks.forEach(action => action.stop());
this.currentTime = 0;
this.playing = false;
}
// 跳转到指定时间
seek(time: number) {
this.tracks.forEach(action => {
action.time = time;
});
this.currentTime = time;
}
// 获取当前时间
getTime(): number {
return this.currentTime;
}
// 更新
update(delta: number) {
if (this.playing) {
this.currentTime += delta;
if (this.currentTime >= this.duration) {
this.currentTime = this.duration;
this.pause();
}
}
}
}
7.9 导入导出
7.9.1 导出动画
// 导出为JSON
function exportAnimationToJSON(clip: THREE.AnimationClip) {
return THREE.AnimationClip.toJSON(clip);
}
// 导出为GLB(包含动画)
async function exportModelWithAnimations(model, animations) {
const exporter = new GLTFExporter();
const options = {
animations: animations,
binary: true
};
const gltf = await exporter.parseAsync(model, options);
// 下载
const blob = new Blob([gltf], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'model_with_animation.glb';
link.click();
}
7.9.2 导入动画
// 从JSON导入
function importAnimationFromJSON(json) {
return THREE.AnimationClip.parse(json);
}
// 从GLB导入
async function importAnimationsFromGLB(url) {
const loader = new GLTFLoader();
const gltf = await loader.loadAsync(url);
return gltf.animations;
}
// 应用导入的动画到不同模型
function retargetAnimation(animation, targetModel) {
// 创建新的轨道数组
const newTracks = animation.tracks.map(track => {
// 根据骨骼名称映射
const newName = mapBoneName(track.name, targetModel);
return track.clone().setName(newName);
});
return new THREE.AnimationClip(
animation.name,
animation.duration,
newTracks
);
}
7.10 本章小结
本章详细介绍了Astral3D的动画编辑器功能,主要内容包括:
- 动画系统概述:动画类型和编辑器界面
- 关键帧动画:创建和编辑关键帧
- 缓动函数:内置缓动和贝塞尔曲线控制
- 动画控制:播放控制和动画混合
- 路径动画:路径创建和沿路径运动
- 骨骼动画:骨骼结构和IK
- 形变动画:Morph Targets和表情动画
- 时间轴:动画序列和时间轴控制
- 导入导出:动画的保存和加载
通过本章的学习,读者应该能够使用Astral3D创建各种类型的动画效果,为3D场景增添动态生命力。
下一章预告:第八章将介绍Astral3D的插件系统开发,包括插件架构、开发流程、API使用和发布部署等内容。

浙公网安备 33010602011771号