threejs-webgl

threejs-webgl

熱門

Three.js 3D 網頁開發的全面技能。在建立互動式 3D 場景、WebGL/WebGPU 應用程式、產品配置器、3D 視覺化或沉浸式網頁體驗時使用此技能。觸發條件為涉及 Three.js、3D 渲染、場景、相機、網格、材質、燈光、動畫、紋理或 WebGL/WebGPU 渲染的任務。

629星標
103分支
更新於 2025/11/20
SKILL.md
唯讀
名稱
threejs-webgl
描述

Three.js 3D 網頁開發的全面技能。在建立互動式 3D 場景、WebGL/WebGPU 應用程式、產品配置器、3D 視覺化或沉浸式網頁體驗時使用此技能。觸發條件為涉及 Three.js、3D 渲染、場景、相機、網格、材質、燈光、動畫、紋理或 WebGL/WebGPU 渲染的任務。

Three.js WebGL/WebGPU 開發

概述

Three.js 是使用 WebGL 和 WebGPU 在網頁瀏覽器中建立 3D 圖形的業界標準 JavaScript 函式庫。此技能提供建立高效能、互動式 3D 體驗的全面指導,包括場景、相機、渲染器、幾何體、材質、燈光、紋理和動畫。

核心概念

場景圖架構

Three.js 使用階層式場景圖,所有 3D 物件以樹狀結構組織:

Scene
├── Camera
├── Lights
│   ├── AmbientLight
│   ├── DirectionalLight
│   └── PointLight
├── Meshes
│   ├── Mesh (Geometry + Material)
│   └── InstancedMesh
└── Groups

必要元件

每個 Three.js 應用程式都需要這些核心元素:

  1. Scene:所有 3D 物件的容器
  2. Camera:定義觀看視角
  3. Renderer:將場景繪製到畫布(WebGL 或 WebGPU)
  4. Geometry:定義物件的形狀
  5. Material:定義表面外觀
  6. Mesh:結合幾何體和材質

快速開始模式

基本場景設定

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// Scene, Camera, Renderer
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x333333);

const camera = new THREE.PerspectiveCamera(
  75, // FOV
  window.innerWidth / window.innerHeight, // 長寬比
  0.1, // 近裁剪平面
  1000 // 遠裁剪平面
);
camera.position.set(0, 2, 5);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);

// 燈光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);

const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 10, 7.5);
directionalLight.castShadow = true;
scene.add(directionalLight);

// 控制
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;

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

animate();

// 處理視窗大小變化
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

WebGPU 設定(現代替代方案)

import * as THREE from 'three/webgpu';

const renderer = new THREE.WebGPURenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setAnimationLoop(animate);
renderer.toneMapping = THREE.LinearToneMapping;
renderer.toneMappingExposure = 1;
document.body.appendChild(renderer.domElement);

常見模式

1. 建立帶有材質的網格

// 基本網格
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({
  color: 0x00ff00,
  roughness: 0.5,
  metalness: 0.5
});
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

// 帶紋理的網格
const loader = new THREE.TextureLoader();
const texture = loader.load('texture.jpg');
texture.colorSpace = THREE.SRGBColorSpace;

const texturedMaterial = new THREE.MeshStandardMaterial({
  map: texture
});
const mesh = new THREE.Mesh(geometry, texturedMaterial);
scene.add(mesh);

2. 燈光策略

// 三點燈光設定
function setupThreePointLight(scene) {
  // 主光(主要)
  const keyLight = new THREE.DirectionalLight(0xffffff, 3);
  keyLight.position.set(5, 10, 7.5);
  keyLight.castShadow = true;
  scene.add(keyLight);

  // 補光(柔化陰影)
  const fillLight = new THREE.DirectionalLight(0xffffff, 1);
  fillLight.position.set(-5, 5, -5);
  scene.add(fillLight);

  // 輪廓光(邊緣定義)
  const rimLight = new THREE.DirectionalLight(0xffffff, 0.5);
  rimLight.position.set(0, 5, -10);
  scene.add(rimLight);

  // 環境光(基礎照明)
  const ambient = new THREE.AmbientLight(0x404040, 0.5);
  scene.add(ambient);
}

// 物理燈光(真實)
const bulbLight = new THREE.PointLight(0xffee88, 1, 100, 2);
bulbLight.power = 1700; // 流明(相當於 100W 燈泡)
bulbLight.castShadow = true;
scene.add(bulbLight);

// 半球光(天空 + 地面)
const hemiLight = new THREE.HemisphereLight(
  0xddeeff, // 天空顏色
  0x0f0e0d, // 地面顏色
  0.02
);
scene.add(hemiLight);

3. 實例化幾何體(效能)

// 用於高效渲染數千個相似物件
const geometry = new THREE.SphereGeometry(0.1, 16, 16);
const material = new THREE.MeshStandardMaterial({ color: 0xff0000 });
const instancedMesh = new THREE.InstancedMesh(geometry, material, 1000);

const matrix = new THREE.Matrix4();
const color = new THREE.Color();

for (let i = 0; i < 1000; i++) {
  matrix.setPosition(
    Math.random() * 10 - 5,
    Math.random() * 10 - 5,
    Math.random() * 10 - 5
  );
  instancedMesh.setMatrixAt(i, matrix);
  instancedMesh.setColorAt(i, color.setHex(Math.random() * 0xffffff));
}

instancedMesh.instanceMatrix.needsUpdate = true;
scene.add(instancedMesh);

4. 載入 3D 模型(glTF)

import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';

// 設定載入器
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/draco/');

const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(dracoLoader);

// 載入模型
gltfLoader.load('model.glb', (gltf) => {
  const model = gltf.scene;

  // 啟用陰影
  model.traverse((child) => {
    if (child.isMesh) {
      child.castShadow = true;
      child.receiveShadow = true;
    }
  });

  scene.add(model);

  // 處理動畫
  if (gltf.animations.length > 0) {
    const mixer = new THREE.AnimationMixer(model);
    const action = mixer.clipAction(gltf.animations[0]);
    action.play();

    // 在動畫迴圈中:
    // mixer.update(deltaTime);
  }
});

5. 陰影設定

// 在渲染器上啟用陰影
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // 或 VSMShadowMap

// 設定燈光陰影
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 50;
directionalLight.shadow.camera.left = -10;
directionalLight.shadow.camera.right = 10;
directionalLight.shadow.camera.top = 10;
directionalLight.shadow.camera.bottom = -10;
directionalLight.shadow.radius = 4;
directionalLight.shadow.blurSamples = 8;

// 物件投射/接收陰影
mesh.castShadow = true;
mesh.receiveShadow = true;

6. 光線投射(互動)

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();

function onMouseClick(event) {
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

  raycaster.setFromCamera(mouse, camera);
  const intersects = raycaster.intersectObjects(scene.children, true);

  if (intersects.length > 0) {
    const object = intersects[0].object;
    object.material.color.set(0xff0000);
  }
}

window.addEventListener('click', onMouseClick);

整合模式

與 GSAP 動畫整合

import gsap from 'gsap';

// 動畫相機
gsap.to(camera.position, {
  x: 5,
  y: 3,
  z: 10,
  duration: 2,
  ease: "power2.inOut",
  onUpdate: () => {
    camera.lookAt(scene.position);
  }
});

// 動畫網格屬性
gsap.to(mesh.rotation, {
  y: Math.PI * 2,
  duration: 3,
  repeat: -1,
  ease: "none"
});

與 React 整合(參見 react-three-fiber 技能)

// Three.js 與 React Three Fiber 自然整合
// 使用 react-three-fiber 技能取得 React 整合模式

與後處理整合

import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';

const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));

const bloomPass = new UnrealBloomPass(
  new THREE.Vector2(window.innerWidth, window.innerHeight),
  1.5, // 強度
  0.4, // 半徑
  0.85 // 閾值
);
composer.addPass(bloomPass);

// 在動畫迴圈中:
composer.render();

效能最佳化

1. 幾何體重用

// 不好:為每個網格建立新幾何體
for (let i = 0; i < 100; i++) {
  const geometry = new THREE.BoxGeometry(1, 1, 1);
  const mesh = new THREE.Mesh(geometry, material);
  scene.add(mesh);
}

// 好:重用幾何體
const sharedGeometry = new THREE.BoxGeometry(1, 1, 1);
for (let i = 0; i < 100; i++) {
  const mesh = new THREE.Mesh(sharedGeometry, material);
  scene.add(mesh);
}

2. 對重複物件使用 InstancedMesh

對於數百/數千個相同物件,使用 InstancedMesh(參見上述模式)。

3. 紋理最佳化

// 壓縮紋理
texture.generateMipmaps = true;
texture.minFilter = THREE.LinearMipmapLinearFilter;
texture.magFilter = THREE.LinearFilter;

// 使用二的冪次方尺寸(512、1024、2048)
// 考慮對多個小紋理使用紋理圖集

4. 細節層級(LOD)

const lod = new THREE.LOD();
lod.addLevel(highDetailMesh, 0);    // 0-50 單位
lod.addLevel(mediumDetailMesh, 50);  // 50-100 單位
lod.addLevel(lowDetailMesh, 100);    // 100+ 單位
scene.add(lod);

5. 視錐剔除

Three.js 自動剔除相機視野外的物件。確保物件有正確的包圍球:

mesh.geometry.computeBoundingSphere();

6. 釋放資源

function disposeScene() {
  scene.traverse((object) => {
    if (object.geometry) object.geometry.dispose();
    if (object.material) {
      if (Array.isArray(object.material)) {
        object.material.forEach(material => material.dispose());
      } else {
        object.material.dispose();
      }
    }
  });
  renderer.dispose();
}

最佳實務

1. 使用動畫時鐘以獲得一致的計時

const clock = new THREE.Clock();

function animate() {
  const deltaTime = clock.getDelta();
  const elapsedTime = clock.getElapsedTime();

  // 使用 deltaTime 進行與幀率無關的動畫
  mesh.rotation.y += deltaTime * Math.PI * 0.5; // 每秒 90°

  renderer.render(scene, camera);
}

2. 相機設定指南

  • FOV:大多數應用程式為 45-75°
  • 近平面:盡可能遠(避免 z-fighting)
  • 遠平面:盡可能近(精確度)
  • 長寬比:始終匹配畫布尺寸

3. 材質選擇

  • MeshBasicMaterial:無光照、平面顏色(除錯、UI)
  • MeshLambertMaterial:便宜的漫反射光照(行動裝置)
  • MeshPhongMaterial:鏡面高光(舊標準)
  • MeshStandardMaterial:PBR、真實(推薦)
  • MeshPhysicalMaterial:進階 PBR(clearcoat、透射)

4. 座標系統

  • Three.js 使用右手座標系統
  • +Y 向上,+Z 朝向相機,+X 向右
  • 旋轉使用弧度(Math.PI = 180°)

5. 場景組織

// 將相關物件分組
const building = new THREE.Group();
building.add(walls, roof, windows);
scene.add(building);

// 使用有意義的名稱
mesh.name = 'player-character';
const found = scene.getObjectByName('player-character');

常見陷阱

1. 調整大小時未更新長寬比

視窗大小改變時,務必更新相機長寬比和投影矩陣。

2. 在動畫迴圈中建立新物件

// 不好:記憶體洩漏
function animate() {
  const geometry = new THREE.BoxGeometry(); // 每一幀都建立!
  // ...
}

// 好:在迴圈外建立一次
const geometry = new THREE.BoxGeometry();
function animate() {
  // 重用幾何體
}

3. 忘記啟用陰影

記得在渲染器、燈光和物件上啟用陰影。

4. Z-Fighting(閃爍)

  • 增加近平面距離
  • 減少遠平面距離
  • 避免重疊的共面表面
  • 使用 material.polygonOffset = true 搭配 material.polygonOffsetFactor

5. 色彩空間問題

// 始終為紋理設定色彩空間
texture.colorSpace = THREE.SRGBColorSpace;

// 設定渲染器輸出編碼
renderer.outputColorSpace = THREE.SRGBColorSpace;

6. 未釋放資源

不再需要時,務必對幾何體、材質、紋理和渲染器呼叫 .dispose()

資源

此技能包含隨附資源,以加速 Three.js 開發:

references/

  • api_reference.md:核心類別(Scene、Camera、Renderer 等)的快速 API 參考
  • materials_guide.md:完整的材質類型和屬性
  • optimization_checklist.md:效能最佳化策略

scripts/

  • setup_scene.py:產生 Three.js 場景設定的樣板程式碼
  • texture_optimizer.py:批次最佳化網頁紋理(調整大小、壓縮)
  • gltf_validator.py:使用前驗證 glTF 模型

assets/

  • starter_scene/:完整的 HTML/JS 樣板專案
  • shaders/:自訂 GLSL 著色器範例(頂點、片段)
  • hdri/:用於 PBR 照明的環境貼圖
  • draco/:用於壓縮模型的 DRACO 解碼器

進階主題

自訂著色器(GLSL)

const material = new THREE.ShaderMaterial({
  uniforms: {
    uTime: { value: 0.0 },
    uColor: { value: new THREE.Color(0x00ff00) }
  },
  vertexShader: `
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    uniform float uTime;
    uniform vec3 uColor;
    varying vec2 vUv;
    void main() {
      gl_FragColor = vec4(uColor * vUv.x, 1.0);
    }
  `
});

渲染目標(渲染到紋理)

const renderTarget = new THREE.WebGLRenderTarget(512, 512);

// 將場景渲染到紋理
renderer.setRenderTarget(renderTarget);
renderer.render(scene, camera);
renderer.setRenderTarget(null);

// 使用紋理
const material = new THREE.MeshBasicMaterial({
  map: renderTarget.texture
});

GPU 計算(GPGPU)

使用 GPUComputationRenderer 進行粒子模擬、布料物理等。

何時使用此技能

在以下情況使用此技能:

  • 建立互動式 3D 網頁體驗
  • 建立產品配置器或視覺化工具
  • 實作 WebGL/WebGPU 渲染
  • 處理 3D 模型、場景或動畫
  • 最佳化 Three.js 效能
  • 將 Three.js 與其他函式庫(GSAP、React 等)整合
  • 除錯 Three.js 渲染問題

如需 React 整合,請使用 react-three-fiber 技能。
如需動畫,請搭配 gsap-scrolltrigger 技能。
如需 UI 動畫,請使用 motion-framer 技能。