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が低いと弾んでコミカルに、高いと落ち着いて堂々とした印象になります。mass・stiffness・damping のチューニングは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-shadowやblurは毎フレーム高コストなラスタライズを強制する。フィルタはキャラクターグループ全体に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-origin は transform-box の挙動に依存し、歴史的に実装間の一貫性がありません。属性形式の rotate(angle cx cy) はピボットをviewBox座標としてtransform自体に焼き込むため、RemotionのChromiumレンダラーを含め、どこでも同一に動作します。
Q: リグの絵柄をもっとリッチにしたい場合は?
リグの構造(グループ階層とピボット)はそのままに、各 <g> の中身を差し替えるだけです。矩形と円をIllustrator/Figmaから書き出した本格的なパスに置き換えても、ポーズを駆動するコードは1行も変わりません。「構造とスタイルの分離」がSVGリグの強みです。
キャラクターモーションスターターパックで最速スタート
ここまでの内容はすべて、ゼロから1〜2日で構築できます。ただし面倒な部分——ピボットの位置決め、スプリングのチューニング、きれいにループする位相の数式、小さいサイズでも判別できる口形状——は、検証済みの出発点から始めるのが最も効率的です。
RenderComp のライブラリには、構築済みSVGリグ、すぐ使える歩行・待機・手振り・トーキングのサイクル、色・タイミング・アニメーション選択の型付きpropsを備えたキャラクターモーションテンプレートが揃っています。すべて編集可能なTypeScriptソースとして提供されるため、白紙のキャンバスで肩のピボットをデバッグする代わりに、動いているキャラクターから自分のマスコットへ作り替えるところから始められます。
キャラクター・モーション系テンプレートは rendercomp.com で公開中 →