本文 AI 產出,尚未審核

Three.js 粒子系統與特效:煙霧、火焰、光線等效果

簡介

在 3D 網頁互動中,煙霧、火焰、光線等粒子特效往往是提升視覺衝擊力的關鍵。它們不僅能營造真實感,還能引導使用者注意力、增強氛圍。Three.js 作為最流行的 WebGL 框架,提供了多種建立與控制粒子的方式,從最簡單的 Points 到支援自訂著色器的 GPU 粒子系統,都能滿足從 快速原型高效能大型場景 的需求。本篇文章將帶你一步步了解核心概念、實作範例,並分享常見陷阱與最佳實踐,讓你在專案中快速加入煙霧、火焰與光線等特效。


核心概念

1. 粒子系統的基本構成

元素 說明
發射器 (Emitter) 控制粒子產生的頻率、位置、方向與初始速度。
生命週期 (Life) 每顆粒子從出生到死亡的時間,常用 age / lifespan 來做漸變。
屬性變化 (Attribute Over Life) 大小、顏色、透明度等屬性可依時間線性或曲線變化。
渲染方式 THREE.Points + PointsMaterialTHREE.Sprite、或自訂 ShaderMaterial
混合模式 (Blending) AdditiveBlendingNormalBlending 等決定粒子與背景的疊加方式。

Tip:大多數特效的關鍵在於「透明度與混合模式的配合」以及「深度寫入 (depthWrite) 的開關」。


2. 常見粒子類型

2.1 Points (點雲)

最簡單、效能最高的方式,適合大量小顆粒(如煙霧、星塵)。

2.2 Sprites (精靈)

每顆粒子以一張貼圖渲染,適合較大的、需要自訂動畫的粒子(如火焰)。

2.3 GPU Shader Particles

利用自訂 Vertex/Fragment shader 在 GPU 上計算屬性,適合需要 每秒上千甚至上萬顆粒子 的特效(如火山爆發、光束)。


3. 材質與貼圖的選擇

材質 何時使用 重要設定
PointsMaterial 大量小顆粒 size, map, transparent, depthWrite: false, blending: AdditiveBlending
SpriteMaterial 中等大小、需要方向性的粒子 map, color, transparent, depthWrite: false, blending
ShaderMaterial 自訂屬性、複雜動畫 自訂 uniformsattributesvertexShaderfragmentShader

Note:貼圖通常使用 alpha 通道 來控制形狀,PNG 或 DDS 格式皆可,建議使用 Power‑of‑Two (64、128、256…) 的尺寸以提升 GPU 快取效能。


程式碼範例

以下示範三種常見特效的完整實作,皆可直接貼到 Three.js 範例中執行。每段程式碼均含說明註解,方便你快速理解與改寫。

範例 1:簡易煙霧 (Points + 透明貼圖)

// 1. 建立基本場景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 1000);
camera.position.set(0, 2, 5);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);

// 2. 載入煙霧貼圖 (alpha PNG)
const texLoader = new THREE.TextureLoader();
const smokeTex = texLoader.load('textures/smoke.png');

// 3. 建立粒子幾何體 (BufferGeometry)
const particleCount = 2000;
const positions = new Float32Array(particleCount * 3);
const sizes = new Float32Array(particleCount);
for (let i = 0; i < particleCount; i++) {
  positions[i * 3 + 0] = (Math.random() - 0.5) * 4;   // x
  positions[i * 3 + 1] = Math.random() * 3;         // y
  positions[i * 3 + 2] = (Math.random() - 0.5) * 4; // z
  sizes[i] = Math.random() * 20 + 10;               // 隨機大小
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));

// 4. 建立材質,使用 AdditiveBlending 產生柔和光暈
const material = new THREE.PointsMaterial({
  map: smokeTex,
  size: 30,
  sizeAttenuation: true,
  transparent: true,
  depthWrite: false,               // 防止深度測試造成遮擋
  blending: THREE.AdditiveBlending // 加法混合,讓煙霧更柔亮
});

// 5. 組合成 Points 物件
const smoke = new THREE.Points(geometry, material);
scene.add(smoke);

// 6. 動畫:讓煙霧緩緩上升並淡出
function animate() {
  requestAnimationFrame(animate);
  const positions = geometry.attributes.position.array;
  for (let i = 0; i < particleCount; i++) {
    positions[i * 3 + 1] += 0.01; // 上升
    if (positions[i * 3 + 1] > 5) {
      positions[i * 3 + 1] = 0;   // 重新產生
    }
  }
  geometry.attributes.position.needsUpdate = true;
  renderer.render(scene, camera);
}
animate();

重點depthWrite: false + AdditiveBlending 能避免粒子互相遮擋,產生自然的煙霧感。


範例 2:火焰特效 (Sprite + 動態貼圖)

// 1. 先建立基礎場景 (略) ...

// 2. 載入火焰序列貼圖 (4 張 PNG)
const fireFrames = [
  'textures/fire_0.png',
  'textures/fire_1.png',
  'textures/fire_2.png',
  'textures/fire_3.png'
];
const fireTextures = fireFrames.map(f => texLoader.load(f));

// 3. 建立 Sprite 物件陣列
const fireSprites = [];
const fireCount = 80;
for (let i = 0; i < fireCount; i++) {
  const material = new THREE.SpriteMaterial({
    map: fireTextures[0],
    color: 0xffaa33,
    transparent: true,
    depthWrite: false,
    blending: THREE.AdditiveBlending
  });
  const sprite = new THREE.Sprite(material);
  sprite.position.set(
    (Math.random() - 0.5) * 0.5,
    Math.random() * 0.2,
    (Math.random() - 0.5) * 0.5
  );
  sprite.scale.set(0.5, 0.8, 1);
  scene.add(sprite);
  fireSprites.push(sprite);
}

// 4. 動畫:切換貼圖、調整大小與透明度
let frame = 0;
function animateFire() {
  requestAnimationFrame(animateFire);
  frame = (frame + 1) % fireTextures.length;
  fireSprites.forEach((s, idx) => {
    // 每顆粒子依序使用不同的貼圖,產生動畫感
    const texIdx = (frame + idx) % fireTextures.length;
    s.material.map = fireTextures[texIdx];

    // 讓火焰向上漂移、逐漸變小、變淡
    s.position.y += 0.02;
    s.material.opacity = THREE.MathUtils.clamp(1 - s.position.y * 2, 0, 1);
    s.scale.multiplyScalar(0.97);

    // 超過高度則重置
    if (s.position.y > 2) {
      s.position.set(
        (Math.random() - 0.5) * 0.5,
        0,
        (Math.random() - 0.5) * 0.5
      );
      s.scale.set(0.5, 0.8, 1);
      s.material.opacity = 1;
    }
  });
  renderer.render(scene, camera);
}
animateFire();

技巧:使用 Sprite 能讓每顆粒子自動面向相機,配合 序列貼圖 可製作簡易的動畫火焰。AdditiveBlending 讓火焰邊緣呈現光暈效果。


範例 3:光線光束 (GPU Shader Particles + Post‑Processing)

// 1. 基本場景與相機 (略) ...

// 2. 建立自訂 ShaderMaterial
const beamVertex = `
  attribute float size;
  attribute vec3 customColor;
  varying vec3 vColor;
  void main() {
    vColor = customColor;
    vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
    gl_PointSize = size * (300.0 / -mvPosition.z); // 隨距離衰減
    gl_Position = projectionMatrix * mvPosition;
  }
`;

const beamFragment = `
  uniform sampler2D pointTexture;
  varying vec3 vColor;
  void main() {
    vec4 texColor = texture2D(pointTexture, gl_PointCoord);
    gl_FragColor = vec4(vColor, 1.0) * texColor;
    // 使用 Additive 混合會在後處理階段更顯眼
  }
`;

const beamMaterial = new THREE.ShaderMaterial({
  uniforms: {
    pointTexture: { value: texLoader.load('textures/flare.png') }
  },
  vertexShader: beamVertex,
  fragmentShader: beamFragment,
  blending: THREE.AdditiveBlending,
  depthWrite: false,
  transparent: true,
  vertexColors: true
});

// 3. 建立 BufferGeometry,儲存位置、大小、顏色
const beamCount = 5000;
const positions = new Float32Array(beamCount * 3);
const sizes = new Float32Array(beamCount);
const colors = new Float32Array(beamCount * 3);
for (let i = 0; i < beamCount; i++) {
  // 起點在原點,方向向上隨機散射
  const angle = Math.random() * Math.PI * 2;
  const radius = Math.random() * 0.2;
  positions[i * 3 + 0] = Math.cos(angle) * radius;
  positions[i * 3 + 1] = Math.random() * 5; // 高度 0~5
  positions[i * 3 + 2] = Math.sin(angle) * radius;

  sizes[i] = Math.random() * 8 + 4;

  // 顏色從淡藍過渡到白
  const t = positions[i * 3 + 1] / 5;
  colors[i * 3 + 0] = THREE.MathUtils.lerp(0.5, 1.0, t);
  colors[i * 3 + 1] = THREE.MathUtils.lerp(0.6, 1.0, t);
  colors[i * 3 + 2] = THREE.MathUtils.lerp(1.0, 1.0, t);
}
const beamGeometry = new THREE.BufferGeometry();
beamGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
beamGeometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
beamGeometry.setAttribute('customColor', new THREE.BufferAttribute(colors, 3));

// 4. 建立 Points 物件
const beam = new THREE.Points(beamGeometry, beamMaterial);
scene.add(beam);

// 5. 動畫:讓光束向上漂移,並使用簡易的後期發光 (Bloom)
const composer = new THREE.EffectComposer(renderer);
composer.addPass(new THREE.RenderPass(scene, camera));

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

function animateBeam() {
  requestAnimationFrame(animateBeam);
  const pos = beamGeometry.attributes.position.array;
  for (let i = 0; i < beamCount; i++) {
    pos[i * 3 + 1] += 0.02; // 向上
    if (pos[i * 3 + 1] > 5) {
      pos[i * 3 + 1] = 0; // 重置
    }
  }
  beamGeometry.attributes.position.needsUpdate = true;
  composer.render();
}
animateBeam();

關鍵點

  1. 使用 自訂 Shader 能讓每顆粒子擁有獨立的大小與顏色。
  2. gl_PointSize 與相機距離掛鉤,使遠端粒子自動縮小。
  3. 結合 UnrealBloomPass,可讓光束產生柔和的發光效果,提升視覺衝擊。

常見陷阱與最佳實踐

陷阱 可能的症狀 解決方案或最佳實踐
過度使用 alpha 渲染 透明度排序錯亂、閃爍 (Z‑fighting) depthWrite: false、使用 AdditiveMultiply 混合;若需要深度排序,改用 Sprite 並啟用 renderer.sortObjects = true
貼圖過大 記憶體消耗大、帧率下降 使用 Power‑of‑Two (128、256…) 的貼圖;壓縮成 DDS / KTX2,或在 WebGL2 中使用 BCn 壓縮
粒子數量過多且未使用 BufferGeometry CPU 端大量 Object3D,導致 GC 卡頓 盡量使用 單一 Points + BufferGeometry;若需要變化,僅更新緩衝區而非新增物件
未調整 sizeAttenuation 粒子在遠處看起來過大或過小 為遠距離粒子開啟 sizeAttenuation: true,或自行在 shader 中根據 cameraDistance 調整 gl_PointSize
光照與陰影衝突 粒子被場景光照不自然 大多數粒子特效不需要光照,直接關閉 material.lights = false;若需要光照,改用 Mesh + Shader 計算光照
混合模式選錯 顏色看起來過暗或過亮 Additive 適合火焰、光束;Normal 適合煙霧、灰塵;Multiply 可產生陰影效果

最佳實踐小結

  1. 先規劃粒子數量:在手機端保持 < 2,000 個 Points,桌面端可提升至 5,000~10,000
  2. 使用 InstancedBufferGeometry:若需要不同形狀的粒子,Instancing 能減少 draw call。
  3. 將貼圖與 shader 放在同一個檔案,方便資源管理與快取。
  4. 利用 requestAnimationFrame時間差 (delta),避免固定帧率導致卡頓。
  5. 測試不同平台:Chrome、Safari、Edge 在透明度排序上略有差異,務必在主要目標瀏覽器上驗證。

實際應用場景

場景 需求 建議使用的技術
遊戲中的火把、熔岩 動態光暈、燃燒感、低功耗 Sprite + 序列貼圖 + AdditiveBlending
科幻 UI 中的光束指示 高亮、發光、可調節寬度 GPU Shader Particles + Bloom 後期
天氣模擬:霧、煙 大範圍、柔和、可疊加 Points + 透明貼圖 + depthWrite:false
VR/AR 中的粒子交互 需要雙眼同步、低延遲 Instanced Points + WebXR 渲染循環
產品展示的火花特效 短暫、亮度高、可觸發 Sprite + 顏色/大小隨時間變化、AdditiveBlending

案例說明:在一個 奇幻 RPG 中,火把的火焰使用 Sprite + 四帧動畫,煙霧使用 Points,而遠端的魔法光束則採用 GPU Shader + Bloom,三者結合後不僅視覺層次分明,也保持了 60 FPS 的流暢度。


總結

粒子系統是 Three.js 中最具表現力的工具之一,透過 Points、Sprite、Shader 三大路線,我們可以輕鬆打造煙霧、火焰、光線等多樣特效。掌握以下關鍵要點,即可在實務專案中快速上手:

  • 透明度與混合模式 必須配合 depthWrite:false 使用,以避免遮擋問題。
  • 貼圖大小與格式 直接影響效能,盡量使用 Power‑of‑Two 且壓縮。
  • BufferGeometry + Instancing 是大規模粒子效能的核心。
  • 後期 Bloom 能為光線類特效加分,提升真實感。

只要遵循 最佳實踐、避免常見陷阱,你就能在 WebGL 領域創造出令人驚豔的動態效果,為使用者帶來沉浸式的視覺體驗。祝你玩得開心,打造出屬於自己的炫麗粒子世界! 🎆