跳到主要内容

粒子系统

粒子系统是 3D 图形中用于模拟大量小物体的技术,可以创建雨雪、火焰、烟雾、星空等效果。Three.js 通过 PointsPointsMaterial 提供了高效的粒子渲染能力。本章将详细介绍如何创建和控制粒子系统。

粒子基础

创建粒子

Three.js 使用 Points 类来渲染粒子,它比渲染多个独立网格高效得多:

// 创建几何体
const geometry = new THREE.BufferGeometry();

// 定义粒子位置
const positions = new Float32Array([
0, 0, 0, // 粒子 1
1, 0, 0, // 粒子 2
0, 1, 0 // 粒子 3
]);

geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));

// 创建粒子材质
const material = new THREE.PointsMaterial({
color: 0xff0000,
size: 0.5
});

// 创建粒子系统
const particles = new THREE.Points(geometry, material);
scene.add(particles);

批量创建粒子

通常需要创建大量粒子,使用循环生成位置数据:

const particleCount = 1000;
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);

for (let i = 0; i < particleCount; i++) {
const i3 = i * 3;

positions[i3] = (Math.random() - 0.5) * 10; // x
positions[i3 + 1] = (Math.random() - 0.5) * 10; // y
positions[i3 + 2] = (Math.random() - 0.5) * 10; // z
}

geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));

const material = new THREE.PointsMaterial({
color: 0x00ff88,
size: 0.1,
sizeAttenuation: true
});

const particles = new THREE.Points(geometry, material);
scene.add(particles);

sizeAttenuation 控制粒子大小是否随距离变化。设为 true 时,近处的粒子大,远处的粒子小;设为 false 时,所有粒子大小一致。

粒子材质

PointsMaterial 属性

const material = new THREE.PointsMaterial({
color: 0xffffff, // 颜色
size: 0.1, // 大小
sizeAttenuation: true, // 是否随距离衰减
map: texture, // 纹理贴图
alphaMap: alphaTexture, // 透明度贴图
transparent: true, // 是否透明
opacity: 0.8, // 透明度
vertexColors: true, // 是否使用顶点颜色
blending: THREE.AdditiveBlending, // 混合模式
depthWrite: false // 是否写入深度缓冲
});

粒子纹理

使用纹理可以让粒子呈现各种形状:

// 创建圆形粒子纹理
function createCircleTexture() {
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(1, 'rgba(255, 255, 255, 0)');

ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 64, 64);

return new THREE.CanvasTexture(canvas);
}

const material = new THREE.PointsMaterial({
size: 0.5,
map: createCircleTexture(),
transparent: true,
depthWrite: false
});

混合模式

不同的混合模式产生不同效果:

// 加法混合:粒子颜色相加,产生发光效果
material.blending = THREE.AdditiveBlending;

// 正常混合(默认)
material.blending = THREE.NormalBlending;

// 减法混合:粒子颜色相减
material.blending = THREE.SubtractiveBlending;

// 乘法混合
material.blending = THREE.MultiplyBlending;

加法混合最适合发光粒子(火焰、火花、星星等)。

顶点颜色

每个粒子可以有不同的颜色:

const count = 1000;
const geometry = new THREE.BufferGeometry();

const positions = new Float32Array(count * 3);
const colors = new Float32Array(count * 3);

for (let i = 0; i < count; i++) {
// 随机位置
positions[i * 3] = (Math.random() - 0.5) * 10;
positions[i * 3 + 1] = (Math.random() - 0.5) * 10;
positions[i * 3 + 2] = (Math.random() - 0.5) * 10;

// 随机颜色
colors[i * 3] = Math.random(); // R
colors[i * 3 + 1] = Math.random(); // G
colors[i * 3 + 2] = Math.random(); // 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 velocities = [];

// 初始化速度
for (let i = 0; i < count; i++) {
velocities.push({
x: (Math.random() - 0.5) * 0.02,
y: -Math.random() * 0.02,
z: (Math.random() - 0.5) * 0.02
});
}

function animate() {
requestAnimationFrame(animate);

const positions = geometry.attributes.position.array;

for (let i = 0; i < count; 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] < -5) {
positions[i * 3 + 1] = 10;
}
}

geometry.attributes.position.needsUpdate = true;

renderer.render(scene, camera);
}

雪花效果

const count = 5000;
const positions = new Float32Array(count * 3);
const speeds = [];

for (let i = 0; i < count; i++) {
positions[i * 3] = (Math.random() - 0.5) * 50;
positions[i * 3 + 1] = Math.random() * 30;
positions[i * 3 + 2] = (Math.random() - 0.5) * 50;

speeds.push(Math.random() * 0.03 + 0.01);
}

geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));

function animate() {
requestAnimationFrame(animate);

const positions = geometry.attributes.position.array;
const time = Date.now() * 0.0001;

for (let i = 0; i < count; i++) {
// 下落
positions[i * 3 + 1] -= speeds[i];

// 水平飘动
positions[i * 3] += Math.sin(time + i) * 0.01;

// 重置
if (positions[i * 3 + 1] < -5) {
positions[i * 3 + 1] = 30;
positions[i * 3] = (Math.random() - 0.5) * 50;
positions[i * 3 + 2] = (Math.random() - 0.5) * 50;
}
}

geometry.attributes.position.needsUpdate = true;
renderer.render(scene, camera);
}

星空效果

const count = 5000;
const positions = new Float32Array(count * 3);
const colors = new Float32Array(count * 3);

for (let i = 0; i < count; i++) {
// 球形分布
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
const radius = 50 + Math.random() * 50;

positions[i * 3] = radius * Math.sin(phi) * Math.cos(theta);
positions[i * 3 + 1] = radius * Math.sin(phi) * Math.sin(theta);
positions[i * 3 + 2] = radius * Math.cos(phi);

// 星星颜色
const colorChoice = Math.random();
if (colorChoice < 0.6) {
// 白色
colors[i * 3] = 1;
colors[i * 3 + 1] = 1;
colors[i * 3 + 2] = 1;
} else if (colorChoice < 0.8) {
// 蓝色
colors[i * 3] = 0.8;
colors[i * 3 + 1] = 0.9;
colors[i * 3 + 2] = 1;
} else {
// 黄色
colors[i * 3] = 1;
colors[i * 3 + 1] = 0.9;
colors[i * 3 + 2] = 0.7;
}
}

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 stars = new THREE.Points(geometry, material);
scene.add(stars);

// 缓慢旋转
function animate() {
requestAnimationFrame(animate);
stars.rotation.y += 0.0001;
renderer.render(scene, camera);
}

自定义着色器粒子

使用 ShaderMaterial 可以创建更复杂的粒子效果:

根据距离改变大小

const vertexShader = `
uniform float uTime;

attribute float aSize;
attribute vec3 aRandomness;

varying vec3 vColor;

void main() {
vec4 modelPosition = modelMatrix * vec4(position, 1.0);

// 添加动画
modelPosition.x += sin(uTime + aRandomness.x * 10.0) * 0.1;
modelPosition.y += cos(uTime + aRandomness.y * 10.0) * 0.1;
modelPosition.z += sin(uTime + aRandomness.z * 10.0) * 0.1;

vec4 viewPosition = viewMatrix * modelPosition;
gl_Position = projectionMatrix * viewPosition;

// 根据距离设置大小
gl_PointSize = aSize * (300.0 / -viewPosition.z);

vColor = vec3(aRandomness * 0.5 + 0.5);
}
`;

const fragmentShader = `
varying vec3 vColor;

void main() {
// 圆形粒子
float distanceToCenter = distance(gl_PointCoord, vec2(0.5));
float strength = 0.05 / distanceToCenter - 0.1;
strength = clamp(strength, 0.0, 1.0);

gl_FragColor = vec4(vColor, strength);
}
`;

const geometry = new THREE.BufferGeometry();
const count = 10000;

const positions = new Float32Array(count * 3);
const sizes = new Float32Array(count);
const randomness = new Float32Array(count * 3);

for (let i = 0; i < count; i++) {
positions[i * 3] = (Math.random() - 0.5) * 10;
positions[i * 3 + 1] = (Math.random() - 0.5) * 10;
positions[i * 3 + 2] = (Math.random() - 0.5) * 10;

sizes[i] = Math.random() * 0.5 + 0.5;

randomness[i * 3] = Math.random();
randomness[i * 3 + 1] = Math.random();
randomness[i * 3 + 2] = Math.random();
}

geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('aSize', new THREE.BufferAttribute(sizes, 1));
geometry.setAttribute('aRandomness', new THREE.BufferAttribute(randomness, 3));

const material = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 }
},
vertexShader,
fragmentShader,
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false
});

银河效果

const vertexShader = `
uniform float uTime;
uniform float uSize;

attribute vec3 aRandomness;
attribute float aScale;

varying vec3 vColor;

void main() {
vec4 modelPosition = modelMatrix * vec4(position, 1.0);

// 旋转
float angle = atan(modelPosition.x, modelPosition.z);
float distanceToCenter = length(modelPosition.xz);
float angleOffset = (1.0 / distanceToCenter) * uTime * 0.2;
angle += angleOffset;

modelPosition.x = cos(angle) * distanceToCenter;
modelPosition.z = sin(angle) * distanceToCenter;

// 随机偏移
modelPosition.xyz += aRandomness;

vec4 viewPosition = viewMatrix * modelPosition;
gl_Position = projectionMatrix * viewPosition;

// 大小
gl_PointSize = uSize * aScale;
gl_PointSize *= (1.0 / -viewPosition.z);

// 颜色
vColor = vec3(1.0, 0.8, 0.5);
}
`;

const fragmentShader = `
varying vec3 vColor;

void main() {
float distanceToCenter = distance(gl_PointCoord, vec2(0.5));
float strength = 0.05 / distanceToCenter - 0.1;
strength = clamp(strength, 0.0, 1.0);

gl_FragColor = vec4(vColor, strength);
}
`;

// 生成螺旋银河
const count = 100000;
const positions = new Float32Array(count * 3);
const scales = new Float32Array(count);
const randomness = new Float32Array(count * 3);

const branches = 5;
const radius = 5;
const spin = 1;

for (let i = 0; i < count; i++) {
const i3 = i * 3;

const r = Math.random() * radius;
const branchAngle = ((i % branches) / branches) * Math.PI * 2;
const spinAngle = r * spin;
const angle = branchAngle + spinAngle;

positions[i3] = Math.cos(angle) * r;
positions[i3 + 1] = 0;
positions[i3 + 2] = Math.sin(angle) * r;

const randomX = (Math.random() - 0.5) * (r / radius) * 2;
const randomY = (Math.random() - 0.5) * 0.5;
const randomZ = (Math.random() - 0.5) * (r / radius) * 2;

randomness[i3] = randomX;
randomness[i3 + 1] = randomY;
randomness[i3 + 2] = randomZ;

scales[i] = Math.random();
}

geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('aScale', new THREE.BufferAttribute(scales, 1));
geometry.setAttribute('aRandomness', new THREE.BufferAttribute(randomness, 3));

const material = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uSize: { value: 30 }
},
vertexShader,
fragmentShader,
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false
});

性能优化

减少粒子数量

粒子数量直接影响性能,根据场景需求选择合适的数量:

// 根据设备性能调整
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
const particleCount = isMobile ? 5000 : 20000;

使用 BufferGeometry

始终使用 BufferGeometry 而不是 Geometry:

// 正确
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));

// 避免
const geometry = new THREE.Geometry();
geometry.vertices.push(new THREE.Vector3(0, 0, 0));

禁用深度写入

对于透明粒子,禁用深度写入可以避免排序问题:

const material = new THREE.PointsMaterial({
transparent: true,
depthWrite: false // 重要
});

完整示例

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// 场景设置
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a0a);

const camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
0.1,
100
);
camera.position.set(0, 2, 5);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;

// 创建粒子纹理
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.2, 'rgba(255, 255, 255, 0.8)');
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');

ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 64, 64);

return new THREE.CanvasTexture(canvas);
}

// 创建火焰粒子
const fireCount = 500;
const fireGeometry = new THREE.BufferGeometry();
const firePositions = new Float32Array(fireCount * 3);
const fireColors = new Float32Array(fireCount * 3);
const fireVelocities = [];

for (let i = 0; i < fireCount; i++) {
firePositions[i * 3] = (Math.random() - 0.5) * 0.5;
firePositions[i * 3 + 1] = Math.random() * 2;
firePositions[i * 3 + 2] = (Math.random() - 0.5) * 0.5;

// 从黄到红的颜色
const t = Math.random();
fireColors[i * 3] = 1;
fireColors[i * 3 + 1] = 0.3 + t * 0.5;
fireColors[i * 3 + 2] = t * 0.3;

fireVelocities.push({
x: (Math.random() - 0.5) * 0.01,
y: Math.random() * 0.03 + 0.02,
z: (Math.random() - 0.5) * 0.01
});
}

fireGeometry.setAttribute('position', new THREE.BufferAttribute(firePositions, 3));
fireGeometry.setAttribute('color', new THREE.BufferAttribute(fireColors, 3));

const fireMaterial = new THREE.PointsMaterial({
size: 0.3,
map: createParticleTexture(),
vertexColors: true,
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false
});

const fireParticles = new THREE.Points(fireGeometry, fireMaterial);
scene.add(fireParticles);

// 动画
function animate() {
requestAnimationFrame(animate);

const positions = fireGeometry.attributes.position.array;
const colors = fireGeometry.attributes.color.array;

for (let i = 0; i < fireCount; i++) {
positions[i * 3] += fireVelocities[i].x;
positions[i * 3 + 1] += fireVelocities[i].y;
positions[i * 3 + 2] += fireVelocities[i].z;

// 重置粒子
if (positions[i * 3 + 1] > 3) {
positions[i * 3] = (Math.random() - 0.5) * 0.5;
positions[i * 3 + 1] = 0;
positions[i * 3 + 2] = (Math.random() - 0.5) * 0.5;
}

// 颜色随高度变化
const t = positions[i * 3 + 1] / 3;
colors[i * 3 + 1] = 0.8 - t * 0.5;
}

fireGeometry.attributes.position.needsUpdate = true;
fireGeometry.attributes.color.needsUpdate = true;

controls.update();
renderer.render(scene, camera);
}

animate();

// 响应窗口变化
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});

小结

粒子系统是创建动态效果的重要工具:

  • PointsPointsMaterial 是创建粒子的基础
  • 纹理 可以让粒子呈现各种形状
  • 混合模式 影响粒子的视觉效果,加法混合适合发光效果
  • 顶点颜色 让每个粒子有不同的颜色
  • 自定义着色器 可以实现更复杂的粒子效果
  • 性能优化 需要注意粒子数量和渲染设置

合理使用粒子系统,可以为场景添加生动的视觉效果。