本文 AI 產出,尚未審核

Three.js 效能最佳化:LOD(Level of Detail)


簡介

在 3D 網頁應用中,畫面渲染的流暢度往往直接影響使用者體驗。即使硬體效能不斷提升,大量的多邊形、貼圖與光照計算仍會讓瀏覽器的幀率跌破底線。
Level of Detail(LOD) 是一種根據相機距離自動切換模型細節等級的技術,能在保留視覺品質的同時大幅降低 GPU 負載。

對於使用 Three.js 開發的遊戲、城市模型或資料視覺化平台而言,正確運用 LOD 不僅能提升 FPS,還能減少記憶體佔用,讓應用在手機或低階筆電上也能順暢運行。本文將從概念、實作、常見陷阱與最佳實踐,帶你一步步掌握 LOD 在 Three.js 中的應用。


核心概念

1. LOD 的原理

LOD 的核心思想是「遠處的物件使用較低解析度的模型,近距離則使用高解析度模型」。

  • 距離判斷:Three.js 內建 THREE.LOD 會根據相機與 LOD 物件的距離自動切換。
  • 多層模型:每個 LOD 物件可以掛載多個不同細節等級的 Mesh,並為每個等級設定切換距離。

這樣的切換是 即時且無縫 的,使用者不會感受到模型「跳動」或「閃爍」。

2. 為什麼要使用 LOD

項目 無 LOD 使用 LOD
GPU 多邊形量 可能達數百萬 減少 30%~90%(依距離)
記憶體佔用 大量貼圖與幾何資料 只載入最近的細節模型
渲染效能 FPS 低、卡頓 FPS 穩定提升 10~30 FPS
手機端體驗 常常無法流暢 大幅提升流暢度

3. Three.js 中的 LOD 類別

THREE.LOD 繼承自 THREE.Object3D,主要方法包括:

方法 功能
addLevel( object, distance ) 為 LOD 加入一個等級,distance 為切換到此等級的最遠距離
update( camera ) 每一幀呼叫,根據相機位置決定顯示哪個等級
getObjectForDistance( distance ) 取得指定距離下的物件(可自行使用)

小技巧:若模型數量龐大,建議在 requestAnimationFrame 迴圈的最前端先呼叫 lod.update(camera),避免在渲染階段才切換造成不必要的計算。


程式碼範例

以下示範三種常見的 LOD 實作方式,從最基礎到進階應用,皆附上說明註解。

範例 1:最簡單的 LOD 設定

import * as THREE from 'three';

// 建立場景、相機與渲染器(略)

// 1. 讀取三個不同解析度的模型(此處以 BoxGeometry 為例)
const highDetail = new THREE.Mesh(
  new THREE.BoxGeometry(1, 1, 1, 32, 32, 32), // 高細節
  new THREE.MeshStandardMaterial({ color: 0xff0000 })
);
const midDetail = new THREE.Mesh(
  new THREE.BoxGeometry(1, 1, 1, 16, 16, 16), // 中等細節
  new THREE.MeshStandardMaterial({ color: 0x00ff00 })
);
const lowDetail = new THREE.Mesh(
  new THREE.BoxGeometry(1, 1, 1, 4, 4, 4), // 低細節
  new THREE.MeshStandardMaterial({ color: 0x0000ff })
);

// 2. 建立 LOD 物件,並加入各等級
const lod = new THREE.LOD();
lod.addLevel(highDetail, 0);    // 距離 0~20 時使用高細節
lod.addLevel(midDetail, 20);    // 距離 20~50 時使用中等細節
lod.addLevel(lowDetail, 50);    // 超過 50 時使用低細節

scene.add(lod);

// 3. 在動畫迴圈中更新 LOD
function animate() {
  requestAnimationFrame(animate);
  lod.update(camera); // 依相機位置自動切換
  renderer.render(scene, camera);
}
animate();

說明addLevel 的第二個參數是「切換的最遠距離」,Three.js 會自動在距離區間內選擇最近的等級。


範例 2:從 GLTF 模型載入多層 LOD

當模型較為複雜時,我們通常會先在外部 3D 軟體(如 Blender)匯出不同細節等級的 GLTF,然後在程式中組合。

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import * as THREE from 'three';

const loader = new GLTFLoader();
const lod = new THREE.LOD();

Promise.all([
  loader.loadAsync('models/tree_high.gltf'), // 高細節
  loader.loadAsync('models/tree_mid.gltf'),  // 中等細節
  loader.loadAsync('models/tree_low.gltf')   // 低細節
]).then(([high, mid, low]) => {
  // GLTFScene 內的第一個子物件即為 Mesh
  lod.addLevel(high.scene, 0);
  lod.addLevel(mid.scene, 30);
  lod.addLevel(low.scene, 70);
  scene.add(lod);
});

重點:使用 loadAsync 可以讓程式以 Promise 方式同步載入多個模型,避免「先渲染低細節再切換」的視覺閃爍。


範例 3:自訂 LOD 切換邏輯(結合視錐體剔除)

在某些大型場景中,我們希望在 視錐體外 的物件直接以最簡單的模型或完全隱藏,以下示範如何在 update 前先檢查 frustum

import * as THREE from 'three';

const frustum = new THREE.Frustum();
const cameraViewProjectionMatrix = new THREE.Matrix4();

function updateLOD(lod, camera) {
  // 1. 計算相機視錐體
  camera.updateMatrixWorld(); // 確保矩陣是最新的
  cameraViewProjectionMatrix.multiplyMatrices(
    camera.projectionMatrix,
    camera.matrixWorldInverse
  );
  frustum.setFromProjectionMatrix(cameraViewProjectionMatrix);

  // 2. 判斷 LOD 是否在視錐體內
  const boundingBox = new THREE.Box3().setFromObject(lod);
  if (!frustum.intersectsBox(boundingBox)) {
    // 完全不在視野內,直接隱藏
    lod.visible = false;
    return;
  }
  // 仍在視野內,才更新細節等級
  lod.visible = true;
  lod.update(camera);
}

// 動畫迴圈
function animate() {
  requestAnimationFrame(animate);
  updateLOD(lod, camera);
  renderer.render(scene, camera);
}
animate();

說明:透過視錐體剔除(Frustum Culling)配合 LOD,可避免遠距離且不在視野的物件仍耗費 GPU 資源。


範例 4:使用 InstancedMesh 搭配 LOD(大量相同物件)

若場景中有成千上萬棵相同樹木,使用 InstancedMesh 能減少 draw call。下面示範如何為每個實例動態切換 LOD。

import * as THREE from 'three';

const geometryHigh = new THREE.ConeGeometry(1, 5, 16);
const geometryLow  = new THREE.ConeGeometry(1, 5, 4);
const material = new THREE.MeshStandardMaterial({ color: 0x228822 });

const count = 5000;
const highInst = new THREE.InstancedMesh(geometryHigh, material, count);
const lowInst  = new THREE.InstancedMesh(geometryLow, material, count);

scene.add(highInst);
scene.add(lowInst);

// 隨機產生位置
for (let i = 0; i < count; i++) {
  const matrix = new THREE.Matrix4()
    .makeTranslation(
      (Math.random() - 0.5) * 200,
      0,
      (Math.random() - 0.5) * 200
    );
  highInst.setMatrixAt(i, matrix);
  lowInst.setMatrixAt(i, matrix);
}

// 每幀根據相機距離切換顯示
function updateInstancedLOD(camera) {
  const camPos = camera.position;
  let highCount = 0, lowCount = 0;

  for (let i = 0; i < count; i++) {
    const pos = new THREE.Vector3();
    highInst.getMatrixAt(i, matrix);
    matrix.decompose(pos, new THREE.Quaternion(), new THREE.Vector3());

    const distance = camPos.distanceTo(pos);
    if (distance < 30) highCount++; else lowCount++;
  }
  highInst.visible = highCount > 0;
  lowInst.visible = lowCount > 0;
}

技巧InstancedMesh 只能使用同一個材質,若需要不同材質的 LOD,請先將材質合併為 Atlas


範例 5:自動生成 LOD(利用 SimplifyModifier

Three.js 自帶的 SimplifyModifier 能在程式執行時自動降低多邊形數量,快速產生低階模型。

import { SimplifyModifier } from 'three/examples/jsm/modifiers/SimplifyModifier.js';
import * as THREE from 'three';

function createLODFromGeometry(geometry, levels = [0.5, 0.2, 0.1]) {
  const lod = new THREE.LOD();
  const material = new THREE.MeshStandardMaterial({ color: 0x8888ff });

  const modifier = new SimplifyModifier();

  // 原始高細節
  const highMesh = new THREE.Mesh(geometry.clone(), material);
  lod.addLevel(highMesh, 0);

  // 產生簡化等級
  levels.forEach((ratio, idx) => {
    const simplified = modifier.modify(geometry.clone(), Math.floor(geometry.attributes.position.count * ratio));
    const mesh = new THREE.Mesh(simplified, material);
    lod.addLevel(mesh, (idx + 1) * 30); // 每 30 單位距離切換一次
  });

  return lod;
}

// 使用範例
const boxGeo = new THREE.BoxGeometry(2, 2, 2, 64, 64, 64);
const lodBox = createLODFromGeometry(boxGeo);
scene.add(lodBox);

注意SimplifyModifier 計算量不小,建議在 載入完成或背景執行 時才呼叫,避免卡住主執行緒。


常見陷阱與最佳實踐

陷阱 可能的影響 解決方案
切換距離設定不合理 物件在遠近切換時出現明顯的細節跳躍 先在開發工具(如 Chrome DevTools)觀察相機距離,使用 階梯式(0‑20‑50‑100)或 平滑過渡 的距離值
高細節模型仍被載入 即使遠距離不顯示,仍會佔用記憶體與載入時間 採用 懶載入(lazy‑load)或 動態 import,在需要時才載入高階模型
多個 LOD 同時可見 造成不必要的 draw call,降低效能 確保每個 LOD 只保留 一個子物件 可見,使用 lod.update(camera) 自動隱藏其他等級
視錐體剔除與 LOD 重複計算 每幀兩次距離判斷,浪費 CPU 合併判斷:先做視錐體剔除,再決定是否需要更新 LOD
貼圖尺寸過大 即使模型簡化,貼圖仍佔用大量 VRAM 為不同 LOD 準備 不同解析度的貼圖(例如 1024 → 512 → 256)

最佳實踐

  1. 先規劃 LOD 階層:在 3D 建模階段就決定要產生多少層,通常 2‑3 層即可覆蓋大部分需求。
  2. 使用 GLTFmeshopt 壓縮:結合 meshopt_encoder 可讓高細節模型的檔案大小減少 50% 以上。
  3. 結合 InstancedMesh:大量相同物件(樹、岩石)時,先做實例化,再針對實例使用 LOD。
  4. 分段載入(Chunked Loading):將場景切成多個區塊,玩家進入區塊時才載入該區塊的 LOD。
  5. 測試與調校:在真機(手機、平板)上使用 stats.js 或 Chrome 的 GPU 計時工具,觀察 FPS、draw call 與記憶體變化,逐步微調切換距離與模型細節。

實際應用場景

場景 為何需要 LOD 實作要點
城市規模的 GIS 可視化 城市中有成千上萬的建築,遠距離只需要低解析度的外觀 為每棟建築產生 3 層 LOD,使用 GLTFdraco 壓縮,並在使用者移動相機時動態載入近距離的高階模型。
第一人稱射擊遊戲 玩家在高速移動時,遠處的環境不需要高細節 把遠景山脈、樹林做成低多邊形模型,配合 天空盒;近距離的武器與角色使用高細節模型。
虛擬實境(VR) VR 裝置的渲染頻率要求 90 FPS 以上,資源受限 使用 InstancedMesh 搭配 LOD,並在每個框架只更新視野內的 LOD,減少 GPU 負載。
AR 產品展示 手機相機畫面會與虛擬模型混合,需保持流暢 只在螢幕中央顯示高細節模型,四周使用低細節或簡化貼圖,以降低渲染成本。
教育與科學模擬(如分子結構) 觀察者可能會放大或縮小模型 依放大倍率切換不同層級的球體或棒狀模型,確保在任何縮放階段都有合理的幾何細節。

總結

Level of Detail 是 Three.js 中提升效能、降低資源消耗的關鍵技術。透過 根據相機距離自動切換模型細節,我們可以在不犧牲視覺品質的前提下,將 GPU 的多邊形計算與記憶體使用量減到最小。

本文從概念說明、完整程式碼範例、常見陷阱與最佳實踐,以及實際應用場景逐層解析,幫助你在 從小型 Demo 到大型商業專案 中,靈活運用 LOD。
在實作時,記得:

  1. 事先規劃好 LOD 階層與切換距離。
  2. 結合視錐體剔除與懶載入,避免不必要的計算。
  3. 使用 InstancedMeshSimplifyModifier 或外部工具(如 Blender)產生低階模型。
  4. 在真機上測試、微調,確保 FPS、draw call 與記憶體皆在可接受範圍。

只要掌握這些要點,你的 Three.js 作品就能在各種裝置上保持 流暢、穩定且視覺上令人滿意 的表現。祝開發順利,玩得開心!