粒子系统
粒子系统用于创建大量小型对象的模拟效果,如雨、雪、火焰、烟雾、星空、爆炸等。Three.js 提供了 THREE.Points 类用于高效渲染大量粒子。
系统架构
code
┌─────────────────────────────────────────────────────────────────────────┐
│ 粒子系统架构 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ 粒子数据管理 │ │ 几何体配置 │ │ 材质配置 │ │
│ │ │ │ │ │ │ │
│ │ • Positions │ │ BufferGeometry │ │ PointsMaterial │ │
│ │ • Colors │───▶│ - position │───▶│ - color │ │
│ │ • Sizes │ │ - color │ │ - size │ │
│ │ • Velocities │ │ - size │ │ - map │ │
│ │ • Lifetimes │ │ │ │ - transparent │ │
│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ THREE.Points │ │
│ │ │ │
│ │ 高效批量渲染 │ │
│ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
渲染流程:
粒子数据 → BufferGeometry → PointsMaterial → THREE.Points → 渲染器核心组件说明
| 组件 | 说明 | 职责 |
|---|---|---|
| THREE.Points | 粒子容器 | 管理粒子对象的渲染 |
| BufferGeometry | 几何体 | 存储粒子位置、颜色、大小等属性 |
| PointsMaterial | 粒子材质 | 控制粒子外观(颜色、大小、纹理) |
| ShaderMaterial | 自定义材质 | 实现复杂的粒子效果 |
| BufferAttribute | 缓冲属性 | 存储具体的粒子数据 |
概述
Three.js 的 THREE.Points 类是专门用于渲染大量粒子的优化方案。与单独创建网格相比,粒子系统有以下优势:
| 特性 | 传统网格 | 粒子系统 |
|---|---|---|
| 渲染调用 | 每个对象一次 | 全部一次 |
| 内存占用 | 高 | 低 |
| 适用数量 | 少量对象 | 数千至百万 |
| 灵活性 | 高 | 中等 |
| 性能 | 低 | 高 |
适用场景
- 自然现象:雨、雪、落叶、火焰、烟雾
- 视觉效果:爆炸、魔法、星空、银河
- 数据可视化:散点图、点云、流体模拟
- 游戏特效:技能效果、粒子武器、环境粒子
Points 基础
创建基础粒子系统
javascript
import * as THREE from 'three';
// ==================== 创建粒子几何体 ====================
const particleCount = 1000;
const geometry = new THREE.BufferGeometry();
// 位置数组(每个粒子 3 个值:x, y, z)
const positions = new Float32Array(particleCount * 3);
for (let i = 0; i < particleCount; i++) {
positions[i * 3] = (Math.random() - 0.5) * 20; // x
positions[i * 3 + 1] = (Math.random() - 0.5) * 20; // y
positions[i * 3 + 2] = (Math.random() - 0.5) * 20; // z
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
// ==================== 创建粒子材质 ====================
const material = new THREE.PointsMaterial({
color: 0xffffff,
size: 0.1,
sizeAttenuation: true // 是否随距离衰减大小
});
// ==================== 创建粒子系统 ====================
const particles = new THREE.Points(geometry, material);
scene.add(particles);
// ==================== 基本动画 ====================
function animate() {
requestAnimationFrame(animate);
// 旋转粒子系统
particles.rotation.y += 0.001;
renderer.render(scene, camera);
}粒子颜色
javascript
// 为每个粒子设置独立颜色
const particleCount = 1000;
const geometry = new THREE.BufferGeometry();
// 位置
const positions = new Float32Array(particleCount * 3);
// 颜色(RGB)
const colors = new Float32Array(particleCount * 3);
for (let i = 0; i < particleCount; i++) {
// 随机位置
positions[i * 3] = (Math.random() - 0.5) * 20;
positions[i * 3 + 1] = (Math.random() - 0.5) * 20;
positions[i * 3 + 2] = (Math.random() - 0.5) * 20;
// 随机颜色(HSL 更容易控制)
const color = new THREE.Color();
color.setHSL(Math.random(), 1.0, 0.5); // 随机色相,饱和度100%,亮度50%
colors[i * 3] = color.r; // R
colors[i * 3 + 1] = color.g; // G
colors[i * 3 + 2] = color.b; // B
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
// 材质启用顶点颜色
const material = new THREE.PointsMaterial({
size: 0.2,
vertexColors: true, // 使用顶点颜色
transparent: true,
opacity: 0.8
});
const particles = new THREE.Points(geometry, material);
scene.add(particles);渐变颜色
javascript
// 基于位置或属性的颜色渐变
function setGradientColors(positions, colors, particleCount) {
for (let i = 0; i < particleCount; i++) {
const y = positions[i * 3 + 1];
// 基于高度的渐变
const t = (y + 10) / 20; // 归一化到 0-1
const color = new THREE.Color();
// 从蓝色渐变到红色
color.setHSL(0.6 - t * 0.6, 1.0, 0.5);
colors[i * 3] = color.r;
colors[i * 3 + 1] = color.g;
colors[i * 3 + 2] = color.b;
}
}粒子大小
使用 PointsMaterial 时所有粒子大小相同,使用 ShaderMaterial 可实现不同大小:
javascript
// 方法一:使用 ShaderMaterial
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
const sizes = new Float32Array(particleCount);
for (let i = 0; i < particleCount; i++) {
positions[i * 3] = (Math.random() - 0.5) * 20;
positions[i * 3 + 1] = (Math.random() - 0.5) * 20;
positions[i * 3 + 2] = (Math.random() - 0.5) * 20;
sizes[i] = Math.random() * 0.5 + 0.1; // 随机大小
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
// 自定义着色器材质
const material = new THREE.ShaderMaterial({
uniforms: {
color: { value: new THREE.Color(0xffffff) },
pointTexture: { value: textureLoader.load('particle.png') }
},
vertexShader: `
attribute float size;
varying vec3 vColor;
void main() {
vColor = vec3(1.0);
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
// 根据距离调整大小
gl_PointSize = size * (300.0 / -mvPosition.z);
gl_Position = projectionMatrix * mvPosition;
}
`,
fragmentShader: `
uniform vec3 color;
uniform sampler2D pointTexture;
varying vec3 vColor;
void main() {
vec4 texColor = texture2D(pointTexture, gl_PointCoord);
gl_FragColor = vec4(color * vColor, 1.0) * texColor;
}
`,
transparent: true,
depthWrite: false
});
const particles = new THREE.Points(geometry, material);
scene.add(particles);粒子纹理
使用纹理贴图
javascript
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load('particle.png');
const material = new THREE.PointsMaterial({
size: 0.5,
map: texture,
transparent: true,
alphaTest: 0.5, // Alpha 测试阈值
depthWrite: false, // 避免深度冲突
blending: THREE.AdditiveBlending // 叠加混合
});
const particles = new THREE.Points(geometry, material);
scene.add(particles);纹理加载选项
javascript
const texture = textureLoader.load('particle.png',
// 加载完成回调
(texture) => {
texture.minFilter = THREE.LinearFilter;
texture.magFilter = THREE.LinearFilter;
texture.wrapS = THREE.ClampToEdgeWrapping;
texture.wrapT = THREE.ClampToEdgeWrapping;
material.map = texture;
material.needsUpdate = true;
},
// 进度回调
undefined,
// 错误回调
(error) => {
console.error('纹理加载失败:', error);
}
);使用 Canvas 创建纹理
javascript
// 创建粒子纹理(圆形渐变)
function createParticleTexture() {
const canvas = document.createElement('canvas');
canvas.width = 64;
canvas.height = 64;
const ctx = canvas.getContext('2d');
// 创建径向渐变
const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
gradient.addColorStop(0, 'rgba(255, 255, 255, 1)');
gradient.addColorStop(0.3, 'rgba(255, 255, 255, 0.8)');
gradient.addColorStop(0.7, 'rgba(255, 255, 255, 0.3)');
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 64, 64);
return new THREE.CanvasTexture(canvas);
}
// 创建星形纹理
function createStarTexture() {
const canvas = document.createElement('canvas');
canvas.width = 64;
canvas.height = 64;
const ctx = canvas.getContext('2d');
const cx = 32, cy = 32;
const spikes = 4;
const outerRadius = 30;
const innerRadius = 10;
ctx.beginPath();
ctx.moveTo(cx, cy - outerRadius);
for (let i = 0; i < spikes * 2; i++) {
const radius = i % 2 === 0 ? outerRadius : innerRadius;
const angle = (i * Math.PI) / spikes - Math.PI / 2;
ctx.lineTo(
cx + Math.cos(angle) * radius,
cy + Math.sin(angle) * radius
);
}
ctx.closePath();
ctx.fillStyle = 'white';
ctx.fill();
return new THREE.CanvasTexture(canvas);
}
// 创建火花纹理
function createSparkTexture() {
const canvas = document.createElement('canvas');
canvas.width = 64;
canvas.height = 64;
const ctx = canvas.getContext('2d');
// 绘制椭圆形火花
const gradient = ctx.createRadialGradient(32, 32, 0, 32, 16, 32);
gradient.addColorStop(0, 'rgba(255, 255, 255, 1)');
gradient.addColorStop(0.5, 'rgba(255, 200, 100, 0.5)');
gradient.addColorStop(1, 'rgba(255, 100, 50, 0)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 64, 64);
return new THREE.CanvasTexture(canvas);
}精灵纹理
javascript
// 使用 Sprite 格式的纹理(自动旋转)
const material = new THREE.PointsMaterial({
size: 0.5,
map: texture,
transparent: true,
sizeAttenuation: true,
// 精灵不会随相机旋转(适用于 2D 风格)
});粒子动画
基础动画
javascript
// 简单的粒子下落动画
const particleCount = 1000;
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
const velocities = [];
for (let i = 0; i < particleCount; i++) {
positions[i * 3] = (Math.random() - 0.5) * 20;
positions[i * 3 + 1] = Math.random() * 20;
positions[i * 3 + 2] = (Math.random() - 0.5) * 20;
// 存储速度
velocities.push({
x: (Math.random() - 0.5) * 0.02,
y: -Math.random() * 0.02 - 0.01, // 向下
z: (Math.random() - 0.5) * 0.02
});
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const material = new THREE.PointsMaterial({
color: 0xffffff,
size: 0.1,
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;
for (let i = 0; i < particleCount; i++) {
// 更新位置
positions[i * 3] += velocities[i].x;
positions[i * 3 + 1] += velocities[i].y;
positions[i * 3 + 2] += velocities[i].z;
// 重置超出边界的粒子
if (positions[i * 3 + 1] < -10) {
positions[i * 3 + 1] = 10;
positions[i * 3] = (Math.random() - 0.5) * 20;
positions[i * 3 + 2] = (Math.random() - 0.5) * 20;
}
}
// 标记需要更新
particles.geometry.attributes.position.needsUpdate = true;
renderer.render(scene, camera);
}雪花效果
javascript
function createSnowfall() {
const particleCount = 5000;
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
const velocities = [];
const sizes = new Float32Array(particleCount);
for (let i = 0; i < particleCount; i++) {
// 初始位置
positions[i * 3] = (Math.random() - 0.5) * 100;
positions[i * 3 + 1] = Math.random() * 50;
positions[i * 3 + 2] = (Math.random() - 0.5) * 100;
// 随机速度和飘动
velocities.push({
x: (Math.random() - 0.5) * 0.05,
y: -0.05 - Math.random() * 0.05,
z: (Math.random() - 0.5) * 0.05,
// 飘动参数
amplitudeX: Math.random() * 0.05,
amplitudeZ: Math.random() * 0.05,
frequency: Math.random() * 2 + 1,
phase: Math.random() * Math.PI * 2
});
sizes[i] = Math.random() * 0.3 + 0.1;
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
// 雪花纹理
const texture = createParticleTexture();
const material = new THREE.PointsMaterial({
color: 0xffffff,
size: 0.2,
map: texture,
transparent: true,
opacity: 0.8,
depthWrite: false
});
const snow = new THREE.Points(geometry, material);
scene.add(snow);
return { particles: snow, velocities };
}
// 更新雪花
let time = 0;
function updateSnow(particles, velocities) {
time += 0.016;
const positions = particles.geometry.attributes.position.array;
for (let i = 0; i < positions.length / 3; i++) {
const vel = velocities[i];
// 基础移动
positions[i * 3] += vel.x;
positions[i * 3 + 1] += vel.y;
positions[i * 3 + 2] += vel.z;
// 飘动效果
positions[i * 3] += Math.sin(time * vel.frequency + vel.phase) * vel.amplitudeX;
positions[i * 3 + 2] += Math.cos(time * vel.frequency + vel.phase) * vel.amplitudeZ;
// 重置
if (positions[i * 3 + 1] < 0) {
positions[i * 3 + 1] = 50;
positions[i * 3] = (Math.random() - 0.5) * 100;
positions[i * 3 + 2] = (Math.random() - 0.5) * 100;
}
}
particles.geometry.attributes.position.needsUpdate = true;
}火焰效果
javascript
function createFire(position = new THREE.Vector3(0, 0, 0)) {
const particleCount = 2000;
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
const colors = new Float32Array(particleCount * 3);
const sizes = new Float32Array(particleCount);
const lifetimes = [];
// 初始化粒子
for (let i = 0; i < particleCount; i++) {
resetFireParticle(i, positions, colors, sizes, lifetimes, position);
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
const texture = createParticleTexture();
const material = new THREE.PointsMaterial({
size: 0.5,
vertexColors: true,
map: texture,
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false
});
const fire = new THREE.Points(geometry, material);
fire.userData = { lifetimes, position };
return fire;
}
function resetFireParticle(i, positions, colors, sizes, lifetimes, position) {
// 位置:从底部中心发出
const angle = Math.random() * Math.PI * 2;
const radius = Math.random() * 0.5;
positions[i * 3] = position.x + Math.cos(angle) * radius;
positions[i * 3 + 1] = position.y;
positions[i * 3 + 2] = position.z + Math.sin(angle) * radius;
// 颜色:从黄色到红色到暗红色
const t = Math.random();
colors[i * 3] = 1.0; // R
colors[i * 3 + 1] = t * 0.8; // G
colors[i * 3 + 2] = 0; // B
// 大小
sizes[i] = Math.random() * 0.5 + 0.5;
// 生命周期
lifetimes[i] = {
life: Math.random() * 2,
maxLife: 2,
speed: 1 + Math.random() * 2,
turbulence: Math.random() * 0.5
};
}
function updateFire(fire, deltaTime) {
const positions = fire.geometry.attributes.position.array;
const colors = fire.geometry.attributes.color.array;
const sizes = fire.geometry.attributes.size.array;
const lifetimes = fire.userData.lifetimes;
const position = fire.userData.position;
for (let i = 0; i < positions.length / 3; i++) {
const lifetime = lifetimes[i];
lifetime.life -= deltaTime;
if (lifetime.life <= 0) {
resetFireParticle(i, positions, colors, sizes, lifetimes, position);
} else {
// 上升
positions[i * 3 + 1] += lifetime.speed * deltaTime;
// 湍流
positions[i * 3] += (Math.random() - 0.5) * lifetime.turbulence * deltaTime;
positions[i * 3 + 2] += (Math.random() - 0.5) * lifetime.turbulence * deltaTime;
// 颜色变化:越往上越暗
const alpha = lifetime.life / lifetime.maxLife;
colors[i * 3] *= 0.99;
colors[i * 3 + 1] *= 0.98;
// 大小衰减
sizes[i] *= 0.995;
}
}
fire.geometry.attributes.position.needsUpdate = true;
fire.geometry.attributes.color.needsUpdate = true;
fire.geometry.attributes.size.needsUpdate = true;
}爆炸效果
javascript
function createExplosion(position = new THREE.Vector3(0, 0, 0)) {
const particleCount = 500;
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
const velocities = new Float32Array(particleCount * 3);
const colors = new Float32Array(particleCount * 3);
const lifetimes = new Float32Array(particleCount);
for (let i = 0; i < particleCount; i++) {
// 从中心点开始
positions[i * 3] = position.x;
positions[i * 3 + 1] = position.y;
positions[i * 3 + 2] = position.z;
// 随机方向的速度
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(Math.random() * 2 - 1);
const speed = 5 + Math.random() * 10;
velocities[i * 3] = Math.sin(phi) * Math.cos(theta) * speed;
velocities[i * 3 + 1] = Math.sin(phi) * Math.sin(theta) * speed;
velocities[i * 3 + 2] = Math.cos(phi) * speed;
// 火焰颜色
colors[i * 3] = 1;
colors[i * 3 + 1] = Math.random() * 0.5 + 0.5;
colors[i * 3 + 2] = 0;
lifetimes[i] = 1 + Math.random();
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const texture = createParticleTexture();
const material = new THREE.PointsMaterial({
size: 0.5,
vertexColors: true,
map: texture,
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false
});
const explosion = new THREE.Points(geometry, material);
explosion.userData = { velocities, lifetimes, age: 0 };
return explosion;
}
function updateExplosion(explosion, deltaTime) {
const positions = explosion.geometry.attributes.position.array;
const colors = explosion.geometry.attributes.color.array;
const velocities = explosion.userData.velocities;
const lifetimes = explosion.userData.lifetimes;
explosion.userData.age += deltaTime;
for (let i = 0; i < positions.length / 3; i++) {
// 应用速度
positions[i * 3] += velocities[i * 3] * deltaTime;
positions[i * 3 + 1] += velocities[i * 3 + 1] * deltaTime;
positions[i * 3 + 2] += velocities[i * 3 + 2] * deltaTime;
// 重力
velocities[i * 3 + 1] -= 9.8 * deltaTime;
// 阻力
velocities[i * 3] *= 0.99;
velocities[i * 3 + 1] *= 0.99;
velocities[i * 3 + 2] *= 0.99;
// 颜色衰减
colors[i * 3] *= 0.98;
colors[i * 3 + 1] *= 0.95;
}
explosion.geometry.attributes.position.needsUpdate = true;
explosion.geometry.attributes.color.needsUpdate = true;
// 检查是否完成
return explosion.userData.age > 3;
}烟雾效果
javascript
function createSmoke(position = new THREE.Vector3(0, 0, 0)) {
const particleCount = 300;
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
const colors = new Float32Array(particleCount * 3);
const sizes = new Float32Array(particleCount);
const lifetimes = [];
for (let i = 0; i < particleCount; i++) {
resetSmokeParticle(i, positions, colors, sizes, lifetimes, position);
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
const texture = createParticleTexture();
const material = new THREE.PointsMaterial({
size: 2,
vertexColors: true,
map: texture,
transparent: true,
opacity: 0.5,
depthWrite: false,
blending: THREE.NormalBlending
});
const smoke = new THREE.Points(geometry, material);
smoke.userData = { lifetimes, position };
return smoke;
}
function resetSmokeParticle(i, positions, colors, sizes, lifetimes, position) {
positions[i * 3] = position.x + (Math.random() - 0.5) * 0.5;
positions[i * 3 + 1] = position.y;
positions[i * 3 + 2] = position.z + (Math.random() - 0.5) * 0.5;
// 灰色系
const gray = 0.3 + Math.random() * 0.3;
colors[i * 3] = gray;
colors[i * 3 + 1] = gray;
colors[i * 3 + 2] = gray;
sizes[i] = 0.5;
lifetimes[i] = {
life: 3 + Math.random() * 2,
maxLife: 5,
speedY: 0.5 + Math.random() * 0.5,
driftX: (Math.random() - 0.5) * 0.5,
driftZ: (Math.random() - 0.5) * 0.5,
growthRate: 0.02 + Math.random() * 0.02
};
}
function updateSmoke(smoke, deltaTime) {
const positions = smoke.geometry.attributes.position.array;
const colors = smoke.geometry.attributes.color.array;
const sizes = smoke.geometry.attributes.size.array;
const lifetimes = smoke.userData.lifetimes;
const position = smoke.userData.position;
for (let i = 0; i < positions.length / 3; i++) {
const lifetime = lifetimes[i];
lifetime.life -= deltaTime;
if (lifetime.life <= 0) {
resetSmokeParticle(i, positions, colors, sizes, lifetimes, position);
} else {
// 上升并扩散
positions[i * 3] += lifetime.driftX * deltaTime;
positions[i * 3 + 1] += lifetime.speedY * deltaTime;
positions[i * 3 + 2] += lifetime.driftZ * deltaTime;
// 增长大小
sizes[i] += lifetime.growthRate;
// 淡出
const alpha = lifetime.life / lifetime.maxLife;
colors[i * 3] *= 0.995;
colors[i * 3 + 1] *= 0.995;
colors[i * 3 + 2] *= 0.995;
}
}
smoke.geometry.attributes.position.needsUpdate = true;
smoke.geometry.attributes.color.needsUpdate = true;
smoke.geometry.attributes.size.needsUpdate = true;
}高级粒子系统
粒子发射器类
javascript
class ParticleEmitter {
constructor(options = {}) {
// 基本参数
this.particleCount = options.particleCount || 1000;
this.position = options.position || new THREE.Vector3();
this.direction = options.direction || new THREE.Vector3(0, 1, 0);
// 发射参数
this.spread = options.spread || 1; // 发散角度
this.speed = options.speed || 1; // 速度
this.speedRandomness = options.speedRandomness || 0.5;
// 生命周期
this.lifetime = options.lifetime || 2;
this.lifetimeRandomness = options.lifetimeRandomness || 0.5;
// 外观
this.size = options.size || 0.1;
this.color = options.color || new THREE.Color(1, 1, 1);
this.texture = options.texture || null;
// 物理
this.gravity = options.gravity || 0;
this.drag = options.drag || 0.98;
this.init();
}
init() {
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(this.particleCount * 3);
const colors = new Float32Array(this.particleCount * 3);
const sizes = new Float32Array(this.particleCount);
const velocities = new Float32Array(this.particleCount * 3);
const lifetimes = new Float32Array(this.particleCount);
// 初始化所有粒子
for (let i = 0; i < this.particleCount; i++) {
this.resetParticle(i, positions, velocities, lifetimes, sizes, colors);
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
// 材质
const materialOptions = {
size: this.size,
vertexColors: true,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending
};
if (this.texture) {
materialOptions.map = this.texture;
}
const material = new THREE.PointsMaterial(materialOptions);
this.particles = new THREE.Points(geometry, material);
this.particles.userData = { velocities, lifetimes };
}
resetParticle(i, positions, velocities, lifetimes, sizes, colors) {
// 位置
positions[i * 3] = this.position.x;
positions[i * 3 + 1] = this.position.y;
positions[i * 3 + 2] = this.position.z;
// 方向(带随机发散)
const theta = Math.random() * Math.PI * 2;
const phi = this.spread * Math.random();
const dir = this.direction.clone().normalize();
const speed = this.speed * (1 + (Math.random() - 0.5) * this.speedRandomness);
velocities[i * 3] = (dir.x + Math.sin(phi) * Math.cos(theta)) * speed;
velocities[i * 3 + 1] = (dir.y + Math.sin(phi) * Math.sin(theta)) * speed;
velocities[i * 3 + 2] = (dir.z + Math.cos(phi)) * speed;
// 生命周期
lifetimes[i] = this.lifetime * (1 + (Math.random() - 0.5) * this.lifetimeRandomness);
// 大小
sizes[i] = this.size * (0.5 + Math.random() * 0.5);
// 颜色
colors[i * 3] = this.color.r;
colors[i * 3 + 1] = this.color.g;
colors[i * 3 + 2] = this.color.b;
}
update(delta) {
const positions = this.particles.geometry.attributes.position.array;
const velocities = this.particles.userData.velocities;
const lifetimes = this.particles.userData.lifetimes;
const sizes = this.particles.geometry.attributes.size.array;
const colors = this.particles.geometry.attributes.color.array;
for (let i = 0; i < this.particleCount; i++) {
lifetimes[i] -= delta;
if (lifetimes[i] <= 0) {
this.resetParticle(i, positions, velocities, lifetimes, sizes, colors);
} else {
// 更新位置
positions[i * 3] += velocities[i * 3] * delta;
positions[i * 3 + 1] += velocities[i * 3 + 1] * delta;
positions[i * 3 + 2] += velocities[i * 3 + 2] * delta;
// 重力
velocities[i * 3 + 1] -= this.gravity * delta;
// 阻力
velocities[i * 3] *= this.drag;
velocities[i * 3 + 1] *= this.drag;
velocities[i * 3 + 2] *= this.drag;
}
}
this.particles.geometry.attributes.position.needsUpdate = true;
}
// 设置位置
setPosition(x, y, z) {
this.position.set(x, y, z);
}
// 获取 Three.js 对象
getObject3D() {
return this.particles;
}
// 添加到场景
addToScene(scene) {
scene.add(this.particles);
}
// 从场景移除
removeFromScene(scene) {
scene.remove(this.particles);
}
// 释放资源
dispose() {
this.particles.geometry.dispose();
this.particles.material.dispose();
}
}
// 使用示例
const emitter = new ParticleEmitter({
particleCount: 2000,
position: new THREE.Vector3(0, 0, 0),
direction: new THREE.Vector3(0, 1, 0),
speed: 3,
spread: 0.5,
lifetime: 3,
gravity: 0.5,
color: new THREE.Color(1, 0.5, 0)
});
emitter.addToScene(scene);
function animate() {
emitter.update(0.016);
renderer.render(scene, camera);
}GPU 粒子
使用着色器在 GPU 上计算粒子动画,性能更好:
javascript
function createGPUParticles(count = 10000) {
const geometry = new THREE.BufferGeometry();
// 初始位置
const positions = new Float32Array(count * 3);
// 随机种子(用于着色器中的随机计算)
const seeds = new Float32Array(count);
for (let i = 0; i < count; i++) {
positions[i * 3] = (Math.random() - 0.5) * 20;
positions[i * 3 + 1] = Math.random() * 20;
positions[i * 3 + 2] = (Math.random() - 0.5) * 20;
seeds[i] = Math.random();
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('seed', new THREE.BufferAttribute(seeds, 1));
const material = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 },
baseSize: { value: 0.1 },
speed: { value: 2.0 },
color1: { value: new THREE.Color(0xff6600) },
color2: { value: new THREE.Color(0xffcc00) }
},
vertexShader: `
uniform float time;
uniform float baseSize;
uniform float speed;
attribute float seed;
varying vec3 vColor;
varying float vAlpha;
// 伪随机函数
float random(float n) {
return fract(sin(n) * 43758.5453123);
}
void main() {
// 基于 seed 的随机参数
float randOffset = random(seed) * 100.0;
float randSpeed = speed * (0.5 + random(seed + 1.0) * 1.0);
float randSize = baseSize * (0.5 + random(seed + 2.0) * 1.0);
// 计算动画位置
vec3 pos = position;
// 循环下落
float cycleTime = 20.0 / randSpeed;
float t = mod(time + randOffset, cycleTime) / cycleTime;
pos.y = 10.0 - t * 20.0;
// 飘动效果
pos.x += sin(time * 2.0 + seed * 10.0) * 0.5;
pos.z += cos(time * 2.0 + seed * 10.0) * 0.5;
// 颜色(基于高度)
float colorMix = (pos.y + 10.0) / 20.0;
vColor = mix(vec3(1.0, 0.4, 0.0), vec3(1.0, 0.8, 0.0), colorMix);
// 透明度(底部淡出)
vAlpha = smoothstep(-10.0, 0.0, pos.y);
vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
gl_PointSize = randSize * (300.0 / -mvPosition.z);
gl_Position = projectionMatrix * mvPosition;
}
`,
fragmentShader: `
varying vec3 vColor;
varying float vAlpha;
void main() {
// 圆形粒子
float dist = length(gl_PointCoord - vec2(0.5));
if (dist > 0.5) discard;
// 边缘柔化
float alpha = 1.0 - smoothstep(0.3, 0.5, dist);
alpha *= vAlpha;
gl_FragColor = vec4(vColor, alpha);
}
`,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending
});
const particles = new THREE.Points(geometry, material);
return particles;
}
// 使用
const gpuParticles = createGPUParticles(50000);
scene.add(gpuParticles);
function animate() {
gpuParticles.material.uniforms.time.value = performance.now() * 0.001;
renderer.render(scene, camera);
}API 参考
THREE.Points
| 属性/方法 | 类型 | 说明 |
|---|---|---|
geometry | BufferGeometry | 几何体 |
material | Material | 材质 |
isPoints | Boolean | 类型标识 |
computeBoundingBox() | Method | 计算包围盒 |
computeBoundingSphere() | Method | 计算包围球 |
PointsMaterial
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
color | Color | 0xffffff | 粒子颜色 |
size | Number | 1 | 粒子大小 |
sizeAttenuation | Boolean | true | 大小是否随距离衰减 |
map | Texture | null | 纹理贴图 |
alphaMap | Texture | null | Alpha 贴图 |
transparent | Boolean | false | 是否透明 |
opacity | Number | 1 | 透明度 |
vertexColors | Boolean | false | 是否使用顶点颜色 |
depthWrite | Boolean | true | 是否写入深度 |
depthTest | Boolean | true | 是否深度测试 |
blending | Blending | NormalBlending | 混合模式 |
BufferAttribute
| 方法 | 说明 |
|---|---|
setArray(array) | 设置数据数组 |
set needsUpdate(value) | 标记需要更新 |
getX(index) | 获取指定索引的值 |
setX(index, value) | 设置指定索引的值 |
getY(index) | 获取 Y 分量 |
getZ(index) | 获取 Z 分量 |
配置参数详解
混合模式
javascript
// 正常混合
material.blending = THREE.NormalBlending;
// 叠加混合(发光效果)
material.blending = THREE.AdditiveBlending;
// 减法混合
material.blending = THREE.SubtractiveBlending;
// 乘法混合
material.blending = THREE.MultiplyBlending;深度设置
javascript
// 透明粒子通常需要禁用深度写入
material.depthWrite = false; // 防止前后遮挡问题
// 完全禁用深度测试(粒子始终在最前)
material.depthTest = false;大小衰减
javascript
// 启用衰减(近大远小)
material.sizeAttenuation = true;
// 禁用衰减(所有粒子同大小)
material.sizeAttenuation = false;
// 在着色器中自定义衰减
gl_PointSize = baseSize * (constant / -mvPosition.z);纹理配置
javascript
// 基础纹理
material.map = texture;
// Alpha 贴图
material.alphaMap = alphaTexture;
// 纹理过滤
texture.minFilter = THREE.LinearFilter;
texture.magFilter = THREE.LinearFilter;
// 纹理包裹
texture.wrapS = THREE.ClampToEdgeWrapping;
texture.wrapT = THREE.ClampToEdgeWrapping;性能优化
粒子数量优化
javascript
// 根据设备性能调整粒子数量
const isMobile = /Mobile/.test(navigator.userAgent);
const particleCount = isMobile ? 1000 : 10000;
// 或使用 GPU 信息
const gl = renderer.getContext();
const maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
const recommendedCount = maxTextureSize > 4096 ? 50000 : 10000;使用 GPU 动画
javascript
// ❌ CPU 动画(性能差)
function animate() {
const positions = geometry.attributes.position.array;
for (let i = 0; i < count; i++) {
positions[i * 3 + 1] -= 0.1; // CPU 计算
}
geometry.attributes.position.needsUpdate = true;
}
// ✅ GPU 动画(性能好)
// 在着色器中计算位置
vertexShader: `
uniform float time;
void main() {
vec3 pos = position;
pos.y -= time * speed; // GPU 计算
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`减少更新频率
javascript
// 不是每帧都更新所有粒子
let updateIndex = 0;
function partialUpdate(count) {
const batchSize = Math.floor(particleCount / 60); // 分60帧更新完
for (let i = 0; i < batchSize; i++) {
const idx = (updateIndex + i) % particleCount;
// 更新粒子 idx
}
updateIndex = (updateIndex + batchSize) % particleCount;
geometry.attributes.position.needsUpdate = true;
}使用 InstancedBuffer
javascript
// 对于更复杂的粒子,可以使用 InstancedBuffer
import { InstancedBufferGeometry, InstancedBufferAttribute } from 'three';
const geometry = new InstancedBufferGeometry();
// ... 配置实例化几何体LOD 粒子系统
javascript
// 根据距离使用不同的粒子数量
const lod = new THREE.LOD();
// 近距离:高密度粒子
lod.addLevel(highDensityParticles, 0);
// 中距离:中等密度
lod.addLevel(mediumDensityParticles, 50);
// 远距离:低密度粒子
lod.addLevel(lowDensityParticles, 100);
scene.add(lod);常见问题
Q1: 粒子显示为方块而不是圆形?
A: 需要使用纹理或在着色器中裁剪:
javascript
// 方法一:使用圆形纹理
const texture = createParticleTexture();
material.map = texture;
// 方法二:着色器裁剪
fragmentShader: `
void main() {
float dist = length(gl_PointCoord - vec2(0.5));
if (dist > 0.5) discard; // 裁剪圆形外
gl_FragColor = vec4(color, 1.0);
}
`Q2: 粒子透明度有问题?
A: 检查以下设置:
javascript
// 1. 启用透明
material.transparent = true;
material.opacity = 0.8;
// 2. 禁用深度写入
material.depthWrite = false;
// 3. 使用合适的混合模式
material.blending = THREE.AdditiveBlending;
// 4. 纹理要有 alpha 通道
// Canvas 纹理需要正确设置 alpha
ctx.fillStyle = 'rgba(255, 255, 255, 0)'; // 透明背景Q3: 粒子排序问题?
A: 粒子默认不排序,解决方案:
javascript
// 方法一:禁用深度测试(粒子始终在最前)
material.depthTest = false;
// 方法二:手动排序(性能开销大)
function sortParticles() {
const positions = geometry.attributes.position.array;
const cameraPosition = camera.position;
// 计算每个粒子到相机的距离
const distances = [];
for (let i = 0; i < particleCount; i++) {
const dx = positions[i * 3] - cameraPosition.x;
const dy = positions[i * 3 + 1] - cameraPosition.y;
const dz = positions[i * 3 + 2] - cameraPosition.z;
distances.push({ index: i, distance: dx*dx + dy*dy + dz*dz });
}
// 从远到近排序
distances.sort((a, b) => b.distance - a.distance);
// 重新排列数据
// ...
}
// 方法三:使用叠加混合避免排序问题
material.blending = THREE.AdditiveBlending;Q4: 如何实现粒子碰撞?
A: 简单碰撞检测:
javascript
function checkCollisions(particles, boundingBox) {
const positions = particles.geometry.attributes.position.array;
for (let i = 0; i < particleCount; i++) {
const x = positions[i * 3];
const y = positions[i * 3 + 1];
const z = positions[i * 3 + 2];
// 边界碰撞
if (x < boundingBox.min.x || x > boundingBox.max.x) {
velocities[i].x *= -0.8; // 反弹
}
if (y < boundingBox.min.y || y > boundingBox.max.y) {
velocities[i].y *= -0.8;
}
if (z < boundingBox.min.z || z > boundingBox.max.z) {
velocities[i].z *= -0.8;
}
// 地面碰撞
if (y < 0) {
positions[i * 3 + 1] = 0;
velocities[i].y *= -0.5;
velocities[i].x *= 0.9; // 摩擦
velocities[i].z *= 0.9;
}
}
}Q5: 粒子数量太多导致性能问题?
A: 优化策略:
javascript
// 1. 减少粒子数量
const maxParticles = Math.min(targetCount, 10000);
// 2. 使用 GPU 着色器动画
// 参见 GPU 粒子章节
// 3. 降低更新频率
// 每隔几帧更新一次
// 4. 分块更新
// 每帧只更新一部分粒子
// 5. 使用简化效果
// 远距离减少粒子细节
// 6. 使用点精灵而非网格
// THREE.Points 已是最优方案Q6: 如何让粒子面向相机?
A: 粒子默认就是面向相机的(Billboard),但可以自定义:
javascript
// 默认行为:粒子始终面向相机
material.sizeAttenuation = true;
// 自定义朝向(在着色器中)
vertexShader: `
void main() {
// 广告牌矩阵
vec3 cameraRight = vec3(modelViewMatrix[0][0], modelViewMatrix[1][0], modelViewMatrix[2][0]);
vec3 cameraUp = vec3(modelViewMatrix[0][1], modelViewMatrix[1][1], modelViewMatrix[2][1]);
vec3 vertexPosition = position
+ cameraRight * aOffset.x
+ cameraUp * aOffset.y;
gl_Position = projectionMatrix * modelViewMatrix * vec4(vertexPosition, 1.0);
}
`Q7: 如何实现粒子轨迹?
A: 存储历史位置:
javascript
// 方法一:使用多条线段
const trailGeometry = new THREE.BufferGeometry();
const trailPositions = new Float32Array(maxTrailLength * 3);
trailGeometry.setAttribute('position', new THREE.BufferAttribute(trailPositions, 3));
const trailMaterial = new THREE.LineBasicMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.5
});
const trail = new THREE.Line(trailGeometry, trailMaterial);
// 更新轨迹
function updateTrail(newPosition) {
const positions = trailGeometry.attributes.position.array;
// 移动所有点
for (let i = positions.length - 3; i >= 3; i -= 3) {
positions[i] = positions[i - 3];
positions[i + 1] = positions[i - 2];
positions[i + 2] = positions[i - 1];
}
// 添加新点
positions[0] = newPosition.x;
positions[1] = newPosition.y;
positions[2] = newPosition.z;
trailGeometry.attributes.position.needsUpdate = true;
}
// 方法二:使用后处理运动模糊