Motion Canvas 中的骨骼动画:实现角色关节运动
你是否曾想过用代码让角色关节自然运动?Motion Canvas 提供了强大的向量运算和动画系统,只需几行代码就能实现专业级骨骼动画。本文将通过制作一个会挥手的卡通角色,带你掌握骨骼动画的核心原理。
骨骼动画基础概念
骨骼动画(Skeletal Animation)通过层级关节结构模拟生物运动,每个关节(Joint)可独立旋转并带动子关节运动。在 Motion Canvas 中,这一过程通过向量变换和插值动画实现。核心依赖以下模块:
构建关节层级结构
关节层级就像一棵倒置的树,根关节(Root Joint)是所有运动的起点。以下代码创建一个包含上臂、前臂和手部的三层结构:
// 创建关节容器
const arm = createRef<Group>();
// 根关节(肩部)
const shoulder = createRef<Circle>();
// 子关节(肘部)
const elbow = createRef<Circle>();
// 孙关节(手腕)
const wrist = createRef<Circle>();
view.add(
group(arm, {
children: [
// 肩部关节
circle(shoulder, {
x: -200,
y: 0,
radius: 15,
fill: '#333',
children: [
// 上臂骨骼
line({
points: [[0, 0], [100, 0]],
stroke: '#666',
lineWidth: 8,
children: [
// 肘部关节
circle(elbow, {
x: 100,
radius: 12,
fill: '#444',
children: [
// 前臂骨骼
line({
points: [[0, 0], [80, 0]],
stroke: '#666',
lineWidth: 6,
children: [
// 手腕关节
circle(wrist, {
x: 80,
radius: 10,
fill: '#555',
children: [
// 手部
circle({
radius: 18,
fill: '#666',
}),
],
}),
],
}),
],
}),
],
}),
],
}),
],
}),
);
关节间通过父子关系建立连接,当父关节移动时,所有子关节会跟随变换。这种层级结构在 Motion Canvas 中通过 group 组件的嵌套实现。
实现关节旋转
关节旋转是骨骼动画的核心。利用 Vector2 类的 rotate 方法,可以围绕任意点旋转向量。以下是肘部旋转的实现代码:
// 肘部旋转函数
function rotateElbow(angle: number) {
// 获取当前肘部位置
const currentPos = elbow().position();
// 计算旋转中心(肩部位置)
const pivot = shoulder().position();
// 使用 Vector2.rotate 方法计算新位置
// 参数:旋转角度(度),旋转中心
const newPos = currentPos.rotate(angle, pivot);
// 更新肘部位置
elbow().position(newPos);
}
Vector2 的旋转功能基于矩阵变换实现,关键代码如下:
// Vector2.rotate 方法实现
public rotate(angle: number, center: PossibleVector2 = Vector2.zero): Vector2 {
const originVector = new Vector2(center);
// 创建旋转矩阵
const matrix = Matrix2D.fromTranslation(originVector)
.rotate(angle)
.translate(originVector.flipped);
return this.transformAsPoint(matrix);
}
创建平滑动画
使用 tween 函数实现关节旋转的平滑过渡。以下代码让角色完成一个挥手动作:
// 挥手动画序列
yield* sequence(
// 抬起手臂:肩部旋转 -30°
tween(0.5, (t) => {
const angle = lerp(0, -30, t);
shoulder().rotation(angle);
}),
// 弯曲肘部:肘部旋转 80°
tween(0.3, (t) => {
const angle = lerp(0, 80, t);
rotateElbow(angle);
}),
// 伸直肘部
tween(0.4, (t) => {
const angle = lerp(80, 0, t);
rotateElbow(angle);
}),
// 放下手臂
tween(0.5, (t) => {
const angle = lerp(-30, 0, t);
shoulder().rotation(angle);
}),
);
tween 函数通过线性插值实现平滑过渡,核心原理是在指定时间内持续更新目标属性:
// tween 函数核心实现
export function* tween(
seconds: number,
onProgress: (value: number, time: number) => void,
): ThreadGenerator {
const startTime = thread.time();
const endTime = startTime + seconds;
while (endTime > thread.fixed) {
const time = thread.fixed - startTime;
const value = time / seconds; // 0到1之间的插值比例
onProgress(value, time);
yield;
}
onProgress(1, seconds); // 确保动画结束在目标状态
}
完整角色动画实现
结合以上技术,我们可以实现一个完整的挥手动画。关键在于通过层级关节的协同运动,模拟自然的生理结构:
import {Circle, Group, Line, createRef, useRef} from '@motion-canvas/2d';
import {tween, sequence, lerp} from '@motion-canvas/core';
export default makeScene2D(function* (view) {
// 创建关节引用
const shoulder = useRef<Circle>();
const elbow = useRef<Circle>();
const wrist = useRef<Circle>();
// 关节旋转函数
const rotateJoint = (
joint: () => Circle,
angle: number,
pivot: () => Circle
) => {
const currentPos = joint().position();
const pivotPos = pivot().position();
joint().position(currentPos.rotate(angle, pivotPos));
};
// 绘制骨骼结构
view.add(
group({
children: [
// 肩部关节
circle(shoulder, {
x: -200,
radius: 15,
fill: '#333',
children: [
line({ // 上臂
points: [[0, 0], [100, 0]],
stroke: '#666',
lineWidth: 8,
children: [
circle(elbow, { // 肘部关节
x: 100,
radius: 12,
fill: '#444',
children: [
line({ // 前臂
points: [[0, 0], [80, 0]],
stroke: '#666',
lineWidth: 6,
children: [
circle(wrist, { // 手腕关节
x: 80,
radius: 10,
fill: '#555',
children: [
circle({ // 手部
radius: 18,
fill: '#666',
}),
],
}),
],
}),
],
}),
],
}),
],
}),
],
}),
);
// 循环挥手动画
while (true) {
yield* sequence([
tween(0.5, t => {
shoulder().rotation(lerp(0, -30, t));
}),
tween(0.3, t => {
rotateJoint(elbow, lerp(0, 80, t), shoulder);
}),
tween(0.4, t => {
rotateJoint(elbow, lerp(80, 0, t), shoulder);
}),
tween(0.5, t => {
shoulder().rotation(lerp(-30, 0, t));
}),
tween(1, () => {}), // 停顿1秒
]);
}
});
优化与高级技巧
缓动函数增强真实感
默认线性动画显得生硬,通过缓动函数可以模拟物理世界的加速度:
import {easeInOutSine} from '@motion-canvas/core/lib/tweening/timingFunctions';
// 使用缓动函数的动画
yield* tween(0.5, (t) => {
// 应用正弦缓动
const easedT = easeInOutSine(t);
shoulder().rotation(lerp(0, -30, easedT));
});
限制关节活动范围
添加角度约束防止关节过度旋转:
// 带约束的旋转函数
function rotateWithConstraints(
joint: () => Circle,
angle: number,
pivot: () => Circle,
minAngle: number,
maxAngle: number
) {
const clampedAngle = Math.max(minAngle, Math.min(maxAngle, angle));
rotateJoint(joint, clampedAngle, pivot);
}
// 使用示例:肘部旋转限制在-30°到120°
rotateWithConstraints(elbow, targetAngle, shoulder, -30, 120);
总结与扩展
通过本文学习,你已掌握 Motion Canvas 骨骼动画的核心技术:
- 层级关节结构:利用 group 组件创建父子关系
- 向量旋转变换:使用 Vector2.rotate 实现关节运动
- 平滑过渡动画:通过 tween 函数控制运动节奏
进阶方向:
- 结合 inverse kinematics(反向运动学)实现更自然的肢体运动
- 使用 Spring 动画 添加物理效果
- 导入外部骨骼数据,实现复杂角色动画
掌握骨骼动画后,你可以创建交互式角色、机械装置演示甚至简单游戏。访问 官方示例库 查看更多动画效果。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



