粒子系统
粒子系统是 3D 图形中用于模拟大量小物体的技术,可以创建雨雪、火焰、烟雾、星空等效果。Three.js 通过 Points 和 PointsMaterial 提供了高效的粒子渲染能力。本章将详细介绍如何创建和控制粒子系统。
粒子基础
创建粒子
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);
});
小结
粒子系统是创建动态效果的重要工具:
- Points 和 PointsMaterial 是创建粒子的基础
- 纹理 可以让粒子呈现各种形状
- 混合模式 影响粒子的视觉效果,加法混合适合发光效果
- 顶点颜色 让每个粒子有不同的颜色
- 自定义着色器 可以实现更复杂的粒子效果
- 性能优化 需要注意粒子数量和渲染设置
合理使用粒子系统,可以为场景添加生动的视觉效果。