Three.js 粒子特效:从基础到高级实现
引言
在 Three.js 这个强大的 3D 图形库中,粒子特效是实现各种绚丽视觉效果的重要手段之一。无论是模拟烟花、雨雪、流星等自然现象,还是创造梦幻般的背景装饰,粒子特效都能发挥巨大作用。本文将深入剖析 Three.js 中粒子特效的相关知识和实现技巧,帮助你更好地掌握这一技能。
一、粒子特效的核心概念
Three.js中的粒子特效通过粒子系统 (THREE.Points
)实现,其核心组件包括:
-
几何体(BufferGeometry)
用于存储粒子的顶点数据(位置、颜色等)。通过
Float32BufferAttribute
高效管理大量粒子的动态信息,例如在飞毯效果中,50万粒子的位置和颜色数据通过数组初始化并绑定到几何体属性。 -
材质(PointsMaterial)
控制粒子的视觉表现,支持以下关键参数:
-
size
:粒子大小(默认单位像素),结合sizeAttenuation
属性可控制粒子随距离变化的透视效果。 -
alphaMap
:透明贴图,用于实现粒子边缘渐变或发光效果(如使用PNG纹理)。 -
vertexColors
:启用后允许每个粒子独立设置颜色,适用于星空或彩虹效果。
-
-
动画插值
通过
requestAnimationFrame
逐帧更新粒子位置,配合数学函数(如正弦波)或物理模拟实现动态效果。例如爱心特效中,粒子通过线性插值(lerp)在随机分散与目标形状(心形/数字)间平滑过渡。
二、基础粒子系统实现
1.1 Points 核心类
Three.js 在 r125+ 版本后使用 Points
类替代旧的 ParticleSystem
:
const particles = new THREE.BufferGeometry();
const count = 10000;
const positions = new Float32Array(count * 3);
// 初始化随机位置
for (let i = 0; i < count * 3; i += 3) {
positions[i] = (Math.random() - 0.5) * 10;
positions[i+1] = (Math.random() - 0.5) * 10;
positions[i+2] = (Math.random() - 0.5) * 10;
}
particles.setAttribute('position', new THREE.BufferAttribute(positions, 3));
1.2 材质配置要点
PointsMaterial
的关键参数:
const material = new THREE.PointsMaterial({
color: 0x00ff00,//颜色
size: 0.1,//大小
transparent: true,//是否透明
opacity: 0.8,//如果transparent为true,设置透明度
sizeAttenuation: true // 透视衰减
});
1.3 完整创建流程
const particleSystem = new THREE.Points(particles, material);
scene.add(particleSystem);
三、动态粒子效果
2.1 CPU 动画实现
function animate() {
const positions = particleSystem.geometry.attributes.position.array;
for (let i = 0; i < positions.length; i += 3) {
positions[i] += Math.random() * 0.01 - 0.005;
positions[i+1] += Math.random() * 0.01 - 0.005;
positions[i+2] += Math.random() * 0.01 - 0.005;
}
particleSystem.geometry.attributes.position.needsUpdate = true;
requestAnimationFrame(animate);
}
2.2 性能优化技巧
-
使用
Float32Array
代替常规数组 -
批量更新替代逐粒子更新
-
启用几何体复用:
const baseGeometry = new THREE.BufferGeometry();
// 初始化基础几何体...function createParticleSystem(params) {
return new THREE.Points(baseGeometry.clone(), new PointsMaterial(params));
}
四、高级着色器特效
3.1 自定义着色器结构
// vertexShader
varying vec3 vPosition;
void main() {
vPosition = position;
gl_PointSize = 8.0;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
// fragmentShader
varying vec3 vPosition;
void main() {
float distance = length(gl_PointCoord - vec2(0.5));
if (distance > 0.5) discard;
vec3 color = mix(vec3(1.0,0,0), vec3(0,0,1.0), vPosition.z * 0.5 + 0.5);
gl_FragColor = vec4(color, 1.0 - distance * 2.0);
}
3.2 着色器动画示例
// 添加时间uniform
uniform float uTime;
void main() {
vec3 animatedPos = position + vec3(
sin(position.x * 10.0 + uTime) * 0.2,
cos(position.y * 8.0 + uTime) * 0.2,
0
);
// 后续变换...
}
四、性能深度优化
优化策略 | CPU 占用降低 | GPU 占用降低 |
---|---|---|
实例化渲染 | 40% | 35% |
LOD 分级 | 25% | 30% |
空间分区 | 30% | 20% |
着色器优化 | 15% | 40% |
关键优化技术:
-
使用
THREE.InstancedBufferGeometry
实现实例化 -
基于距离的细节层级控制:
function updateLOD() {
const distance = camera.position.distanceTo(particleSystem.position);
particleSystem.material.size = THREE.MathUtils.lerp(
0.1, 2.0, 1.0 - Math.min(distance / 100, 1)
);
}
五、实战应用案例
5.1 星空背景
const starCount = 5000;
const starPositions = new Float32Array(starCount * 3);
for (let i = 0; i < starCount; i++) {
const i3 = i * 3;
starPositions[i3] = (Math.random() - 0.5) * 200;
starPositions[i3 + 1] = (Math.random() - 0.5) * 200;
starPositions[i3 + 2] = (Math.random() - 0.5) * 200;
}
const starGeometry = new THREE.BufferGeometry();
starGeometry.setAttribute('position', new THREE.BufferAttribute(starPositions, 3));
const starMaterial = new THREE.PointsMaterial({
size: 0.05,
color: 0xffffff
});
const stars = new THREE.Points(starGeometry, starMaterial);
scene.add(stars);
5.2 星空背景增强版
function createStarField() {
const vertices = new Float32Array(20000 * 3);
// 生成球状分布
const radius = 100;
for (let i = 0; i < 20000; i++) {
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos((Math.random() * 2) - 1);
const r = radius * Math.cbrt(Math.random());
vertices[i*3] = r * Math.sin(phi) * Math.cos(theta);
vertices[i*3+1] = r * Math.sin(phi) * Math.sin(theta);
vertices[i*3+2] = r * Math.cos(phi);
}
// 创建渐变材质...
}
5.3 交互式粒子系统
document.addEventListener('mousemove', (e) => {
const mouse = new THREE.Vector2(
(e.clientX / window.innerWidth) * 2 - 1,
-(e.clientY / window.innerHeight) * 2 + 1
);
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObject(particleSystem);
if (intersects.length > 0) {
const index = intersects[0].index * 3;
// 修改选中粒子的属性...
}
});
5.4 烟花效果
// 创建场景
const scene = new THREE.Scene();
// 创建相机
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;
// 创建渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);
// 存储烟花粒子系统的数组
const fireworks = [];
// 创建烟花
function createFirework() {
const color = new THREE.Color(Math.random(), Math.random(), Math.random());
const position = new THREE.Vector3(
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10,
0
);
const particleCount = 100;
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
const colors = new Float32Array(particleCount * 3);
for (let i = 0; i < particleCount; i++) {
const i3 = i * 3;
positions[i3] = position.x;
positions[i3 + 1] = position.y;
positions[i3 + 2] = position.z;
colors[i3] = color.r;
colors[i3 + 1] = color.g;
colors[i3 + 2] = color.b;
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({
size: 0.1,
vertexColors: true
});
const particles = new THREE.Points(geometry, material);
scene.add(particles);
fireworks.push({
particles,
velocities: new Array(particleCount).fill().map(() => {
const speed = Math.random() * 0.1;
const angle = Math.random() * Math.PI * 2;
return new THREE.Vector3(
Math.cos(angle) * speed,
Math.sin(angle) * speed,
0
);
}),
life: 100
});
}
// 更新烟花状态
function updateFireworks() {
for (let i = fireworks.length - 1; i >= 0; i--) {
const firework = fireworks[i];
const positions = firework.particles.geometry.attributes.position.array;
for (let j = 0; j < firework.velocities.length; j++) {
const j3 = j * 3;
positions[j3] += firework.velocities[j].x;
positions[j3 + 1] += firework.velocities[j].y;
positions[j3 + 2] += firework.velocities[j].z;
}
firework.particles.geometry.attributes.position.needsUpdate = true;
firework.life--;
if (firework.life <= 0) {
scene.remove(firework.particles);
fireworks.splice(i, 1);
}
}
}
// 动画循环
function animate() {
requestAnimationFrame(animate);
if (Math.random() < 0.01) {
createFirework();
}
updateFireworks();
renderer.render(scene, camera);
}
animate();
5.5 雨雪效果
// 创建场景
const scene = new THREE.Scene();
// 创建相机
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;
// 创建渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);
// 粒子数量,可根据雨雪强度调整
const particleCount = 1000;
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
const velocities = new Float32Array(particleCount * 3);
const sizes = new Float32Array(particleCount);
for (let i = 0; i < particleCount; i++) {
const i3 = i * 3;
// 随机初始化粒子位置
positions[i3] = (Math.random() - 0.5) * 20;
positions[i3 + 1] = Math.random() * 10;
positions[i3 + 2] = (Math.random() - 0.5) * 20;
// 初始化粒子速度
velocities[i3] = (Math.random() - 0.5) * 0.01; // 水平随机摆动
velocities[i3 + 1] = -Math.random() * 0.05; // 垂直下落速度
velocities[i3 + 2] = (Math.random() - 0.5) * 0.01;
// 根据雨雪强度调整粒子大小
sizes[i] = Math.random() * 0.1 + 0.05;
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('customVelocity', new THREE.BufferAttribute(velocities, 3));
geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
const material = new THREE.PointsMaterial({
color: 0xffffff,
sizeAttenuation: true,
transparent: true,
opacity: 0.8 // 透明度,可根据雨雪强度调整
});
const particles = new THREE.Points(geometry, material);
scene.add(particles);
// 动画循环
function animate() {
requestAnimationFrame(animate);
const positions = particles.geometry.attributes.position.array;
const velocities = particles.geometry.attributes.customVelocity.array;
for (let i = 0; i < particleCount; i++) {
const i3 = i * 3;
positions[i3] += velocities[i3];
positions[i3 + 1] += velocities[i3 + 1];
positions[i3 + 2] += velocities[i3 + 2];
// 如果粒子超出底部,重新初始化位置到顶部
if (positions[i3 + 1] < -5) {
positions[i3] = (Math.random() - 0.5) * 20;
positions[i3 + 1] = Math.random() * 10;
positions[i3 + 2] = (Math.random() - 0.5) * 20;
}
}
particles.geometry.attributes.position.needsUpdate = true;
renderer.render(scene, camera);
}
animate();
六、调试与问题排查
-
内存泄漏检测
// 监控GPU内存
const memInfo = renderer.getMemoryInfo();
console.log(GPU Memory: ${memInfo.geometries} geometries, ${memInfo.textures} textures
); -
常见问题处理
- 闪烁问题 :启用深度测试
material.depthTest = true
- 尺寸异常 :检查
sizeAttenuation
和透视相机设置 - 性能骤降 :使用
stats.js
监控FPS和绘制调用次数
总之,Three.js 中的粒子特效功能强大且灵活多样。掌握粒子系统的创建、属性设置和动画技巧,以及性能优化方法,能够让你在 3D 开发中创造出令人惊叹的视觉效果,为你的项目增添独特的魅力。无论是游戏开发、动画制作还是可视化展示,粒子特效都是一个不可或缺的工具,值得深入学习和探索。