將影片作為 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> 的所有缺點(畫面不保證精確、格式支援較少等),因此同樣不建議使用。
完整專案範例
- React Three Fiber 模板原始碼 — 包含完整的影片材質實作
- Remotion testbed — 包含上述範例的測試案例
相關連結
@remotion/three— Three.js 整合文件- React Three Fiber 模板 — 起始模板
- 影片元件比較 — OffthreadVideo vs Html5Video vs @remotion/media
<Video>headless 屬性 — API 文件