RemotionでプロのブロードキャストCGを作る:ロワーサード・選挙速報・ニュースティッカー完全ガイド
RemotionでプロのブロードキャストCGを作る:ロワーサード・選挙速報・ニュースティッカー完全ガイド
ニュース番組の画面を見ていると、常に何らかのグラフィックが動いています。出演者の名前と肩書を表示するロワーサード、選挙夜の得票率棒グラフ、速報テロップ、スポーツのスコアバグ、画面下部を流れるティッカー。こうした要素はすべて「データと時間」によって駆動される精密なアニメーションです。
Remotionは本質的にReactベースのビデオレンダリングエンジンです。データをpropsとして受け取り、フレーム番号を基準にアニメーションを計算し、決定論的に映像を出力する。この特性は、放送CGグラフィックの要件とほぼ完全に一致します。本記事では、実際の放送現場で使われるグラフィック要素をRemotionで実装する方法を、実働コードとともに詳しく解説します。
Remotionと従来の放送CGシステムの違い
VizrtやChyron、Ross Xpressionといったプロ用ブロードキャストCGシステムは、リアルタイムGPUレンダリングと放送機器との直接連携に優れています。対してRemotionが提供するのは異なるトレードオフです。
Remotionはリアルタイム出力ではなく、最高品質の事前レンダリング映像を生成します。その代わりに:
- React・TypeScript・CSSという標準的なWebスキルで開発できる
- バージョン管理が可能(Gitで差分管理・チームでの協業)
- データバインドが容易(JSONフィードからグラフィックを自動生成できる)
- 透過PNGシーケンスまたはProRes 4444の透過動画で書き出し、任意のNLEと組み合わせられる
ポッドキャスト番組・Webニュース・企業の動画制作・ドキュメンタリー制作など、同日締め切りを持ちつつもリアルタイムシステムが不要なケースでRemotionは理想的な選択肢になります。
ロワーサード:ワイプインする名前・肩書バー
放送グラフィックの代表格です。標準的な実装は2フェーズのアニメーションで構成されます。まずアクセントバーが水平にワイプイン、次にテキストがフェードアップします。
import {
AbsoluteFill,
useCurrentFrame,
interpolate,
spring,
} from "remotion";
interface LowerThirdProps {
name: string;
title: string;
accentColor?: string;
}
export const LowerThird: React.FC<LowerThirdProps> = ({
name,
title,
accentColor = "#e63946",
}) => {
const frame = useCurrentFrame();
// フェーズ1:バーのワイプイン(フレーム0〜18)
const barWidth = spring({
frame,
fps: 30,
config: { damping: 18, stiffness: 120, mass: 1 },
durationInFrames: 18,
});
// フェーズ2:テキストフェードアップ(フレーム12から開始、バーと重なる)
const textOpacity = interpolate(frame, [12, 28], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const textY = interpolate(frame, [12, 28], [14, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<AbsoluteFill>
<div
style={{
position: "absolute",
bottom: "12%",
left: "5%",
overflow: "hidden",
}}
>
{/* アクセントバー */}
<div
style={{
width: interpolate(barWidth, [0, 1], [0, 420]),
height: 4,
backgroundColor: accentColor,
marginBottom: 10,
}}
/>
{/* 名前・肩書ブロック */}
<div
style={{
opacity: textOpacity,
transform: `translateY(${textY}px)`,
backgroundColor: "rgba(10,10,20,0.88)",
padding: "12px 20px",
borderLeft: `4px solid ${accentColor}`,
backdropFilter: "blur(4px)",
}}
>
<div
style={{
fontFamily: `"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif`,
fontSize: 30,
fontWeight: 700,
color: "#ffffff",
lineHeight: 1.25,
}}
>
{name}
</div>
<div
style={{
fontFamily: `"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif`,
fontSize: 18,
fontWeight: 400,
color: accentColor,
marginTop: 4,
letterSpacing: "0.05em",
}}
>
{title}
</div>
</div>
</div>
</AbsoluteFill>
);
};
バー幅に spring() を使うのは、物理的な減速感を加えるためです。フレーム12から始まるテキストフェードがバーの完了(フレーム18)より先に始まることで、放送モーションデザインに特有のダブルビートのリズムが生まれます。
選挙速報グラフィック:アニメーション付き棒グラフ
選挙夜の中心グラフィックです。各バーが目標値まで伸び、同時にパーセンテージもカウントアップします。
import { AbsoluteFill, useCurrentFrame, spring, interpolate } from "remotion";
interface Candidate {
name: string;
votes: number;
color: string;
party: string;
}
interface ElectionResultsProps {
candidates: Candidate[];
totalVotes: number;
title: string;
}
export const ElectionResults: React.FC<ElectionResultsProps> = ({
candidates,
totalVotes,
title,
}) => {
const frame = useCurrentFrame();
return (
<AbsoluteFill
style={{
backgroundColor: "#080814",
justifyContent: "center",
alignItems: "center",
padding: "0 120px",
}}
>
<div style={{ width: "100%" }}>
<div
style={{
fontFamily: `"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif`,
fontSize: 22,
color: "#aaaacc",
letterSpacing: "0.12em",
marginBottom: 36,
textTransform: "uppercase",
}}
>
{title}
</div>
{candidates.map((candidate, i) => {
const targetPct = (candidate.votes / totalVotes) * 100;
const delay = i * 8;
const progress = spring({
frame: Math.max(0, frame - delay),
fps: 30,
config: { damping: 16, stiffness: 80, mass: 1.2 },
});
const pct = interpolate(progress, [0, 1], [0, targetPct]);
const displayPct = Math.round(pct);
return (
<div key={i} style={{ marginBottom: 32 }}>
<div
style={{
display: "flex",
justifyContent: "space-between",
fontFamily: `"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif`,
color: "#fff",
fontSize: 26,
fontWeight: 600,
marginBottom: 10,
}}
>
<span>
<span style={{ color: candidate.color, marginRight: 8 }}>
{candidate.party}
</span>
{candidate.name}
</span>
<span style={{ color: candidate.color }}>{displayPct}%</span>
</div>
<div
style={{
width: "100%",
height: 20,
backgroundColor: "rgba(255,255,255,0.08)",
borderRadius: 4,
overflow: "hidden",
}}
>
<div
style={{
width: `${pct}%`,
height: "100%",
backgroundColor: candidate.color,
borderRadius: 4,
}}
/>
</div>
</div>
);
})}
</div>
</AbsoluteFill>
);
};
各候補のバーは i * 8 フレームずつ遅延してアニメーション開始します。これにより次々と展開していくような視覚的リズムが生まれます。
速報バナー:注目を集める高コントラスト演出
速報バナーは放送グラフィックの中で最も緊急性の高い要素です。高コントラスト・太字・素早い登場アニメーションが必要です。
import { AbsoluteFill, useCurrentFrame, spring, interpolate } from "remotion";
interface BreakingNewsProps {
headline: string;
subline?: string;
}
export const BreakingNewsBanner: React.FC<BreakingNewsProps> = ({
headline,
subline,
}) => {
const frame = useCurrentFrame();
const slideIn = spring({
frame,
fps: 30,
config: { damping: 22, stiffness: 200, mass: 0.8 },
});
const translateY = interpolate(slideIn, [0, 1], [130, 0]);
const opacity = interpolate(slideIn, [0, 1], [0, 1]);
// 「速報」ラベルの点滅
const pulse = Math.sin(frame * 0.18) * 0.5 + 0.5;
const labelOpacity = interpolate(pulse, [0, 1], [0.65, 1]);
return (
<AbsoluteFill>
<div
style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
transform: `translateY(${translateY}px)`,
opacity,
}}
>
<div style={{ height: 6, backgroundColor: "#e63946" }} />
<div
style={{
backgroundColor: "rgba(8,8,20,0.96)",
padding: "22px 48px",
display: "flex",
alignItems: "center",
gap: 28,
}}
>
<div
style={{
backgroundColor: "#e63946",
padding: "6px 16px",
opacity: labelOpacity,
flexShrink: 0,
}}
>
<span
style={{
fontFamily: `"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif`,
fontSize: 18,
fontWeight: 900,
color: "#fff",
letterSpacing: "0.15em",
}}
>
速 報
</span>
</div>
<div>
<div
style={{
fontFamily: `"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif`,
fontSize: 34,
fontWeight: 700,
color: "#ffffff",
lineHeight: 1.2,
}}
>
{headline}
</div>
{subline && (
<div
style={{
fontFamily: `"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif`,
fontSize: 20,
color: "#aaaacc",
marginTop: 5,
}}
>
{subline}
</div>
)}
</div>
</div>
</div>
</AbsoluteFill>
);
};
スポーツスコアオーバーレイ:常駐するスコアバグ
スコアバグは試合を通じて常に画面に表示される要素です。背景コンテンツを問わず可読性が必要なため、高コントラストのデザインが求められます。
import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate } from "remotion";
interface ScoreOverlayProps {
homeTeam: string;
awayTeam: string;
homeScore: number;
awayScore: number;
period: string;
clock: string;
}
export const ScoreOverlay: React.FC<ScoreOverlayProps> = ({
homeTeam,
awayTeam,
homeScore,
awayScore,
period,
clock,
}) => {
const { width, height } = useVideoConfig();
const frame = useCurrentFrame();
const opacity = interpolate(frame, [0, 12], [0, 1], {
extrapolateRight: "clamp",
});
return (
<AbsoluteFill>
<div
style={{
position: "absolute",
top: height * 0.04,
left: width * 0.03,
opacity,
}}
>
<div
style={{
backgroundColor: "rgba(0,0,0,0.85)",
borderRadius: 6,
overflow: "hidden",
minWidth: 220,
}}
>
<div
style={{
backgroundColor: "#e63946",
padding: "4px 12px",
textAlign: "center",
fontFamily: `-apple-system, "Segoe UI", Roboto, sans-serif`,
fontSize: 13,
fontWeight: 700,
color: "#fff",
letterSpacing: "0.1em",
}}
>
{period} · {clock}
</div>
{[
{ team: homeTeam, score: homeScore },
{ team: awayTeam, score: awayScore },
].map((row, i) => (
<div
key={i}
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "10px 14px",
borderTop: i > 0 ? "1px solid rgba(255,255,255,0.1)" : "none",
}}
>
<span
style={{
fontFamily: `-apple-system, "Segoe UI", Roboto, sans-serif`,
fontSize: 19,
fontWeight: 600,
color: "#fff",
}}
>
{row.team}
</span>
<span
style={{
fontFamily: `-apple-system, "Segoe UI", Roboto, sans-serif`,
fontSize: 24,
fontWeight: 700,
color: "#fff",
minWidth: 32,
textAlign: "right",
}}
>
{row.score}
</span>
</div>
))}
</div>
</div>
</AbsoluteFill>
);
};
ニュースティッカー:水平スクロールテロップ
useCurrentFrame() でスクロール位置を駆動するシンプルな実装です。
import { AbsoluteFill, useCurrentFrame, useVideoConfig } from "remotion";
interface TickerProps {
items: string[];
speed?: number;
}
export const NewsTicker: React.FC<TickerProps> = ({ items, speed = 3 }) => {
const frame = useCurrentFrame();
const { width, height } = useVideoConfig();
const tickerText =
items.join(" / ") + " / " + items.join(" / ");
const offset = -(frame * speed);
return (
<AbsoluteFill>
<div
style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
backgroundColor: "#e63946",
height: height * 0.055,
overflow: "hidden",
display: "flex",
alignItems: "center",
}}
>
{/* 「速報」ラベル */}
<div
style={{
backgroundColor: "#0a0a14",
height: "100%",
padding: "0 20px",
display: "flex",
alignItems: "center",
flexShrink: 0,
zIndex: 2,
}}
>
<span
style={{
fontFamily: `"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif`,
fontSize: height * 0.025,
fontWeight: 700,
color: "#e63946",
letterSpacing: "0.1em",
}}
>
速報
</span>
</div>
{/* スクロールするテキスト */}
<div
style={{
transform: `translateX(${offset}px)`,
whiteSpace: "nowrap",
fontFamily: `"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif`,
fontSize: height * 0.026,
color: "#fff",
fontWeight: 500,
paddingLeft: 20,
}}
>
{tickerText}
</div>
</div>
</AbsoluteFill>
);
};
シームレスにループさせるには、コンポジションの durationInFrames をテキスト幅の半分をスクロールし終えるフレーム数に設定します(テキスト文字列は前半と後半が同一内容のため)。
透過背景でのProRes書き出し(NLEへの組み込み)
放送ワークフローで最も強力なのが、Remotionのアルファチャンネル付き書き出しです。生放送素材の上にRemotionグラフィックをコンポジットできます。
# PNGシーケンス(アルファチャンネルあり)
npx remotion render LowerThird --codec=png --image-format=png ./output/frames/
# ProRes 4444(NLEへの直接取り込み)
npx remotion render LowerThird --codec=prores --prores-profile=4444 ./output/lower-third.mov
透過書き出し時は、コンポジションのルート <AbsoluteFill> に backgroundColor を設定しないことが必須条件です。
// 透過書き出し用:背景なし
export const LowerThirdTransparent: React.FC<LowerThirdProps> = (props) => (
<AbsoluteFill>
<LowerThirdContent {...props} />
</AbsoluteFill>
);
NLE別の推奨フォーマットは次の通りです。
| NLE | 推奨フォーマット |
|---|---|
| DaVinci Resolve | ProRes 4444 .mov |
| Adobe Premiere Pro | ProRes 4444 または PNGシーケンス |
| Final Cut Pro | ProRes 4444 .mov |
| Web配信のみ | H.264(マットアプローチ) |
ProRes 4444 は12ビットフルカラー+フルアルファをサポートする放送業界標準フォーマットです。主要NLEすべてでネイティブサポートされています。
JSONフィードからのデータドリブンなグラフィックパッケージ生成
Remotionの真の強みは、データとグラフィックの自動バインドです。出演者リストのJSONから全ロワーサードを自動生成する例を示します。
// Root.tsx — データエントリーごとにコンポジションを生成
import { Composition } from "remotion";
import guestsData from "./data/guests.json";
import { LowerThird } from "./LowerThird";
export const RemotionRoot: React.FC = () => (
<>
{guestsData.map((guest) => (
<Composition
key={guest.id}
id={`LowerThird-${guest.id}`}
component={LowerThird}
width={1920}
height={1080}
fps={30}
durationInFrames={150}
defaultProps={{
name: guest.name,
title: guest.title,
accentColor: guest.accentColor ?? "#e63946",
}}
/>
))}
</>
);
出演者リストのCSVをJSONに変換するだけで全員分のロワーサードが自動生成されます。Remotion Lambdaを使えば並列クラウドレンダリングも可能で、放送当日のタイトな締め切りにも対応できます。
制作ワークフローの推奨事項
関心の分離: データはJSONファイルで管理し、コンポーネントのコードとは分離します。制作スタッフがTypeScriptに触れることなくコンテンツを更新できます。
書き出し解像度でテスト: テキストサイズは必ず最終書き出し解像度で確認します。28pxのテキストは1080pでは読みやすくても720pでは読めません。フォントサイズは height * 0.025 のように比率で指定することを推奨します。
データのバージョン管理: 放送日ごとに data-ep012.json のようにバージョン番号をつけて管理します。これにより過去放送のグラフィック再現も容易になります。
RenderCompの放送テンプレートで制作を加速する
放送品質のグラフィックパッケージをゼロから構築するのは、数日単位の作業になることもあります。RenderComp(rendercomp.com)では、ロワーサードパッケージ・選挙グラフィック・スポーツスコアオーバーレイ・ティッカーシステムをRemotionテンプレートとして提供しています。透過書き出し対応・データバインド済みのテンプレートにブランドカラーとフォントを適用するだけで、数時間で本番品質のグラフィックパッケージが完成します。
よくある質問
Q1:Remotionで本当に透過動画を書き出せますか?
はい。--codec=prores --prores-profile=4444 でProRes 4444の .mov ファイル(フルアルファチャンネルあり)が生成されます。--codec=png でPNGシーケンスも書き出せます。どちらもDaVinci Resolve・Premiere Pro・Final Cut Proで正しく取り込めます。コンポジションのルートに不透明な背景色がないことを確認してください。
Q2:プログラマー不要でロワーサードのデータを更新するには?
出演者名・肩書をJSONファイルで管理し、それを defaultProps として参照します。レンダリング時に --props='{"name":"山田太郎","title":"解説委員"}' を渡せばコードを変更せずにデータを更新できます。CSVからJSONへの変換スクリプトを組み合わせれば、スプレッドシートベースの運用が可能です。
Q3:放送自動化パイプラインでRemotionを使えますか? はい。Remotion CLIは完全なスクリプタビリティを持ち、CI/CDツール・cronジョブ・Node.jsベースの自動化との統合が可能です。Remotion Lambdaはクラウド並列レンダリングをサポートしており、毎日のニュースパッケージのような締め切りの厳しいワークフローに適しています。
Q4:放送グラフィックに適したフレームレートは?
PAL/欧州放送は25fps、NTSC/日本・北米放送は29.97fps(Remotionでは30fpsに設定し、NLE側で29.97タイムラインに合わせる)、スポーツハイライトは50/60fps。 <Composition> の fps をNLEのタイムラインに合わせることが重要です。
Q5:ティッカーをシームレスにループさせるには? テキスト文字列を「前半 + セパレーター + 前半と同一の後半」で構成し、コンポジションの長さをテキストが前半分だけスクロールするフレーム数に設定します。後半が前半と同じなのでループ点が目立ちません。
Q6:RemotionはライブブロードキャストのCGシステムとして使えますか? Remotionは事前レンダリングエンジンです。リアルタイムCGシステムではありません。ライブ放送への組み込みは「放送直前にデータを取得してレンダリングし、完成した動画ファイルをスイッチャーやNLEから再生する」方式で対応します。本当にフレーム精度のリアルタイムCGが必要な場合は、専用の放送機器が必要です。
Q7:オンエアブランドフォントを正確に再現するには?
プロプライエタリフォントを使う場合は、レンダリングサーバーにシステムフォントとしてインストールします。RemotionはレンダリングHTMLとCSSを通じてOSのフォントシステムを使用するため、フォントがシステムにインストールされていれば正確に再現されます。Linux CI/CDサーバーでは apt-get でのフォントインストールが必要になる場合があります。