Motion Canvas 中的骨骼动画:实现角色关节运动

Motion Canvas 中的骨骼动画:实现角色关节运动

【免费下载链接】motion-canvas Visualize Your Ideas With Code 【免费下载链接】motion-canvas 项目地址: https://gitcode.com/gh_mirrors/mo/motion-canvas

你是否曾想过用代码让角色关节自然运动?Motion Canvas 提供了强大的向量运算和动画系统,只需几行代码就能实现专业级骨骼动画。本文将通过制作一个会挥手的卡通角色,带你掌握骨骼动画的核心原理。

骨骼动画基础概念

骨骼动画(Skeletal Animation)通过层级关节结构模拟生物运动,每个关节(Joint)可独立旋转并带动子关节运动。在 Motion Canvas 中,这一过程通过向量变换和插值动画实现。核心依赖以下模块:

  • 向量系统Vector2 类 提供旋转、缩放等空间变换能力
  • 动画系统tween 函数 实现平滑过渡效果
  • 场景图:通过父子关系构建关节层级结构

构建关节层级结构

关节层级就像一棵倒置的树,根关节(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 骨骼动画的核心技术:

  1. 层级关节结构:利用 group 组件创建父子关系
  2. 向量旋转变换:使用 Vector2.rotate 实现关节运动
  3. 平滑过渡动画:通过 tween 函数控制运动节奏

进阶方向:

  • 结合 inverse kinematics(反向运动学)实现更自然的肢体运动
  • 使用 Spring 动画 添加物理效果
  • 导入外部骨骼数据,实现复杂角色动画

掌握骨骼动画后,你可以创建交互式角色、机械装置演示甚至简单游戏。访问 官方示例库 查看更多动画效果。

【免费下载链接】motion-canvas Visualize Your Ideas With Code 【免费下载链接】motion-canvas 项目地址: https://gitcode.com/gh_mirrors/mo/motion-canvas

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值