Remotion LabRemotion Lab
影片將影片作為 Three.js 材質貼圖

將影片作為 Three.js 材質貼圖

在 Remotion 中使用 @remotion/media 的 Video 元件搭配 onVideoFrame,將影片作為 Three.js 材質貼圖,實現畫面精確的 3D 影片效果。

將影片作為 Three.js 材質貼圖

在 Remotion 中,若要將影片嵌入為 Three.js 的材質貼圖(texture),有多種方式可選擇。本文說明各方式的優缺點,並提供官方推薦的完整實作範例。

推薦方式:使用 @remotion/media(v4.0.387+)

官方推薦使用 @remotion/media 套件中的 <Video> 元件,搭配 headless 模式與 onVideoFrame 屬性,在每次畫面更新時將影格繪製到 Three.js 材質上。

這個方式的優點:

  • 畫面精確:透過 WebCodecs 逐格提取,確保渲染結果完全準確
  • 效能最佳:重複使用同一個 CanvasTexture,不會每格建立新物件
  • 支援客戶端渲染:可在瀏覽器中直接渲染
  • 適用於並發渲染:正確處理 advance() 確保並發大於 1 時不會出現舊影格

安裝依賴

npm install @remotion/media @remotion/three @react-three/fiber three
# 或
yarn add @remotion/media @remotion/three @react-three/fiber three
# 或
pnpm add @remotion/media @remotion/three @react-three/fiber three

完整程式碼範例

// VideoTexture.tsx
import { useThree } from '@react-three/fiber';
import { Video } from '@remotion/media';
import { ThreeCanvas } from '@remotion/three';
import React, { useCallback, useState } from 'react';
import { useRemotionEnvironment, useVideoConfig } from 'remotion';
import { CanvasTexture } from 'three';
 
const videoSrc = 'https://remotion.media/video.mp4';
 
// 影片的原始解析度
const videoWidth = 1920;
const videoHeight = 1080;
const aspectRatio = videoWidth / videoHeight;
 
// Three.js 場景中平面的尺寸
const scale = 3;
const planeHeight = scale;
const planeWidth = aspectRatio * scale;
 
const Inner: React.FC = () => {
  // 初始化 OffscreenCanvas、2D Context 與 CanvasTexture(僅建立一次)
  const [canvasStuff] = useState(() => {
    const canvas = new OffscreenCanvas(videoWidth, videoHeight);
    const context = canvas.getContext('2d')!;
    const texture = new CanvasTexture(canvas);
    return { canvas, context, texture };
  });
 
  // Three.js 提供的畫面更新方法
  const { invalidate, advance } = useThree();
  const { isRendering } = useRemotionEnvironment();
 
  const onVideoFrame = useCallback(
    (frame: CanvasImageSource) => {
      // 將影格繪製到 OffscreenCanvas
      canvasStuff.context.drawImage(frame, 0, 0, videoWidth, videoHeight);
      // 標記材質需要更新
      canvasStuff.texture.needsUpdate = true;
 
      if (isRendering) {
        // 渲染模式:ThreeCanvas 的 ManualFrameRenderer 已在 useEffect 中呼叫
        // advance(),但影格擷取是非同步的(BroadcastChannel 往返),會在
        // useEffect 之後才完成。因此當 onVideoFrame 觸發時,場景已用舊材質
        // 渲染過一次。需要再次呼叫 advance() 以使用新材質重新渲染場景。
        advance(performance.now());
      } else {
        // 預覽模式:預設 frameloop='always' 會自動更新,僅在
        // frameloop='demand' 時需要呼叫 invalidate()。
        invalidate();
      }
    },
    [canvasStuff.context, canvasStuff.texture, invalidate, advance, isRendering],
  );
 
  return (
    <>
      {/*
        headless 模式:<Video> 不會渲染任何可見元素,
        僅用於驅動 onVideoFrame 回呼。
        這讓它可以安全地掛載在 <ThreeCanvas> 內部。
      */}
      <Video src={videoSrc} onVideoFrame={onVideoFrame} muted headless />
 
      {/* 顯示影片材質的平面幾何體 */}
      <mesh>
        <planeGeometry args={[planeWidth, planeHeight]} />
        <meshBasicMaterial
          color={0xffffff}
          toneMapped={false}
          map={canvasStuff.texture}
        />
      </mesh>
    </>
  );
};
 
export const RemotionMediaVideoTexture: React.FC = () => {
  const { width, height } = useVideoConfig();
 
  return (
    <ThreeCanvas
      style={{ backgroundColor: 'white' }}
      linear
      width={width}
      height={height}
    >
      <Inner />
    </ThreeCanvas>
  );
};

技術細節說明

headless 屬性

headless 傳給 <Video> 時,元件不會渲染任何可見的 DOM 元素,僅作為資料來源驅動 onVideoFrame 回呼。這使它可以安全地掛載在 <ThreeCanvas> 內部,而不會影響 Three.js 的場景結構。

advance() vs invalidate()

方法特性適用場景
advance(time)同步觸發一次渲染渲染模式(確保材質更新前不截圖)
invalidate()非同步排程渲染預覽模式(frameloop='demand' 時)

在渲染模式下必須使用 advance(),因為影格擷取是非同步操作,若只用 invalidate() 可能在材質更新前就截圖,導致畫面不正確——尤其在並發渲染(concurrency > 1)時更容易發生。

CanvasTexture 的效能優勢

// 好的做法:建立一次,重複更新
const [canvasStuff] = useState(() => {
  const canvas = new OffscreenCanvas(videoWidth, videoHeight);
  const context = canvas.getContext('2d')!;
  const texture = new CanvasTexture(canvas);
  return { canvas, context, texture };
});
 
// 每次影格更新時,只更新材質內容,不重新建立材質
canvasStuff.texture.needsUpdate = true;

這比每次影格都建立新材質高效得多,可避免記憶體洩漏與 GPU 資源過度消耗。

已棄用方式

使用 <OffthreadVideo>(已棄用)

此方式已棄用,建議改用 @remotion/media 的方式。

您可以使用 @remotion/three 中的 useOffthreadVideoTexture() hook:

import { useOffthreadVideoTexture, ThreeCanvas } from '@remotion/three';
import { useVideoConfig } from 'remotion';
 
const Scene: React.FC = () => {
  // 此 hook 會建立並自動更新材質,但每格都會建立新材質
  const videoTexture = useOffthreadVideoTexture({
    src: 'https://example.com/video.mp4',
  });
 
  if (!videoTexture) return null;
 
  return (
    <mesh>
      <planeGeometry args={[3, 2]} />
      <meshBasicMaterial map={videoTexture} />
    </mesh>
  );
};

已棄用的原因:

  • 必須先將整段影片下載到磁碟才能擷取影格
  • 不支援客戶端渲染
  • 每一格都會建立新的材質物件,效能較差

使用 <Html5Video>(已棄用)

此方式已棄用,建議改用 @remotion/media 的方式。

您可以使用 @remotion/three 中的 useVideoTexture() hook,但它繼承了 <Html5Video> 的所有缺點(畫面不保證精確、格式支援較少等),因此同樣不建議使用。

完整專案範例

相關連結