@remotion/noise で作る有機的なアニメーション:パーリンノイズ完全活用ガイド
@remotion/noise で作る有機的なアニメーション:パーリンノイズ完全活用ガイド
映像制作の現場で長年課題とされてきたのが「機械的に見えるアニメーション」との戦いです。Math.sin() で作った動きは整然としすぎていて、まるでメトロノームのように単調に繰り返す。タイトルカードが呼吸しているように見えたり、背景が生き物のようにゆったり変化したりする表現を実現するには、別のアプローチが必要です。
その答えがパーリンノイズです。1983年にケン・パーリンが映画「トロン」の制作中に考案したこのアルゴリズムは、「連続的でありながら非周期的」という自然界の動きの本質を数学的に再現します。@remotion/noise パッケージは、このアルゴリズムをRemotionコンポーネントの中で直接使えるように設計されています。
本記事では、パーリンノイズの仕組みから始まり、位置ジッター・パーティクルシステム・グラデーション背景・ウェーブテキストという4つの実装パターンを、実際に動くコードとともに詳しく解説します。
パーリンノイズとは何か、なぜ Math.sin() より優れているか
Math.sin(t) は 2π ごとに完全に同じ波形を繰り返します。これは制御がしやすい一方で、繰り返しが見えた瞬間に「コンピューター臭さ」が出てしまいます。一方、パーリンノイズは次の2つの性質を同時に満たします。
- 連続性:隣り合う入力値は必ず近い出力値を返す(急なジャンプがない)
- 非周期性:どれだけ長く再生しても、まったく同じパターンが繰り返されることはない
たとえば木の葉が風に揺れる動きを考えてみてください。葉は急に逆方向には動かない(連続性)が、かといって完全に同じ軌跡を辿ることもない(非周期性)。パーリンノイズはまさにこの性質を持っています。
加えて、@remotion/noise の関数は決定論的です。同じシード値と入力座標を渡せば、常に同じ値が返ります。Remotionのフレーム単位レンダリングモデルはこの決定性に依存しているため、再レンダリングしても同一のフレームが必ず出力されます。
インストールとAPI概要
npm i @remotion/noise
import { noise2D, noise3D, noise4D } from "@remotion/noise";
3つの関数はいずれも -1 から 1 の範囲の値を返します。
| 関数 | シグネチャ | 主な用途 |
|---|---|---|
noise2D | noise2D(seed, x) | 時間変化する1次元の動き(位置・スケール・回転) |
noise3D | noise3D(seed, x, y) | 2次元の空間フィールド(パーティクルの速度場) |
noise4D | noise4D(seed, x, y, z) | 時間経過する3次元フィールド |
seed パラメーターは独立した複数のノイズストリームを生成するためのキーです。X軸・Y軸・回転をそれぞれ別のシード(1、2、3など)で生成することで、互いに相関のない動きが実現します。
パターン1:位置ジッターでテキストを”呼吸”させる
最も手軽で効果的な使い方が、テキストや図形に微細なジッターを加えることです。タイトルカードが生きているように見える「フローティング」の表現はこの技法が基本になっています。
import { AbsoluteFill, useCurrentFrame } from "remotion";
import { noise2D } from "@remotion/noise";
export const FloatingTitle: React.FC = () => {
const frame = useCurrentFrame();
// 3つの独立したノイズストリーム
const x = noise2D(1, frame * 0.03) * 14; // 水平 ±14px
const y = noise2D(2, frame * 0.03) * 9; // 垂直 ±9px
const rotation = noise2D(3, frame * 0.02) * 3; // 回転 ±3deg
return (
<AbsoluteFill
style={{
justifyContent: "center",
alignItems: "center",
backgroundColor: "#0a0a0f",
}}
>
<h1
style={{
fontFamily: `"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif`,
fontSize: 88,
color: "#ffffff",
transform: `translate(${x}px, ${y}px) rotate(${rotation}deg)`,
margin: 0,
letterSpacing: "-1px",
}}
>
Organic Motion
</h1>
</AbsoluteFill>
);
};
速度係数(frame * 0.03)を小さくするほどゆっくりとした優雅な動き、大きくするほどエネルギッシュな動きになります。動きの幅(* 14 の部分)は振幅です。まず速度を決め、次に振幅を調整するのが快適なチューニング順序です。
パターン2:ノイズフィールドで動かすパーティクルシステム
ノイズフィールドは2次元空間の各点に速度ベクトルを割り当てます。パーティクルがそのベクトルに従って移動すると、煙・水流・群れの動きのような有機的な流れが生まれます。
import { AbsoluteFill, useCurrentFrame, useVideoConfig } from "remotion";
import { noise3D } from "@remotion/noise";
const PARTICLE_COUNT = 80;
// 再現性のある初期位置(シードを使った疑似ランダム)
const initialPositions = Array.from({ length: PARTICLE_COUNT }, (_, i) => ({
x: (i * 37.3) % 1920,
y: (i * 59.7) % 1080,
seed: i,
}));
export const NoiseParticles: React.FC = () => {
const frame = useCurrentFrame();
const { width, height } = useVideoConfig();
return (
<AbsoluteFill style={{ backgroundColor: "#050510" }}>
<svg width={width} height={height}>
{initialPositions.map((p, i) => {
let px = p.x;
let py = p.y;
// フレーム0から現在のフレームまで軌跡を積算
for (let f = 0; f < frame; f++) {
const ft = f * 0.007;
const vx = noise3D(p.seed, px / 280, ft) * 2.8;
const vy = noise3D(p.seed + 1000, py / 280, ft) * 2.8;
px = ((px + vx) % width + width) % width;
py = ((py + vy) % height + height) % height;
}
const opacity =
0.25 + (noise3D(p.seed + 2000, frame * 0.02, 0) + 1) * 0.35;
return (
<circle
key={i}
cx={px}
cy={py}
r={2.5}
fill="#7eb8f7"
opacity={opacity}
/>
);
})}
</svg>
</AbsoluteFill>
);
};
実装のポイントは noise3D の第2・第3引数を空間座標として使っていることです。パーティクルの位置が変わるたびにサンプリング位置も変わるため、流れが自然な曲線を描きます。
パフォーマンスに関しては、内側のループがフレーム数に比例して重くなるため、長尺コンポジションでは calculateMetadata 内で軌跡を事前計算してpropsとして渡す方法が有効です。
パターン3:ノイズ駆動のグラデーション背景
HSLのパラメーター(色相・彩度・明度)をノイズで動かすことで、常に少しずつ変化し続ける背景が作れます。気象写真のような光の変化や、深夜のLEDライトが揺らぐような雰囲気を表現できます。
import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate } from "remotion";
import { noise2D } from "@remotion/noise";
export const FlowingGradient: React.FC = () => {
const frame = useCurrentFrame();
const t = frame * 0.012;
const hue1 = interpolate(noise2D(10, t), [-1, 1], [200, 280]);
const hue2 = interpolate(noise2D(11, t), [-1, 1], [280, 360]);
const hue3 = interpolate(noise2D(12, t), [-1, 1], [160, 220]);
const angle = interpolate(noise2D(13, t), [-1, 1], [120, 240]);
const stop1 = interpolate(noise2D(14, t), [-1, 1], [0, 30]);
const stop2 = interpolate(noise2D(15, t), [-1, 1], [40, 70]);
const gradient = `linear-gradient(
${angle}deg,
hsl(${hue1}, 70%, 28%) ${stop1}%,
hsl(${hue2}, 65%, 18%) ${stop2}%,
hsl(${hue3}, 75%, 14%) 100%
)`;
return (
<AbsoluteFill style={{ background: gradient }}>
{/* コンテンツレイヤーをここに */}
</AbsoluteFill>
);
};
interpolate() でノイズの出力範囲(-1〜1)をHSLの意味のある範囲(色相なら200〜280など)にマッピングしているのがこのパターンの核心です。ノイズ自体は連続的に変化するため、interpolate 後の色も滑らかに遷移します。
パターン4:文字ごとのノイズで作るウェーブテキスト
テキストを1文字ずつ分解し、それぞれに独立したノイズ変換を適用することでウェーブエフェクトが生まれます。映像制作の現場で人気の高い表現です。
import { AbsoluteFill, useCurrentFrame } from "remotion";
import { noise2D } from "@remotion/noise";
const LABEL = "ORGANIC";
export const WavyText: React.FC = () => {
const frame = useCurrentFrame();
return (
<AbsoluteFill
style={{
justifyContent: "center",
alignItems: "center",
backgroundColor: "#111",
}}
>
<div style={{ display: "flex", gap: 6 }}>
{LABEL.split("").map((char, i) => {
// 文字インデックスでノイズ位相をずらす
const t = frame * 0.04 + i * 0.4;
const translateY = noise2D(i * 19 + 1, t) * 30;
const scale = 1 + noise2D(i * 19 + 2, t) * 0.18;
const rotation = noise2D(i * 19 + 3, t) * 8;
return (
<span
key={i}
style={{
fontFamily: `-apple-system, "Segoe UI", Roboto, sans-serif`,
fontSize: 96,
fontWeight: 700,
color: "#ffffff",
display: "inline-block",
transform: `translateY(${translateY}px) scale(${scale}) rotate(${rotation}deg)`,
transformOrigin: "bottom center",
}}
>
{char}
</span>
);
})}
</div>
</AbsoluteFill>
);
};
i * 0.4 という位相オフセットが重要です。これがないとすべての文字が同じノイズの位置でサンプリングされ、同期して動いてしまいます。異なる seed 値(i * 19 + 1 など)と位相オフセットの組み合わせが自然なバラつきを生みます。
ノイズパラメーターのチューニング指針
映像制作者が実際に使う感覚値をまとめました。
| パラメーター | 効果 | 推奨範囲 |
|---|---|---|
速度係数(frame * X) | 変化の速さ | 0.01(ゆったり)〜0.12(速い) |
出力スケール(* Y) | 動きの幅 | 位置±5〜30px、回転±2〜15deg |
| シード値 | 複数ストリームの独立性 | 必ず異なる整数を使う |
| 空間周波数(座標を÷する) | 滑らかさ | ÷200(滑らか)〜÷30(荒い) |
デバッグのコツとして「いったん Math.sin に置き換える」方法があります。Math.sin バージョンで速度と振幅が適切に見えれば、noise2D に戻すだけで即座に有機的な動きに変わります。
ノイズ・spring・interpolate:使い分けの判断軸
3つのアニメーション手法は補完的な関係にあります。
spring():画面への入場・退場など「目標値に向かって収束する」トリガー型トランジションに最適interpolate():フレーム範囲が決まっていて2点間を移動する場合に最適noise2D():繰り返さない連続的な動きが必要な場合に最適。特定の終端値に到達させたい場合には不向き
組み合わせも効果的です。spring で要素を画面内に引き込み、その後 noise2D によるジッターを上乗せする実装はよく使われるパターンです。
SVGパスをノイズで変形させる有機的ブロブ
import { AbsoluteFill, useCurrentFrame } from "remotion";
import { noise2D } from "@remotion/noise";
export const OrganicBlob: React.FC = () => {
const frame = useCurrentFrame();
const cx = 960;
const cy = 540;
const baseRadius = 200;
const POINTS = 12;
const points = Array.from({ length: POINTS }, (_, i) => {
const angle = (i / POINTS) * Math.PI * 2;
const t = frame * 0.025;
const r = baseRadius + noise2D(i * 100 + 1, t) * 65;
return {
x: cx + Math.cos(angle) * r,
y: cy + Math.sin(angle) * r,
};
});
// 滑らかな閉じたパスを生成
const d =
points
.map((p, i) => {
const next = points[(i + 1) % POINTS];
const mx = (p.x + next.x) / 2;
const my = (p.y + next.y) / 2;
return i === 0 ? `M ${mx},${my}` : `Q ${p.x},${p.y} ${mx},${my}`;
})
.join(" ") + " Z";
return (
<AbsoluteFill style={{ backgroundColor: "#0d0d18" }}>
<svg width={1920} height={1080}>
<defs>
<radialGradient id="blobGrad">
<stop offset="0%" stopColor="#7eb8f7" />
<stop offset="100%" stopColor="#3a6fd8" stopOpacity={0} />
</radialGradient>
</defs>
<path d={d} fill="url(#blobGrad)" opacity={0.55} />
</svg>
</AbsoluteFill>
);
};
SVGのQuadratic Bezierで各頂点の中点を通ることで、鋭角なポリゴンではなく滑らかな有機的フォルムになります。
よく使うノイズレシピ集
// ふわふわ浮遊(位置)
const y = noise2D(1, frame * 0.025) * 15;
// 呼吸するスケール
const scale = 1 + noise2D(2, frame * 0.02) * 0.08;
// 微細な回転ゆらぎ
const rot = noise2D(3, frame * 0.018) * 4;
// 不透明度の脈動
const opacity = interpolate(noise2D(4, frame * 0.03), [-1, 1], [0.6, 1]);
// 色温度のシフト(色相を動かす)
const hue = interpolate(noise2D(5, frame * 0.01), [-1, 1], [210, 250]);
RenderCompのテンプレートで即戦力の有機的アニメーションを
ノイズアニメーションのパラメーターを1から最適化するのは奥が深く、経験が必要です。RenderComp(rendercomp.com)では、フローティングパーティクル・ノイズ駆動のロワーサード・アンビエントグラデーション背景など、すでに調整済みのRemotionテンプレートを多数用意しています。必要なパラメーターを調整するだけで、すぐに本番品質の有機的アニメーションをプロジェクトに組み込めます。
よくある質問
Q1:@remotion/noise はRemotionプレーヤー(ブラウザ)でも動作しますか?
はい。@remotion/noise はネイティブ依存のない純粋なJavaScriptライブラリです。ブラウザプレビュー・Remotion Studio・サーバーサイドレンダリングのすべてで同一の動作をします。
Q2:noise2D・noise3D・noise4D の違いを教えてください。
数字はシード以外に受け取る座標引数の数です。noise2D(seed, x) は1つの座標(主に時間)を受け取り1次元のストリームを返します。noise3D(seed, x, y) は2つの空間座標を受け取り2次元速度フィールドに使います。noise4D はさらに1次元追加されており、時間変化する3Dフィールドに向いています。
Q3:ノイズが決定論的(再現性あり)であることの意味は? 同じシード・座標入力を与えると常に同じ値が返ります。Remotionのレンダリングはフレーム単位の再現性を前提としているため、再レンダリングしても同一フレームが出力されます。
Q4:動きがランダムすぎて落ち着かない場合はどうすればよいですか?
速度係数を小さくし(例:frame * 0.1 → frame * 0.025)、出力スケールを下げます。ノイズ空間をゆっくり移動するほど、意図的に見える優雅な動きになります。
Q5:ノイズをspring()と組み合わせることはできますか?
できます。よくある実装パターンは spring() で要素を画面内に引き込み、定着後に noise2D ジッターを上乗せするものです。加算で重ねます:translateY = springValue + noise2D(1, frame * 0.03) * 10。
Q6:パーティクルシステムがStudioで重い場合の対処法は?
内側の軌跡積算ループがフレーム×パーティクル数のO(N²)になるためです。対策は2つあります。(1)PARTICLE_COUNT を減らす。(2)軌跡を calculateMetadata 内で事前計算してpropsとして渡す方法です。
Q7:ノイズをシームレスにループさせることはできますか?
パーリンノイズは本来ループしません。ループが必要な場合は円周上をサンプリングする手法を使います:noise3D(seed, Math.cos(t) * r, Math.sin(t) * r) で t = (frame / totalFrames) * 2 * Math.PI。2π の整数倍で同じ点に戻るため、シームレスなループになります。