three.js實現(xiàn)幾個炫酷的粒子特效實例(火焰、煙霧、煙花)
前言
都說 3D(Three.js,Babylon.js) 是前端最后的護城河,為什么這么說呢?因為隨著AI編碼工具(Cursor,Trae)和當今主流框架(React,Vue)的生態(tài)發(fā)展成熟前端的門檻似乎被拉的更低了,因為通過AI編碼工具許多非前端崗位的開發(fā)者也可以很輕松的去實現(xiàn)一些門戶頁面和后臺管理系統(tǒng)了。
而3D前端(Three.js,Babylon.js)之所以是前端最后的護城河,我想更多是因為3D相關(guān)開發(fā)的內(nèi)容有更高的門檻和更高的難度吧。
本篇給大家分享一下如何使用Three.js去實現(xiàn)火焰,煙霧,煙花這三個粒子特效

涉及的Three.js核心API方法介紹
1.THREE.PointsMaterial
THREE.PointsMaterial 是一種專門用于 點渲染 的材質(zhì)(類似于 MeshBasicMaterial 是網(wǎng)格的材質(zhì))。它定義了每一個點的外觀,比如大小、顏色、是否透明等
2.THREE.Points
THREE.Points 是繼承自 THREE.Object3D 的渲染對象,用于 將幾何體中的所有頂點作為單個點渲染出來,搭配 PointsMaterial 使用。
3.THREE.BufferGeometry
是一種通過 緩沖區(qū)(Buffer)存儲頂點數(shù)據(jù) 的幾何體結(jié)構(gòu),用于高效地構(gòu)建和管理頂點數(shù)據(jù)
創(chuàng)建火焰粒子特效方法
這里將創(chuàng)建火焰特效的方法邏輯進行單獨抽離,方法接收以下幾個參數(shù),方便我們創(chuàng)建不同效果的火焰粒子
1.meshPosition 粒子位置
2.name 粒子名稱
3.color 粒子顏色
4.size 粒子大小
5.height 粒子高度
6.range 粒子范圍
7.praticleCount 粒子數(shù)量
hexToHSL 函數(shù)方法是用于將十六進制的顏色值轉(zhuǎn)化為HSL 對象
/**
* 將十六進制顏色字符串轉(zhuǎn)換為HSL對象
* @param hex 十六進制顏色字符串 (#RRGGBB)
* @returns HSL對象
*/
function hexToHSL(hex: string): { h: number; s: number; l: number } {
// 移除#號
hex = hex.replace(/^#/, '');
// 解析十六進制值
const r = parseInt(hex.substring(0, 2), 16) / 255;
const g = parseInt(hex.substring(2, 4), 16) / 255;
const b = parseInt(hex.substring(4, 6), 16) / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h = 0,
s = 0;
const l = (max + min) / 2;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return { h, s, l };
}
/**
* 創(chuàng)建火焰特效
* @param params 特效參數(shù)
* meshPosition 粒子位置
* name 粒子名稱
* color 粒子顏色
* size 粒子大小
* height 粒子高度
* range 粒子范圍
* praticleCount 粒子數(shù)量
*/
function createFireEffect(params: EffectParams) {
const { meshPosition, name, color,size=1, height=1, range=1, praticleCount=1} = params;
// 處理顏色參數(shù),將十六進制顏色值轉(zhuǎn)化為 HSL 格式
const color =this.hexToHSL(color)
// 加載火焰貼圖
const fireTexture = textureLoader.load(fireImage);
// 粒子系統(tǒng)參數(shù)
const PARTICLE_COUNT = particleCount;
const positions = new Float32Array(PARTICLE_COUNT * 3);
const velocities = new Float32Array(PARTICLE_COUNT * 3);
const lifetimes = new Float32Array(PARTICLE_COUNT);
const startTimes = new Float32Array(PARTICLE_COUNT);
const sizes = new Float32Array(PARTICLE_COUNT);
const colors = new Float32Array(PARTICLE_COUNT * 3);
const maxLifetime = 2.0; // 粒子最大存活時間(秒)
// 初始化粒子
for (let i = 0; i < PARTICLE_COUNT; i++) {
// 初始都放在發(fā)射點
positions[3 * i] = 0;
positions[3 * i + 1] = 0;
positions[3 * i + 2] = 0;
// 隨機速度,主要向上,加一點隨機散射
velocities[3 * i] = (Math.random() - 0.5) * 0.5 * range;
velocities[3 * i + 1] = (Math.random() * 1.5 + 1.0) * height;
velocities[3 * i + 2] = (Math.random() - 0.5) * 0.5 * range;
// 生命周期
lifetimes[i] = Math.random() * maxLifetime;
startTimes[i] = 0; // 會在動畫中重置
// 底部火焰更小,應(yīng)用size參數(shù)
sizes[i] = (Math.random() * 0.2 + 0.2) * size;
// 應(yīng)用自定義顏色
const c = new THREE.Color().setHSL(
color.h + Math.random() * 0.03,
color.s,
color.l
);
colors[3 * i] = c.r;
colors[3 * i + 1] = c.g;
colors[3 * i + 2] = c.b;
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('velocity', new THREE.BufferAttribute(velocities, 3));
geometry.setAttribute('startTime', new THREE.BufferAttribute(startTimes, 1));
geometry.setAttribute('lifetime', new THREE.BufferAttribute(lifetimes, 1));
geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({
size: 0.2 * size,
map: fireTexture,
vertexColors: true,
transparent: true,
depthWrite: false, // 禁止寫入深度,讓重疊粒子都可見
blending: THREE.AdditiveBlending, // 加法混合
});
// 釋放紋理資源
fireTexture.dispose();
const particles = new THREE.Points(geometry, material);
particles.name = name;
// 添加粒子方法標識
particles.useData.effectMethod='CreateFireEffect'
// 設(shè)置特效位置
particles.position.copy(meshPosition);
// 添加到場景
this?.scene?.add(particles)
// 存儲到火焰特效集合中,便于后續(xù)更新
this.effectsMap.set(particles.uuid, particles);
}
好了,這樣一個創(chuàng)建粒子火焰效果的方法就寫好了

但這個時候的火焰效果是靜態(tài)的,所有的點材質(zhì)內(nèi)容的位置都是固定的,這里為了實現(xiàn)一個動態(tài)的火焰效果,我們就需要通過 requestAnimationFrame 動畫循環(huán)的方式不間斷的更新點材質(zhì)的位置,顏色等參數(shù)值,從而實現(xiàn)一個動態(tài)的火焰粒子效果。
后面的煙霧粒子和煙花粒子的實現(xiàn)邏輯方式也是類似,都是通過不間斷的去修改點材質(zhì)的位置和顏色等值來實現(xiàn)的,后續(xù)就不過多闡述了。
以下是實現(xiàn)動態(tài)火焰粒子方法的代碼邏輯??
/**
* 更新火焰粒子動畫
* @param effect 特效
* @param delta 時間差
* @param elapsed 經(jīng)過的時間
*/
animateFireEffect(effect: THREE.Points, delta: number, elapsed: number) {
const geometry = effect.geometry;
const posAttr = geometry.getAttribute('position');
const velAttr = geometry.getAttribute('velocity');
const startAttr = geometry.getAttribute('startTime');
const lifeAttr = geometry.getAttribute('lifetime');
const sizeAttr = geometry.getAttribute('size');
const colorAttr = geometry.getAttribute('color');
const count = posAttr.count;
for (let i = 0; i < count; i++) {
let age = elapsed - startAttr.getX(i);
// 如果粒子已超出生命周期,則重置它
if (age > lifeAttr.getX(i)) {
startAttr.setX(i, elapsed);
age = 0;
// 重置位置
posAttr.setXYZ(i, 0, 0, 0);
}
// 更新位置:p += v * dt
const vx = velAttr.getX(i);
const vy = velAttr.getY(i);
const vz = velAttr.getZ(i);
posAttr.setXYZ(
i,
posAttr.getX(i) + vx * delta,
posAttr.getY(i) + vy * delta,
posAttr.getZ(i) + vz * delta
);
// 隨年齡衰減大小
const t = age / lifeAttr.getX(i);
sizeAttr.setX(i, (1 - t) * sizeAttr.getX(i));
// 根據(jù)高度調(diào)整顏色 - 越高的粒子略微變亮
const height = posAttr.getY(i);
if (height > 0.5) {
const baseColor = new THREE.Color(
colorAttr.getX(i),
colorAttr.getY(i),
colorAttr.getZ(i)
);
const hsl: { h: number; s: number; l: number } = { h: 0, s: 0, l: 0 };
baseColor.getHSL(hsl as THREE.HSL);
const newColor = new THREE.Color().setHSL(
hsl.h,
hsl.s,
Math.min(hsl.l + height * 0.1, 0.6) // 亮度隨高度增加但有上限
);
colorAttr.setXYZ(i, newColor.r, newColor.g, newColor.b);
}
}
// 標記屬性已更新
posAttr.needsUpdate = true;
colorAttr.needsUpdate = true;
startAttr.needsUpdate = true;
sizeAttr.needsUpdate = true;
velAttr.needsUpdate = true;
}
/**
* 更新粒子動畫
*/
renderEffectAnimation() {
if (this.effectsMap.size === 0) return;
const delta = clock.getDelta();
const elapsed = clock.getElapsedTime();
/根據(jù)不同類型的粒子特效調(diào)用對應(yīng)的方法
this.effectsMap.forEach((effect) => {
switch (effect.userData.effectMethod) {
// 火焰特效
case EFFECT_METHOD.CreateFireEffect:
this.animateFireEffect(effect, delta, elapsed);
break;
// 煙霧特效
case EFFECT_METHOD.CreateSmokeEffect:
this.animateSmokeEffect(effect, delta, elapsed);
break;
// 煙花特效
case EFFECT_METHOD.CreateFireworkEffect:
this.animateFireworkEffect(effect, delta, elapsed);
break;
default:
break;
}
});
}
/**
* 場景動畫循環(huán)
*/
sceneAnimation(): void {
if (!this.controls || !this.renderer || !this.scene || !this.camera) return;
// 確保動畫循環(huán)持續(xù)進行
this.renderAnimation = requestAnimationFrame(() => this.sceneAnimation());
// 渲染粒子特效
this.renderEffectAnimation()
}
創(chuàng)建兩個顏色大小不同的火焰粒子

創(chuàng)建煙霧粒子特效方法
煙霧粒子方法的實現(xiàn)方式和火焰粒子類似,需要有兩個函數(shù)方法
- 創(chuàng)建煙霧粒子方法 createSmokeEffect
- 煙霧粒子動畫方法 animateSmokeEffect
/**
* 創(chuàng)建煙霧特效
* @param params 特效參數(shù)
*/
function createSmokeEffect(params: EffectParams) {
const { meshPosition, name, color='#888888',size=1, height=1, range=1, praticleCount=2000} = params;
// 加載煙霧紋理
const smokeTexture = textureLoader.load(smokeImage);
// 粒子系統(tǒng)參數(shù)
const PARTICLE_COUNT = particleCount;
const positions = new Float32Array(PARTICLE_COUNT * 3);
const velocities = new Float32Array(PARTICLE_COUNT * 3);
const lifetimes = new Float32Array(PARTICLE_COUNT);
const startTimes = new Float32Array(PARTICLE_COUNT);
const sizes = new Float32Array(PARTICLE_COUNT);
const colors = new Float32Array(PARTICLE_COUNT * 3);
const maxLifetime = 8.0; // 煙霧粒子最大存活時間(秒)
// 初始化粒子
for (let i = 0; i < PARTICLE_COUNT; i++) {
// 在小范圍內(nèi)隨機分布,形成煙霧源
const emitRadius = Math.random() * 0.8; // 增大初始發(fā)射半徑,從0.5到0.8
const emitAngle = Math.random() * Math.PI * 2;
positions[3 * i] = Math.cos(emitAngle) * emitRadius;
positions[3 * i + 1] = -0.2 + Math.random() * 0.2; // 略低于地面
positions[3 * i + 2] = Math.sin(emitAngle) * emitRadius;
// 速度:主要向上,帶有輕微的水平漂移
const upwardSpeed = (0.05 + Math.random() * 0.1) * height; // 上升速度
const horizontalSpeed = (0.04 + Math.random() * 0.03) * range; // 增大水平擴散速度
velocities[3 * i] = (Math.random() - 0.5) * horizontalSpeed;
velocities[3 * i + 1] = upwardSpeed;
velocities[3 * i + 2] = (Math.random() - 0.5) * horizontalSpeed;
// 生命周期,錯開時間讓煙霧連續(xù)
lifetimes[i] = maxLifetime * (0.7 + Math.random() * 0.3);
// 錯開起始時間,確保煙霧從一開始就存在
const currentTime = clock.getElapsedTime();
startTimes[i] = currentTime - Math.random() * maxLifetime;
// 初始粒子較小,應(yīng)用size參數(shù)
sizes[i] = (1.0 + Math.random() * 1.0) * size;
// 煙霧顏色 - 默認灰色
// 處理顏色參數(shù),支持十六進制字符串
let colorValue: { r: number; g: number; b: number };
if (typeof color === 'string') {
const c = new THREE.Color(color);
colorValue = { r: c.r, g: c.g, b: c.b };
} else {
colorValue = color as { r: number; g: number; b: number };
}
// 設(shè)置初始灰度
const shade = 0.8 + Math.random() * 0.2;
colors[3 * i] = colorValue.r * shade;
colors[3 * i + 1] = colorValue.g * shade;
colors[3 * i + 2] = colorValue.b * shade;
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('velocity', new THREE.BufferAttribute(velocities, 3));
geometry.setAttribute('startTime', new THREE.BufferAttribute(startTimes, 1));
geometry.setAttribute('lifetime', new THREE.BufferAttribute(lifetimes, 1));
geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({
size: 4.0 * size, // 增大基礎(chǔ)大小,從3.0到4.0
map: smokeTexture,
vertexColors: true,
transparent: true,
depthWrite: false,
opacity: 0.6, // 稍微降低不透明度,從0.7到0.6
blending: THREE.NormalBlending,
});
// 釋放紋理資源
smokeTexture.dispose();
const particles = new THREE.Points(geometry, material);
particles.name = name;
particles.userData.effectMethod = 'CreateSmokeEffect'
particles.position.copy(meshPosition);
// 添加到場景
this?.scene?.add(particles)
// 存儲到特效集合中,便于后續(xù)更新
this.effectsMap.set(particles.uuid, particles);
}
/**
* 更新煙霧粒子動畫
* @param effect 特效
* @param delta 時間差
* @param elapsed 經(jīng)過的時間
*/
animateSmokeEffect(effect: THREE.Points, delta: number, elapsed: number) {
const geometry = effect.geometry;
const posAttr = geometry.getAttribute('position');
const velAttr = geometry.getAttribute('velocity');
const startAttr = geometry.getAttribute('startTime');
const lifeAttr = geometry.getAttribute('lifetime');
const sizeAttr = geometry.getAttribute('size');
const colorAttr = geometry.getAttribute('color');
const count = posAttr.count;
for (let i = 0; i < count; i++) {
let age = elapsed - startAttr.getX(i);
const lifetime = lifeAttr.getX(i);
// 如果粒子已超出生命周期,則重置它
if (age > lifetime) {
startAttr.setX(i, elapsed);
age = 0;
// 重置位置到底部發(fā)射區(qū)
const emitRadius = Math.random() * 0.8; // 增大初始發(fā)射半徑,從0.5到0.8
const emitAngle = Math.random() * Math.PI * 2;
posAttr.setXYZ(
i,
Math.cos(emitAngle) * emitRadius,
-0.2 + Math.random() * 0.2,
Math.sin(emitAngle) * emitRadius
);
// 重置速度 - 煙霧上升較慢,但水平擴散更大
const upwardSpeed = 0.05 + Math.random() * 0.1;
const horizontalSpeed = 0.04 + Math.random() * 0.03; // 增大水平初始速度,從0.02+0.02到0.04+0.03
velAttr.setXYZ(
i,
(Math.random() - 0.5) * horizontalSpeed,
upwardSpeed,
(Math.random() - 0.5) * horizontalSpeed
);
// 重置大小
sizeAttr.setX(i, 1.0 + Math.random() * 1.0);
// 重置顏色 - 煙霧為灰色
const shade = 0.8 + Math.random() * 0.2;
colorAttr.setXYZ(i, shade, shade, shade);
}
// 更新位置:p += v * dt + 微弱擺動
const swayX = Math.sin(elapsed * 0.3 + i * 0.05) * 0.004; // 增大擺動幅度,從0.002到0.004
const swayZ = Math.cos(elapsed * 0.2 + i * 0.07) * 0.004; // 增大擺動幅度,從0.002到0.004
posAttr.setXYZ(
i,
posAttr.getX(i) + velAttr.getX(i) * delta + swayX,
posAttr.getY(i) + velAttr.getY(i) * delta,
posAttr.getZ(i) + velAttr.getZ(i) * delta + swayZ
);
// 獲取當前高度用于計算
const currentHeight = posAttr.getY(i);
const t = age / lifetime; // 生命周期進度
// 隨高度調(diào)整速度 - 越高水平擴散越大
if (currentHeight > 0.5) {
const heightFactor = Math.min(1, currentHeight / 3.0);
const horizontalSpread = 0.02 * heightFactor; // 增大擴散系數(shù),從0.01到0.02
// 向上速度隨高度降低,水平擴散增加
velAttr.setXYZ(
i,
velAttr.getX(i) + (Math.random() - 0.5) * horizontalSpread * delta,
velAttr.getY(i) * (1 - 0.02 * delta), // 減緩上升速度減少率,從0.03到0.02,讓煙霧上升更高
velAttr.getZ(i) + (Math.random() - 0.5) * horizontalSpread * delta
);
}
// 隨高度增大粒子尺寸 - 模擬擴散
const growthFactor = 1 + (0.15 + currentHeight * 0.15) * delta; // 增大生長因子,從0.1到0.15
sizeAttr.setX(i, sizeAttr.getX(i) * growthFactor);
// 隨高度和年齡調(diào)整顏色 - 煙霧越高越淡
if (currentHeight > 0.3) {
const heightRatio = Math.min(1, currentHeight / 3.0);
// 隨著高度,顏色稍微變暗
const newShade = Math.max(0.7, 0.8 - heightRatio * 0.1);
colorAttr.setXYZ(i, newShade, newShade, newShade);
}
}
// 標記屬性已更新
posAttr.needsUpdate = true;
colorAttr.needsUpdate = true;
startAttr.needsUpdate = true;
sizeAttr.needsUpdate = true;
velAttr.needsUpdate = true;
}
創(chuàng)建幾個不同顏色的煙霧粒子

創(chuàng)建煙花粒子特效方法
和上面兩個方法一樣,煙花粒子也需要兩個函數(shù)方法
1.createFireworkEffect
2.animateFireworkEffect
/**
* 創(chuàng)建煙花特效
* @param params 特效參數(shù)
*/
function createFireworkEffect(params: EffectParams) {
const { meshPosition, name,size=1, height=1, range=1, praticleCount=0} = params;
// 大幅增加粒子數(shù)量
const PARTICLE_COUNT = particleCount > 0 ? particleCount : 1200 + Math.floor(Math.random() * 500);
const positions = new Float32Array(PARTICLE_COUNT * 3);
const velocities = new Float32Array(PARTICLE_COUNT * 3);
const lifetimes = new Float32Array(PARTICLE_COUNT);
const startTimes = new Float32Array(PARTICLE_COUNT);
const sizes = new Float32Array(PARTICLE_COUNT);
const colors = new Float32Array(PARTICLE_COUNT * 3);
const originalColors = new Float32Array(PARTICLE_COUNT * 3); // 新增:原始顏色
// 保持當前的生命周期
const maxLifetime = 3.0 + Math.random() * 1.5; // 3-4.5秒
const currentTime = clock.getElapsedTime();
// 初始化粒子
for (let i = 0; i < PARTICLE_COUNT; i++) {
// 初始都放在發(fā)射點
positions[i * 3] = 0;
positions[i * 3 + 1] = 0;
positions[i * 3 + 2] = 0;
// 顯著減小速度范圍,創(chuàng)造更緊湊的爆炸效果
const theta = Math.random() * Math.PI * 2;
const phi = Math.random() * Math.PI;
const speed = (2 + Math.random() * 3) * range; // 速度范圍縮小到2-5
velocities[i * 3] = Math.sin(phi) * Math.cos(theta) * speed;
velocities[i * 3 + 1] = Math.cos(phi) * speed * height;
velocities[i * 3 + 2] = Math.sin(phi) * Math.sin(theta) * speed;
// 生命周期
lifetimes[i] = maxLifetime;
startTimes[i] = currentTime;
// 減小粒子大小
sizes[i] = 2 * size; // 從3降至2
// 使用HSL色彩空間
let r, g, b;
if (!color) {
const threeColor = new THREE.Color().setHSL(Math.random(), 1.0, 0.5);
r = threeColor.r;
g = threeColor.g;
b = threeColor.b;
} else {
// 使用指定顏色或顏色范圍
let colorValue: { r: number; g: number; b: number };
if (typeof color === 'string') {
const c = new THREE.Color(color);
colorValue = { r: c.r, g: c.g, b: c.b };
} else {
colorValue = color as { r: number; g: number; b: number };
}
r = colorValue.r;
g = colorValue.g;
b = colorValue.b;
}
// 設(shè)置當前顏色和原始顏色
colors[i * 3] = r;
colors[i * 3 + 1] = g;
colors[i * 3 + 2] = b;
originalColors[i * 3] = r;
originalColors[i * 3 + 1] = g;
originalColors[i * 3 + 2] = b;
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('velocity', new THREE.BufferAttribute(velocities, 3));
geometry.setAttribute('startTime', new THREE.BufferAttribute(startTimes, 1));
geometry.setAttribute('lifetime', new THREE.BufferAttribute(lifetimes, 1));
geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
geometry.setAttribute('originalColor', new THREE.BufferAttribute(originalColors, 3)); // 添加原始顏色屬性
const fireworksTexture = textureLoader.load(fireworksImage);
// 使用與示例相同的材質(zhì)參數(shù),但調(diào)整粒子大小
const material = new THREE.PointsMaterial({
map: fireworksTexture,
size: 2 * size, // 從3降至2
vertexColors: true,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
alphaTest: 0.01
});
fireworksTexture.dispose();
const particles = new THREE.Points(geometry, material);
particles.name = name;
particles.userData.effectMethod = 'CreateFireworkEffect'
// 設(shè)置特效位置
particles.position.copy(meshPosition);
// 添加到場景
this?.scene?.add(particles);
// 存儲到特效集合中,便于后續(xù)更新
this.effectsMap.set(particles.uuid, particles);
}
/**
* 更新煙花粒子動畫
* @param effect 特效
* @param delta 時間差
* @param elapsed 經(jīng)過的時間
*/
animateFireworkEffect(effect: THREE.Points, delta: number, elapsed: number) {
const geometry = effect.geometry;
const posAttr = geometry.getAttribute('position');
const velAttr = geometry.getAttribute('velocity');
const startAttr = geometry.getAttribute('startTime');
const lifeAttr = geometry.getAttribute('lifetime');
const colorAttr = geometry.getAttribute('color');
// 新增:原始顏色屬性,如果不存在則創(chuàng)建
let originalColorAttr = geometry.getAttribute('originalColor');
if (!originalColorAttr) {
// 如果原始顏色屬性不存在,創(chuàng)建并復(fù)制當前顏色
const originalColors = new Float32Array(colorAttr.array.length);
for (let i = 0; i < originalColors.length; i++) {
originalColors[i] = colorAttr.array[i];
}
originalColorAttr = new THREE.BufferAttribute(originalColors, 3);
geometry.setAttribute('originalColor', originalColorAttr);
}
// 保持當前的重力和速度
const GRAVITY = -4.0;
const count = posAttr.count;
for (let i = 0; i < count; i++) {
let age = elapsed - startAttr.getX(i);
const lifetime = lifeAttr.getX(i);
// 如果粒子已超出生命周期,重新設(shè)置
if (age > lifetime) {
// 重置為中心位置
posAttr.setXYZ(i, 0, 0, 0);
// 進一步減小爆炸范圍,更緊湊的效果
const theta = Math.random() * Math.PI * 2;
const phi = Math.random() * Math.PI;
// 更小的速度范圍,爆炸范圍更緊湊
const speed = 2 + Math.random() * 3;
velAttr.setXYZ(
i,
Math.sin(phi) * Math.cos(theta) * speed,
Math.cos(phi) * speed,
Math.sin(phi) * Math.sin(theta) * speed
);
// 重置生命周期和開始時間
startAttr.setX(i, elapsed);
age = 0;
// 重置顏色為隨機HSL
const color = new THREE.Color().setHSL(Math.random(), 1.0, 0.5);
colorAttr.setXYZ(i, color.r, color.g, color.b);
// 同時更新原始顏色
originalColorAttr.setXYZ(i, color.r, color.g, color.b);
}
// 應(yīng)用重力
velAttr.setY(i, velAttr.getY(i) + GRAVITY * delta);
// 保持當前的速度更新因子
const speedFactor = 0.6;
posAttr.setXYZ(
i,
posAttr.getX(i) + velAttr.getX(i) * delta * speedFactor,
posAttr.getY(i) + velAttr.getY(i) * delta * speedFactor,
posAttr.getZ(i) + velAttr.getZ(i) * delta * speedFactor
);
// 顏色周期變化 - 在生命周期內(nèi)周期性地返回到原始顏色
const lifeRatio = age / lifetime;
const colorCycle = Math.sin(lifeRatio * Math.PI * 3) * 0.5 + 0.5; // 0到1之間的正弦波,頻率為3次
// 獲取當前顏色
const currentColor = new THREE.Color(
colorAttr.getX(i),
colorAttr.getY(i),
colorAttr.getZ(i)
);
// 獲取原始顏色
const originalColor = new THREE.Color(
originalColorAttr.getX(i),
originalColorAttr.getY(i),
originalColorAttr.getZ(i)
);
// 混合當前顏色和原始顏色
const mixedColor = currentColor.clone().lerp(originalColor, colorCycle);
colorAttr.setXYZ(i, mixedColor.r, mixedColor.g, mixedColor.b);
}
// 基于生命周期調(diào)整不透明度
if (effect.material instanceof THREE.PointsMaterial) {
const maxLife = Math.max(...Array.from({length: count}, (_, i) => lifeAttr.getX(i)));
const currentMaxAge = Math.max(...Array.from({length: count}, (_, i) => elapsed - startAttr.getX(i)));
effect.material.opacity = Math.max(0, 1 - (currentMaxAge / maxLife) * 0.8);
}
// 標記屬性已更新
posAttr.needsUpdate = true;
velAttr.needsUpdate = true;
colorAttr.needsUpdate = true;
originalColorAttr.needsUpdate = true;
}
創(chuàng)建多個煙花粒子效果

結(jié)語
ok,以上就是作者個人基于 Three.js 實現(xiàn)的三種粒子特效的方法,如果你有更好的實現(xiàn)方式歡迎留言溝通
相關(guān)文章
JavaScript快速排序(quickSort)算法的實現(xiàn)方法總結(jié)
快速排序的思想式 分治法,選一個基準點,然后根據(jù)大小進行分配,分配然完畢之后,對已經(jīng)分配的進行遞歸操作,最終形成快速排序,所以遞歸也是快速排序思想的一個重要組成部分,本文主要給大家介紹了JavaScript實現(xiàn)快速排序的寫法,需要的朋友可以參考下2023-11-11
javascript間隔定時器(延時定時器)學(xué)習(xí) 間隔調(diào)用和延時調(diào)用
這篇文章主要介紹了javascript間隔調(diào)用和延時調(diào)用示例,介紹setInterval方法和clearInterval方法的使用方法,大家參考使用吧2014-01-01
詳談js中數(shù)組(array)和對象(object)的區(qū)別
下面小編就為大家?guī)硪黄斦刯s中數(shù)組(array)和對象(object)的區(qū)別。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-02-02

