R RenderComp
remotion ブロードキャスト ロワーサード ニュースグラフィック 透過書き出し

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 ResolveProRes 4444 .mov
Adobe Premiere ProProRes 4444 または PNGシーケンス
Final Cut ProProRes 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 でのフォントインストールが必要になる場合があります。

販売中

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

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

料金プランを見る →