R RenderComp
remotion キャラクターアニメーション svg アニメーション チュートリアル

Remotionでキャラクターアニメーション — Reactで2Dキャラを動かす完全ガイド

Remotionでキャラクターアニメーション — Reactで2Dキャラを動かす完全ガイド

マスコット、解説動画のプレゼンター、SNSクリップのリアクションキャラ——アニメーションキャラクターは「After Effectsと専用リギングプラグインの領域」と思われがちです。しかし2Dキャラクターは、構造的に見れば「ピボット点を持つ図形のツリー」にすぎません。これはRemotionと非常に相性の良い構造です。リグはReactコンポーネント、各関節はグループ化されたtransform、そしてすべての動きは useCurrentFrame() の決定論的な関数として書けます。

この記事では、Remotion 4.x でキャラクターアニメーションを組み立てるワークフローを解説します。手法の選択、SVGリグの構築、interpolate()spring() による関節駆動、きれいにループする歩行・待機サイクル、基本のリップシンク、そしてprops駆動の再利用可能なコンポーネント化までを扱います。


3つのアプローチ — スプライトシート・SVGリグ・Lottie

Remotionでキャラクターを動かす実用的な方法は3つあり、それぞれ「絵の自由度」と「プログラムからの制御性」のトレードオフが異なります。

スプライトシートは、事前に描いたアニメーションのコマを1枚の画像に敷き詰め、現在のフレームに応じてセルを切り替える方式です。どんな絵柄でも使え、描画コストも軽い一方、すべての動きを事前に描いておく必要があります。レンダリング時に色・ポーズ・タイミングを変えることはできません。

SVGリグは、キャラクターをボディパーツごとにグループ化したベクター図形として描き、関節のピボット点に回転transformを仕込む方式です。手足の角度・色・表情・タイミングのすべてがコード上の変数になり、propsやデータから駆動できます。最も「Remotionらしい」アプローチで、この記事の主役です。

Lottieキャラクターは、After EffectsのアニメーションをBodymovinで書き出し、@remotion/lottie パッケージでコンポジションにフレーム同期させて再生する方式です。デザイナー品質のモーションが得られますが、アニメーションは書き出し時点で固定されます。制御できるのは再生であって、骨格ではありません。「焼き込み済みアニメーションが有利になる場面」は RemotionとLottieの徹底比較で詳しく解説しています。

最も実装が短いスプライトシート方式を、まず完全なコードで示します。

import { staticFile, useCurrentFrame, useVideoConfig } from 'remotion';

const FRAME_W = 256;
const FRAME_H = 256;
const SHEET_FRAMES = 8; // シートに描かれたコマ数
const PLAYBACK_FPS = 12; // 伝統的な2Dアニメは12fpsのタイミングで動く

export const SpriteWalk: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // 現在の動画フレームで表示すべきスプライトシートのセル
  const cell = Math.floor((frame / fps) * PLAYBACK_FPS) % SHEET_FRAMES;

  return (
    <div
      style={{
        width: FRAME_W,
        height: FRAME_H,
        backgroundImage: `url(${staticFile('walk-sheet.png')})`,
        backgroundPosition: `${-cell * FRAME_W}px 0px`,
        backgroundRepeat: 'no-repeat',
      }}
    />
  );
};

動画は30fpsでレンダリングしながら、シートは12fpsで再生する——この2つを切り離すことで、手描きアニメ特有のリズムが生まれます。セルのインデックスが useCurrentFrame() から導出されるため、結果は決定論的で、Studioのシークにも正しく追従します。


グループ化transformでSVGキャラクターリグを作る

SVGリグは階層構造です。胴体が腕と頭を運び、腰が脚を運びます。独立して回転するパーツごとに <g> 要素を用意し、SVGのtransform属性 rotate(angle cx cy) で明示的なピボット点を中心に回転させます。SVG要素に対するCSSの transform-origin よりレンダラー間の挙動が安定しており、ピボットの位置がコード上で明示される利点もあります。

リグ本体は純粋でステートレスなコンポーネントにします。フレームや時間のことは何も知らず、ただ「ポーズ」を受け取って描くだけです。

export type CharacterPose = {
  headTilt: number; // 度数。首がピボット
  leftArm: number;  // 度数。肩がピボット
  rightArm: number;
  leftLeg: number;  // 度数。股関節がピボット
  rightLeg: number;
  bodyY: number;    // 全身の上下動(px)
};

export const Character: React.FC<{
  pose: CharacterPose;
  shirtColor?: string;
  skinColor?: string;
}> = ({ pose, shirtColor = '#3b82f6', skinColor = '#fcd9b8' }) => {
  return (
    <svg viewBox="0 0 200 320" style={{ width: '100%', height: '100%' }}>
      <g transform={`translate(0, ${pose.bodyY})`}>
        {/* 脚 — 股関節がピボット */}
        <g transform={`rotate(${pose.leftLeg} 88 205)`}>
          <rect x="80" y="200" width="16" height="90" rx="8" fill="#1e293b" />
        </g>
        <g transform={`rotate(${pose.rightLeg} 112 205)`}>
          <rect x="104" y="200" width="16" height="90" rx="8" fill="#1e293b" />
        </g>

        {/* 胴体 */}
        <rect x="68" y="115" width="64" height="95" rx="24" fill={shirtColor} />

        {/* 腕 — 肩がピボット */}
        <g transform={`rotate(${pose.leftArm} 74 130)`}>
          <rect x="66" y="122" width="14" height="75" rx="7" fill={shirtColor} />
        </g>
        <g transform={`rotate(${pose.rightArm} 126 130)`}>
          <rect x="120" y="122" width="14" height="75" rx="7" fill={shirtColor} />
        </g>

        {/* 頭 — 首がピボット */}
        <g transform={`rotate(${pose.headTilt} 100 118)`}>
          <circle cx="100" cy="72" r="44" fill={skinColor} />
          <circle cx="84" cy="64" r="5" fill="#1e293b" />
          <circle cx="116" cy="64" r="5" fill="#1e293b" />
          <path
            d="M 86 90 Q 100 100 114 90"
            stroke="#1e293b"
            strokeWidth="4"
            fill="none"
            strokeLinecap="round"
          />
        </g>
      </g>
    </svg>
  );
};

ここで重要な構造上の判断が2つあります。第一に、SVGでは描画順がそのままz順になります。脚を胴体より先に描くことで胴体の後ろに隠れ、腕を後に描くことで前を通ります。第二に、目と口は頭のグループの内側に置きます。こうすると首をかしげたときに顔全体が一緒に動きます。リギングの8割は階層設計であり、アニメーションのコードはむしろ単純です。

IllustratorやFigmaで描いたキャラクターを書き出す場合も原則は同じです。パスをボディパーツごとにグループ化し、グループレベルにピボット回転を追加してください。ストロークの描画やモーフィングなどパスレベルのテクニックはSVGアニメーションガイドで解説しており、リグと組み合わせて使えます。


関節駆動のモーション — interpolateとspringで手足と頭を動かす

リグが角度をデータとして受け取る構造になっていれば、アニメーションは「フレームごとに角度を計算する」だけの問題になります。制御されたイージングには interpolate()、腕がパッと上がる・頭がわずかにオーバーシュートしながら向くといった物理的な動きには spring() を使います。スプリング物理で腕を上げ、手を振るキャラクターの例です。

import {
  AbsoluteFill,
  Easing,
  interpolate,
  spring,
  useCurrentFrame,
  useVideoConfig,
} from 'remotion';
import { Character } from './Character';

export const WavingCharacter: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // 腕はスプリング物理で持ち上がり……
  const raise = spring({
    frame,
    fps,
    config: { mass: 0.8, stiffness: 120, damping: 12 },
  });

  // ……持ち上がった位置を中心に毎秒2往復で振れる
  const wave = Math.sin((frame / fps) * Math.PI * 4) * 18;
  const rightArm = interpolate(raise, [0, 1], [0, -150]) + wave * raise;

  // 上げた腕の方向へ、ベジェ曲線イージングで軽く首をかしげる
  const headTilt = interpolate(frame, [0, 20], [0, -6], {
    extrapolateRight: 'clamp',
    easing: Easing.bezier(0.33, 1, 0.68, 1),
  });

  return (
    <AbsoluteFill style={{ alignItems: 'center', justifyContent: 'center' }}>
      <div style={{ width: 360, height: 576 }}>
        <Character
          pose={{ headTilt, leftArm: 4, rightArm, leftLeg: 0, rightLeg: 0, bodyY: 0 }}
        />
      </div>
    </AbsoluteFill>
  );
};

盗む価値があるディテールは wave * raise です。振り子の振動にスプリングの進行度を掛けることで、腕が上がるにつれて手振りがフェードインします。これがないと、腕が下がったままの状態で手首だけがぶるぶる震えてしまいます。「周期関数をスプリングの上に重ねる」——これが、説得力のあるキャラクターモーションの大半を支える核心のテクニックです。

スプリングの設定はキャラクターの性格に直結します。dampingが低いと弾んでコミカルに、高いと落ち着いて堂々とした印象になります。massstiffnessdamping のチューニングはspringアニメーション完全ガイドで深掘りしています。


歩行サイクル・待機ループ・リアクションビート

歩行サイクル

歩行サイクルは周期運動であり、Remotionにおける周期運動は「時間に対する Math.sin()」です。押さえるべき関係は3つ。左右の脚は半周期ずれる、腕は同じ側の脚と逆に振れる、体は足が着地するたびに1回上下する、です。

export const WalkingCharacter: React.FC<{ stepsPerSecond?: number }> = ({
  stepsPerSecond = 2,
}) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // πごとに1歩、2πで1サイクル(左右1歩ずつ)
  const phase = (frame / fps) * stepsPerSecond * Math.PI;

  const leftLeg = Math.sin(phase) * 26;
  const rightLeg = Math.sin(phase + Math.PI) * 26;
  const leftArm = Math.sin(phase + Math.PI) * 20; // 同じ側の脚と逆位相
  const rightArm = Math.sin(phase) * 20;
  const bodyY = Math.abs(Math.sin(phase)) * -4; // 着地のたびに上下動
  const headTilt = Math.sin(phase) * 2;

  return <Character pose={{ headTilt, leftArm, rightArm, leftLeg, rightLeg, bodyY }} />;
};

すべての値が1つの位相(phase)から導出されるため、サイクルはどんな尺でも継ぎ目なくループし、stepsPerSecond を変えるだけで全身のタイミングが整合したまま変わります。親要素に interpolate(frame, ...) で駆動する translateX を足せば、キャラクターが画面を横切って歩きます。

待機ループ

何も起きていない時間にキャラクターが「ただのステッカー」に見えないようにするには、安価な材料が2つあれば十分です。ゆっくりした呼吸と、周期的なまばたきです。

// ゆっくりした呼吸: 約0.4Hzの微細な上下動
const breathe = Math.sin((frame / fps) * Math.PI * 0.8) * 1.5;

// まばたき: 3秒ごとに4フレームだけ目を閉じる
const blink = frame % (fps * 3) < 4 ? 0.1 : 1;

// 頭のグループ内で目をellipseにしておくと、ryを潰して閉じられる:
// <ellipse cx="84" cy="64" rx="5" ry={5 * blink} fill="#1e293b" />

リアクションビート

驚いて跳ねる、うれしくてバウンドする——リアクションは特定の瞬間に置かれる単発モーションです。最もきれいな構造は「1ビートにつき1つの <Sequence>」。各ポーズコンポーネントの useCurrentFrame() がゼロから再スタートするため、スプリングが毎回フレッシュに発火します。

<AbsoluteFill>
  <Sequence durationInFrames={90}>
    <IdleCharacter />
  </Sequence>
  <Sequence from={90} durationInFrames={60}>
    <SurprisedCharacter />
  </Sequence>
  <Sequence from={150}>
    <WavingCharacter />
  </Sequence>
</AbsoluteFill>

既存コンポーネントの内部でビートを起こしたい場合は、スプリングの delay オプションを使います。spring({ frame, fps, delay: 60, config }) はフレーム60まで静止し、そこから発火します。出力を interpolate(pop, [0, 0.5, 1], [0, -50, 0]) に通せば、1本のスプリングで「跳ねて戻る」動きが作れます。<Sequence> によるタイムライン設計の詳細はSequenceタイミング完全ガイドを参照してください。


基本のリップシンクとトーキングヘッドのタイミング

本格的なリップシンクは奥の深い分野ですが、説得力のある基本形に必要なのは「閉じ・半開き・開き」の3つの口形状と、「いつどの形状がアクティブか」を示すキューのタイムラインだけです。オープンソースのRhubarb Lip Syncを使うと、ナレーション音声ファイルからこのキューを自動生成でき、その出力はこの構造にそのままマッピングできます。

import { staticFile, useCurrentFrame, useVideoConfig } from 'remotion';
import { Audio } from '@remotion/media';

type MouthCue = {
  start: number; // 秒
  end: number;
  shape: 'closed' | 'mid' | 'open';
};

const cues: MouthCue[] = [
  { start: 0.0, end: 0.22, shape: 'mid' },
  { start: 0.22, end: 0.4, shape: 'open' },
  { start: 0.4, end: 0.55, shape: 'closed' },
  // ……実際には音声から自動生成する(手書きしない)
];

const Mouth: React.FC<{ shape: MouthCue['shape'] }> = ({ shape }) => {
  if (shape === 'open') {
    return <ellipse cx="100" cy="92" rx="10" ry="8" fill="#7f1d1d" />;
  }
  if (shape === 'mid') {
    return <ellipse cx="100" cy="92" rx="9" ry="4" fill="#7f1d1d" />;
  }
  return (
    <path d="M 88 92 L 112 92" stroke="#1e293b" strokeWidth="4" strokeLinecap="round" />
  );
};

export const TalkingHead: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
  const t = frame / fps;

  const active = cues.find((c) => t >= c.start && t < c.end);

  return (
    <>
      <Audio src={staticFile('voiceover.mp3')} />
      <svg viewBox="0 0 200 200" style={{ width: 600, height: 600, margin: 'auto' }}>
        <circle cx="100" cy="72" r="44" fill="#fcd9b8" />
        <circle cx="84" cy="64" r="5" fill="#1e293b" />
        <circle cx="116" cy="64" r="5" fill="#1e293b" />
        <Mouth shape={active?.shape ?? 'closed'} />
      </svg>
    </>
  );
};

音声の再生と口形状のルックアップが同じフレームクロックから導出されるため、両者がズレることは原理的にありません。Studioでどのフレームにシークしても、口はその瞬間の音声と一致します。長い台詞では、キューの上にわずかな頭の動きを重ねてください。口だけが動く完全静止の頭は、すぐに不気味に見え始めます。音声の扱い全般はRemotionオーディオガイドにまとめています。


キャラクターをprops駆動にして複数動画で再利用する

キャラクターをコンポーネントとして作る本当の見返りは再利用です。見た目と振る舞いのすべての判断をpropsにしておけば、同じリグが色・アニメーション・テンポの異なる何十本もの動画に使い回せます。Studioでは defaultProps、自動レンダリングパイプラインでは inputProps から駆動します。

export type MascotProps = {
  shirtColor: string;
  skinColor: string;
  animation: 'idle' | 'walk' | 'wave';
  stepsPerSecond: number;
};

// src/Root.tsx 内
<Composition
  id="Mascot"
  component={Mascot}
  durationInFrames={240}
  fps={30}
  width={1080}
  height={1080}
  defaultProps={{
    shirtColor: '#3b82f6',
    skinColor: '#fcd9b8',
    animation: 'wave',
    stepsPerSecond: 2,
  }}
/>

Mascot の内部では animation propがどのポーズ計算ブランチを実行するかを選び、色のpropsはSVGのfillにそのまま流れ込みます。キャラクターコンポーネントを本当に再利用可能に保つための指針は次のとおりです。

  • リグとモーションを分離する。 Character コンポーネントはポーズを描くだけ、モーションコンポーネントはポーズを計算するだけ。1つのリグがすべてのアニメーションに使える
  • propsは interface ではなく type で定義する。 さらにZodスキーマを用意すると、Remotion Studioのpropsパネルで編集・バリデーションできる
  • タイミングはフレームでなく秒で表現するstepsPerSecond、キューの秒数)。コンポーネント内部で fps を使って変換すれば、24・30・60fpsのどれでも同じキャラクターが動く
  • すべてのpropsに妥当なデフォルトを与える。 設定ゼロでも見られるものがレンダリングされる状態を保つ

パフォーマンス — 複数キャラクターのシーンを速く保つ

キャラクターリグは軽量です。フルのSVGキャラクターでもDOMノードは数十個で、transformのアニメーションは安価です。5〜10体のシーンは問題なくレンダリングできます。さらにスケールさせる場合の指針です。

  • 画面外のキャラクターはアンマウントする。 各キャラクターの出演区間を <Sequence durationInFrames={...}> で包む。Sequenceはウィンドウ外で子をアンマウントするため、3秒だけ登場するキャラクターは他の時間帯のコストがゼロになる
  • 静的なジオメトリはメモ化する。 柄物の衣装や生成された髪の毛など、マウント時に決まる計算は useMemo で包み、毎フレーム再計算させない
  • フィルタを手足の単位でかけない。 ボディパーツごとの drop-shadowblur は毎フレーム高コストなラスタライズを強制する。フィルタはキャラクターグループ全体に1つだけかけるか、足元の楕円で影を擬似的に作る
  • 背景の群衆にはスプライトシートを使う。 主役リグ10体+スプライトシートの背景歩行者40体のほうが、ライブリグ50体よりはるかに良い予算配分になる
  • プレビューとレンダリングの性能は別物。 Studioのプレビューが重くても、最終レンダリングが遅い・壊れるわけではない。レンダリングは決定論的にすべてのフレームを確実にキャプチャする。判断はレンダリング結果で行い、重いシーンのプレビューは解像度を半分に落とす

より広いパフォーマンス最適化はRemotionパフォーマンスガイドで扱っています。


よくある質問(FAQ)

Q: キャラクターモーションにCSSアニメーションやtransitionを使えますか?

使えません。CSSアニメーションは実時間(wall-clock)で動くため、Remotionのフレームクロックと同期せず、意図した動きとズレた状態でフレームがキャプチャされます。アニメーションする値はすべて useCurrentFrame() から導出する必要があります。

Q: CSSの transform + transform-origin ではなく、SVGの transform 属性を使う理由は?

SVG要素に対するCSSの transform-origintransform-box の挙動に依存し、歴史的に実装間の一貫性がありません。属性形式の rotate(angle cx cy) はピボットをviewBox座標としてtransform自体に焼き込むため、RemotionのChromiumレンダラーを含め、どこでも同一に動作します。

Q: リグの絵柄をもっとリッチにしたい場合は?

リグの構造(グループ階層とピボット)はそのままに、各 <g> の中身を差し替えるだけです。矩形と円をIllustrator/Figmaから書き出した本格的なパスに置き換えても、ポーズを駆動するコードは1行も変わりません。「構造とスタイルの分離」がSVGリグの強みです。


キャラクターモーションスターターパックで最速スタート

ここまでの内容はすべて、ゼロから1〜2日で構築できます。ただし面倒な部分——ピボットの位置決め、スプリングのチューニング、きれいにループする位相の数式、小さいサイズでも判別できる口形状——は、検証済みの出発点から始めるのが最も効率的です。

RenderComp のライブラリには、構築済みSVGリグ、すぐ使える歩行・待機・手振り・トーキングのサイクル、色・タイミング・アニメーション選択の型付きpropsを備えたキャラクターモーションテンプレートが揃っています。すべて編集可能なTypeScriptソースとして提供されるため、白紙のキャンバスで肩のピボットをデバッグする代わりに、動いているキャラクターから自分のマスコットへ作り替えるところから始められます。

キャラクター・モーション系テンプレートは rendercomp.com で公開中 →

販売中

1,000以上のRemotionテンプレートを一括入手

買い切り(一括払い)・サブスクなし・生涯アップデート無料。TypeScript製。

料金プランを見る →