前言
都说 3D(Three.js ,Babylon.js ) 是前端最后的护城河,为什么这么说呢?因为随着AI编码工具(Cursor ,Trae )和当今主流框架(React ,Vue)的生态发展成熟前端的门槛似乎被拉的更低了,因为通过AI编码工具许多非前端岗位的开发者也可以很轻松的去实现一些门户页面和后台管理系统了。
而3D前端(Three.js ,Babylon.js)之所以是前端最后的护城河,我想更多是因为3D相关开发的内容有更高的门槛和更高的难度吧。
本篇给大家分享一下如何使用Three.js去实现火焰,烟雾,烟花这三个粒子特效

涉及的Three.js核心API方法介绍
1.THREE.PointsMaterial
THREE.PointsMaterial
是一种专门用于 点渲染 的材质(类似于 MeshBasicMaterial
是网格的材质)。它定义了每一个点的外观,比如大小、颜色、是否透明等
2.THREE.Points
THREE.Points
是继承自 THREE.Object3D
的渲染对象,用于 将几何体中的所有顶点作为单个点渲染出来 ,搭配 PointsMaterial
使用。
3.THREE.BufferGeometry
是一种通过 缓冲区(Buffer)存储顶点数据 的几何体结构,用于高效地构建和管理顶点数据
创建火焰粒子特效方法
这里将创建火焰特效的方法逻辑进行单独抽离,方法接收以下几个参数,方便我们创建不同效果的火焰粒子
1.meshPosition 粒子位置
2.name 粒子名称
3.color 粒子颜色
4.size 粒子大小
5.height 粒子高度
6.range 粒子范围
7.praticleCount 粒子数量
hexToHSL
函数方法是用于将十六进制的颜色值转化为HSL
对象
js
/**
* 将十六进制颜色字符串转换为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 };
}
/**
* 创建火焰特效
* @param params 特效参数
* meshPosition 粒子位置
* name 粒子名称
* color 粒子颜色
* size 粒子大小
* height 粒子高度
* range 粒子范围
* praticleCount 粒子数量
*/
function createFireEffect(params: EffectParams) {
const { meshPosition, name, color,size=1, height=1, range=1, praticleCount=1} = params;
// 处理颜色参数,将十六进制颜色值转化为 HSL 格式
const color =this.hexToHSL(color)
// 加载火焰贴图
const fireTexture = textureLoader.load(fireImage);
// 粒子系统参数
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++) {
// 初始都放在发射点
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; // 会在动画中重置
// 底部火焰更小,应用size参数
sizes[i] = (Math.random() * 0.2 + 0.2) * size;
// 应用自定义颜色
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'
// 设置特效位置
particles.position.copy(meshPosition);
// 添加到场景
this?.scene?.add(particles)
// 存储到火焰特效集合中,便于后续更新
this.effectsMap.set(particles.uuid, particles);
}
好了,这样一个创建粒子火焰效果的方法就写好了

但这个时候的火焰效果是静态的,所有的点材质内容的位置都是固定的,这里为了实现一个动态的火焰效果,我们就需要通过 requestAnimationFrame
动画循环的方式不间断的更新点材质 的位置,颜色等参数值,从而实现一个动态的火焰粒子效果。
后面的烟雾粒子 和烟花粒子的实现逻辑方式也是类似,都是通过不间断的去修改点材质的位置和颜色等值来实现的,后续就不过多阐述了。
以下是实现动态火焰粒子方法的代码逻辑👇
js
/**
* 更新火焰粒子动画
* @param effect 特效
* @param delta 时间差
* @param elapsed 经过的时间
*/
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));
// 根据高度调整颜色 - 越高的粒子略微变亮
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();
/根据不同类型的粒子特效调用对应的方法
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;
}
});
}
/**
* 场景动画循环
*/
sceneAnimation(): void {
if (!this.controls || !this.renderer || !this.scene || !this.camera) return;
// 确保动画循环持续进行
this.renderAnimation = requestAnimationFrame(() => this.sceneAnimation());
// 渲染粒子特效
this.renderEffectAnimation()
}
创建两个颜色大小不同的火焰粒子

创建烟雾粒子特效方法
烟雾粒子方法的实现方式和火焰粒子类似,需要有两个函数方法
- 创建烟雾粒子方法 createSmokeEffect
- 烟雾粒子动画方法 animateSmokeEffect
js
/**
* 创建烟雾特效
* @param params 特效参数
*/
function createSmokeEffect(params: EffectParams) {
const { meshPosition, name, color='#888888',size=1, height=1, range=1, praticleCount=2000} = params;
// 加载烟雾纹理
const smokeTexture = textureLoader.load(smokeImage);
// 粒子系统参数
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++) {
// 在小范围内随机分布,形成烟雾源
const emitRadius = Math.random() * 0.8; // 增大初始发射半径,从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;
// 生命周期,错开时间让烟雾连续
lifetimes[i] = maxLifetime * (0.7 + Math.random() * 0.3);
// 错开起始时间,确保烟雾从一开始就存在
const currentTime = clock.getElapsedTime();
startTimes[i] = currentTime - Math.random() * maxLifetime;
// 初始粒子较小,应用size参数
sizes[i] = (1.0 + Math.random() * 1.0) * size;
// 烟雾颜色 - 默认灰色
// 处理颜色参数,支持十六进制字符串
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 };
}
// 设置初始灰度
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, // 增大基础大小,从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)
// 存储到特效集合中,便于后续更新
this.effectsMap.set(particles.uuid, particles);
}
/**
* 更新烟雾粒子动画
* @param effect 特效
* @param delta 时间差
* @param elapsed 经过的时间
*/
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;
// 重置位置到底部发射区
const emitRadius = Math.random() * 0.8; // 增大初始发射半径,从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; // 生命周期进度
// 随高度调整速度 - 越高水平扩散越大
if (currentHeight > 0.5) {
const heightFactor = Math.min(1, currentHeight / 3.0);
const horizontalSpread = 0.02 * heightFactor; // 增大扩散系数,从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);
// 随高度和年龄调整颜色 - 烟雾越高越淡
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;
}
创建几个不同颜色的烟雾粒子

创建烟花粒子特效方法
和上面两个方法一样,烟花粒子也需要两个函数方法
1.createFireworkEffect
2.animateFireworkEffect
js
/**
* 创建烟花特效
* @param params 特效参数
*/
function createFireworkEffect(params: EffectParams) {
const { meshPosition, name,size=1, height=1, range=1, praticleCount=0} = params;
// 大幅增加粒子数量
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++) {
// 初始都放在发射点
positions[i * 3] = 0;
positions[i * 3 + 1] = 0;
positions[i * 3 + 2] = 0;
// 显著减小速度范围,创造更紧凑的爆炸效果
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;
}
// 设置当前颜色和原始颜色
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);
// 使用与示例相同的材质参数,但调整粒子大小
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'
// 设置特效位置
particles.position.copy(meshPosition);
// 添加到场景
this?.scene?.add(particles);
// 存储到特效集合中,便于后续更新
this.effectsMap.set(particles.uuid, particles);
}
/**
* 更新烟花粒子动画
* @param effect 特效
* @param delta 时间差
* @param elapsed 经过的时间
*/
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');
// 新增:原始颜色属性,如果不存在则创建
let originalColorAttr = geometry.getAttribute('originalColor');
if (!originalColorAttr) {
// 如果原始颜色属性不存在,创建并复制当前颜色
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);
// 如果粒子已超出生命周期,重新设置
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);
}
// 应用重力
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
);
// 颜色周期变化 - 在生命周期内周期性地返回到原始颜色
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);
}
// 基于生命周期调整不透明度
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;
}
创建多个烟花粒子效果

结语
ok,以上就是作者个人基于 Three.js 实现的三种粒子特效的方法,如果你有更好的实现方式欢迎留言沟通