本文 AI 產出,尚未審核
Three.js 教學:多 Renderer 或分割畫面
簡介
在 3D 網頁應用中,常見的需求是 同時顯示多個視角、分割畫面 或 在不同的 DOM 容器裡渲染獨立的場景。例如:
- 車輛模擬器需要同時呈現車外視角與車內儀表板。
- 監控系統要把多個攝影機畫面併排顯示。
- 遊戲開發者想實作「分屏」對戰模式。
Three.js 天生支援多個 WebGLRenderer、多個 Scene、多個 Camera,只要妥善管理更新與渲染順序,就能輕鬆完成這類需求。本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,帶你一步步掌握 多 Renderer / 分割畫面 的技巧,適合從入門到中階的開發者使用。
核心概念
1. 為什麼要使用多個 Renderer?
- 獨立的渲染設定:每個 Renderer 可以擁有自己的尺寸、像素比、抗鋸齒等參數,互不干擾。
- 不同的輸出目標:Renderer 可以渲染到不同的
<canvas>,甚至渲染到WebGLRenderTarget(離屏緩衝)再做後製。 - 分割畫面 (Split‑Screen):只需要在同一個畫布上調整視口 (viewport) 即可,同時渲染多個相機畫面。
2. 基本流程
- 建立 多個 Canvas(或同一 Canvas 但使用不同 viewport)。
- 為每個 Canvas 建立獨立的 Renderer、Scene、Camera。
- 在動畫迴圈 (
requestAnimationFrame) 中,依序 更新物件、設定視口、呼叫renderer.render(scene, camera)。 - 如有需要,使用
renderer.setScissor限制渲染區域,以避免不同視口之間相互覆寫。
3. Viewport 與 Scissor 的差異
- Viewport:決定渲染結果在畫布中的 位置與大小,但渲染時仍會覆寫整個畫布。
- Scissor:除了設定位置與大小外,還會 裁切 渲染區域,只有在 scissor 範圍內的像素會被寫入畫布。配合
renderer.setScissorTest(true)使用,可避免畫面交叉。
程式碼範例
以下範例示範 三種常見的多 Renderer 實作,從最簡單的兩個 canvas,到同一 canvas 的分割畫面,最後展示離屏渲染再合併的技巧。每段程式碼皆附有說明註解。
範例 1️⃣:兩個獨立 Canvas 各渲染一個場景
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<title>Two Renderers Example</title>
<style>
body { margin:0; display:flex; }
canvas { flex:1; }
</style>
</head>
<body>
<canvas id="canvasA"></canvas>
<canvas id="canvasB"></canvas>
<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.166/build/three.module.js';
// ---------- Renderer A ----------
const canvasA = document.getElementById('canvasA');
const rendererA = new THREE.WebGLRenderer({ canvas: canvasA, antialias: true });
rendererA.setSize(window.innerWidth / 2, window.innerHeight);
const sceneA = new THREE.Scene();
const cameraA = new THREE.PerspectiveCamera(45, canvasA.clientWidth / canvasA.clientHeight, 0.1, 100);
cameraA.position.set(0, 2, 5);
sceneA.add(new THREE.AxesHelper(2));
const boxA = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshStandardMaterial({ color: '#ff5555' })
);
sceneA.add(boxA);
const lightA = new THREE.DirectionalLight('#ffffff', 1);
lightA.position.set(5, 5, 5);
sceneA.add(lightA);
// ---------- Renderer B ----------
const canvasB = document.getElementById('canvasB');
const rendererB = new THREE.WebGLRenderer({ canvas: canvasB, antialias: true });
rendererB.setSize(window.innerWidth / 2, window.innerHeight);
const sceneB = new THREE.Scene();
const cameraB = new THREE.PerspectiveCamera(45, canvasB.clientWidth / canvasB.clientHeight, 0.1, 100);
cameraB.position.set(0, 2, 5);
sceneB.add(new THREE.AxesHelper(2));
const sphereB = new THREE.Mesh(
new THREE.SphereGeometry(0.7, 32, 32),
new THREE.MeshStandardMaterial({ color: '#55aaff' })
);
sceneB.add(sphereB);
const lightB = new THREE.PointLight('#ffffff', 1);
lightB.position.set(-5, 5, 5);
sceneB.add(lightB);
// ---------- Animation Loop ----------
function animate() {
requestAnimationFrame(animate);
boxA.rotation.y += 0.01;
sphereB.rotation.x += 0.02;
rendererA.render(sceneA, cameraA);
rendererB.render(sceneB, cameraB);
}
animate();
// Resize handling
window.addEventListener('resize', () => {
const w = window.innerWidth / 2;
const h = window.innerHeight;
rendererA.setSize(w, h);
rendererB.setSize(w, h);
cameraA.aspect = w / h; cameraA.updateProjectionMatrix();
cameraB.aspect = w / h; cameraB.updateProjectionMatrix();
});
</script>
</body>
</html>
重點:每個 Renderer 都有自己的
<canvas>,所以可以各自設定不同的解析度、抗鋸齒或後處理效果。
範例 2️⃣:單一 Canvas 分割成左右兩個視口(Split‑Screen)
import * as THREE from 'three';
// 建立單一 renderer,綁定到 body
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 共用同一個 scene(也可以分開)
const scene = new THREE.Scene();
scene.add(new THREE.AxesHelper(3));
// 左側相機(俯視)
const camTop = new THREE.OrthographicCamera(-5, 5, 5, -5, 0.1, 100);
camTop.position.set(0, 10, 0);
camTop.lookAt(0, 0, 0);
// 右側相機(透視)
const camPersp = new THREE.PerspectiveCamera(60, 1, 0.1, 100);
camPersp.position.set(5, 3, 5);
camPersp.lookAt(0, 0, 0);
// 加入兩個測試模型
const cube = new THREE.Mesh(
new THREE.BoxGeometry(2, 2, 2),
new THREE.MeshStandardMaterial({ color: '#ff8800' })
);
scene.add(cube);
const sphere = new THREE.Mesh(
new THREE.SphereGeometry(1, 32, 32),
new THREE.MeshStandardMaterial({ color: '#0088ff' })
);
sphere.position.set(-3, 1, 0);
scene.add(sphere);
// 簡易光源
const light = new THREE.DirectionalLight('#ffffff', 1);
light.position.set(5, 10, 5);
scene.add(light);
// ---------- 渲染函式 ----------
function render() {
requestAnimationFrame(render);
cube.rotation.y += 0.01;
sphere.rotation.x += 0.015;
// --- 左半部 (0~0.5) ---
const width = window.innerWidth;
const height = window.innerHeight;
renderer.setViewport(0, 0, width / 2, height);
renderer.setScissor(0, 0, width / 2, height);
renderer.setScissorTest(true);
renderer.render(scene, camTop);
// --- 右半部 (0.5~1) ---
renderer.setViewport(width / 2, 0, width / 2, height);
renderer.setScissor(width / 2, 0, width / 2, height);
renderer.render(scene, camPersp);
}
render();
// Resize
window.addEventListener('resize', () => {
const w = window.innerWidth, h = window.innerHeight;
renderer.setSize(w, h);
});
技巧:使用
renderer.setScissorTest(true)可以保證左半部的渲染不會影響右半部,避免畫面重疊。
範例 3️⃣:離屏渲染 (RenderTarget) + 合併顯示
此範例先把兩個不同的場景渲染到 RenderTarget,再將結果貼到同一個畫布的兩個四分之一區域,適合做 鏡像、後製特效。
import * as THREE from 'three';
// 建立主 renderer
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 兩個 RenderTarget
const rtParams = { minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBAFormat };
const targetA = new THREE.WebGLRenderTarget(512, 512, rtParams);
const targetB = new THREE.WebGLRenderTarget(512, 512, rtParams);
// Scene A
const sceneA = new THREE.Scene();
const camA = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
camA.position.set(3, 2, 4);
camA.lookAt(0, 0, 0);
sceneA.add(new THREE.AxesHelper(2));
sceneA.add(new THREE.Mesh(
new THREE.TorusKnotGeometry(1, 0.3, 100, 16),
new THREE.MeshStandardMaterial({ color: '#ff00ff' })
));
sceneA.add(new THREE.HemisphereLight());
// Scene B
const sceneB = new THREE.Scene();
const camB = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
camB.position.set(-3, 2, -4);
camB.lookAt(0, 0, 0);
sceneB.add(new THREE.AxesHelper(2));
sceneB.add(new THREE.Mesh(
new THREE.IcosahedronGeometry(1, 0),
new THREE.MeshStandardMaterial({ color: '#00ffff' })
));
sceneB.add(new THREE.DirectionalLight('#ffffff', 1));
// 全螢幕四角佈局的 helper 平面
const quadGeo = new THREE.PlaneGeometry(2, 2);
const materialA = new THREE.MeshBasicMaterial({ map: targetA.texture });
const materialB = new THREE.MeshBasicMaterial({ map: targetB.texture });
const quadA = new THREE.Mesh(quadGeo, materialA);
const quadB = new THREE.Mesh(quadGeo, materialB);
quadA.position.set(-0.5, 0.5, -1); // 左上
quadB.position.set(0.5, -0.5, -1); // 右下
const overlayScene = new THREE.Scene();
overlayScene.add(quadA, quadB);
const overlayCam = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 10);
// ---------- Animation ----------
function animate() {
requestAnimationFrame(animate);
// 1. Render each scene to its own target
renderer.setRenderTarget(targetA);
renderer.render(sceneA, camA);
renderer.setRenderTarget(targetB);
renderer.render(sceneB, camB);
// 2. Render overlay (two quads) to screen
renderer.setRenderTarget(null);
renderer.clear();
renderer.render(overlayScene, overlayCam);
}
animate();
應用:可把 即時陰影貼圖、後製濾鏡 先渲染到離屏緩衝,再在主畫面上以不同方式拼接或混合。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 渲染順序錯亂 | 多個 Renderer 時,若先渲染後面的 Canvas,前面的 Canvas 可能被覆寫。 | 確保在同一幀內 依序呼叫 renderer.render,或使用 requestAnimationFrame 的同一回呼。 |
| Viewport/Scissor 設定遺漏 | 未呼叫 setScissorTest(true) 時,左側視口的渲染會覆寫右側畫面。 |
在使用 setScissor 後 一定要開啟 scissor test。 |
| 相機長寬比不正確 | 視口尺寸改變後相機的 aspect 未更新,畫面會變形。 |
在 resize 事件中 更新相機的 aspect 並呼叫 updateProjectionMatrix()。 |
| 效能問題 | 多個 Renderer 同時渲染會占用大量 GPU 記憶體,尤其使用高解析度 RenderTarget。 | - 只在需要時開啟離屏渲染。 - 控制 RenderTarget 的解析度 (如 512×512)。 - 使用 renderer.setPixelRatio(window.devicePixelRatio) 只在必要時調整。 |
| 事件傳遞混亂 | 多個 Canvas 時,滑鼠/觸控事件會只發送到最上層的 Canvas。 | 為每個 Canvas 分別掛上事件監聽,或在最外層容器捕獲事件後自行分派。 |
最佳實踐
- 將共用資源(幾何、材質)抽離,避免重複載入。
- 使用
requestAnimationFrame的單一回呼,在其中完成所有渲染與更新,保持時間同步。 - 視窗大小改變時統一調整:寫一個
onResize()函式,集中處理所有 renderer、camera 的尺寸更新。 - 在開發階段啟用
renderer.debug.checkShaderErrors = true,可快速定位因多 renderer 造成的 shader 錯誤。
實際應用場景
| 場景 | 需求 | 實作要點 |
|---|---|---|
| 多視角車輛模擬 | 同時呈現外部第三人稱視角與車內儀表板 | 使用兩個 Canvas,左側渲染外部視角,右側渲染儀表板(可用 RenderTarget 先渲染儀表板再貼圖)。 |
| 即時監控牆 | 多路攝影機畫面併排顯示,需支援即時切換 | 單一 Canvas + 多 viewport + scissor,根據使用者選擇動態調整視口排列。 |
| 分屏對戰遊戲 | 兩位玩家各自控制視角,畫面同步更新 | 使用單一 Canvas,兩個相機分別渲染左/右半屏,並共享同一個 scene 以減少記憶體佔用。 |
| AR/VR 雙目顯示 | 左右眼需分別渲染不同的影像 | 利用 RenderTarget 為每隻眼渲染,最後交給 WebXR API 輸出。 |
| 後製特效合成 | 先渲染光暈、模糊等效果,再與主畫面混合 | 離屏渲染到 RenderTarget,使用 THREE.ShaderPass 或自訂 shader 合併至最終畫面。 |
總結
- 多 Renderer / 分割畫面 是 Three.js 強大的彈性之一,讓開發者能同時呈現多個 3D 視角或把不同的渲染結果組合在一起。
- 主要概念在於:獨立的 Canvas、Renderer、Scene、Camera,或 同一 Canvas + Viewport + Scissor,以及 RenderTarget 的離屏渲染。
- 實作時務必注意 渲染順序、相機比例、Scissor 設定,並以 單一 animation loop 來保證畫面同步。
- 依照需求選擇最合適的架構:
- 兩個 Canvas → 各自設定不同解析度、後處理。
- 單一 Canvas 分割 → 使用 Viewport/Scissor,效能較佳。
- 離屏渲染 → 需要後製或特效合成時的首選。
掌握上述技巧後,你就能在專案中靈活運用 多視窗與多渲染場景,打造出更具沉浸感與互動性的 Web 3D 體驗。祝你開發順利,玩得開心!