第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的动画编辑器功能,主要内容包括:

  1. 动画系统概述:动画类型和编辑器界面
  2. 关键帧动画:创建和编辑关键帧
  3. 缓动函数:内置缓动和贝塞尔曲线控制
  4. 动画控制:播放控制和动画混合
  5. 路径动画:路径创建和沿路径运动
  6. 骨骼动画:骨骼结构和IK
  7. 形变动画:Morph Targets和表情动画
  8. 时间轴:动画序列和时间轴控制
  9. 导入导出:动画的保存和加载

通过本章的学习,读者应该能够使用Astral3D创建各种类型的动画效果,为3D场景增添动态生命力。


下一章预告:第八章将介绍Astral3D的插件系统开发,包括插件架构、开发流程、API使用和发布部署等内容。


posted @ 2026-01-10 13:17  我才是银古  阅读(13)  评论(0)    收藏  举报