Three.js 粒子系統與特效:煙霧、火焰、光線等效果
簡介
在 3D 網頁互動中,煙霧、火焰、光線等粒子特效往往是提升視覺衝擊力的關鍵。它們不僅能營造真實感,還能引導使用者注意力、增強氛圍。Three.js 作為最流行的 WebGL 框架,提供了多種建立與控制粒子的方式,從最簡單的 Points 到支援自訂著色器的 GPU 粒子系統,都能滿足從 快速原型 到 高效能大型場景 的需求。本篇文章將帶你一步步了解核心概念、實作範例,並分享常見陷阱與最佳實踐,讓你在專案中快速加入煙霧、火焰與光線等特效。
核心概念
1. 粒子系統的基本構成
| 元素 | 說明 |
|---|---|
| 發射器 (Emitter) | 控制粒子產生的頻率、位置、方向與初始速度。 |
| 生命週期 (Life) | 每顆粒子從出生到死亡的時間,常用 age / lifespan 來做漸變。 |
| 屬性變化 (Attribute Over Life) | 大小、顏色、透明度等屬性可依時間線性或曲線變化。 |
| 渲染方式 | THREE.Points + PointsMaterial、THREE.Sprite、或自訂 ShaderMaterial。 |
| 混合模式 (Blending) | AdditiveBlending、NormalBlending 等決定粒子與背景的疊加方式。 |
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 |
自訂屬性、複雜動畫 | 自訂 uniforms、attributes、vertexShader、fragmentShader |
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();
關鍵點
- 使用 自訂 Shader 能讓每顆粒子擁有獨立的大小與顏色。
gl_PointSize與相機距離掛鉤,使遠端粒子自動縮小。- 結合 UnrealBloomPass,可讓光束產生柔和的發光效果,提升視覺衝擊。
常見陷阱與最佳實踐
| 陷阱 | 可能的症狀 | 解決方案或最佳實踐 |
|---|---|---|
| 過度使用 alpha 渲染 | 透明度排序錯亂、閃爍 (Z‑fighting) | 設 depthWrite: false、使用 Additive 或 Multiply 混合;若需要深度排序,改用 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 可產生陰影效果 |
最佳實踐小結:
- 先規劃粒子數量:在手機端保持 < 2,000 個 Points,桌面端可提升至 5,000~10,000。
- 使用
InstancedBufferGeometry:若需要不同形狀的粒子,Instancing 能減少 draw call。 - 將貼圖與 shader 放在同一個檔案,方便資源管理與快取。
- 利用
requestAnimationFrame及 時間差 (delta),避免固定帧率導致卡頓。 - 測試不同平台: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 領域創造出令人驚豔的動態效果,為使用者帶來沉浸式的視覺體驗。祝你玩得開心,打造出屬於自己的炫麗粒子世界! 🎆