在 Three.js 中实现烟花、雨雪、火焰等特效,通常需要使用粒子系统(Particle System)。下面我将分别介绍这些特效的实现思路和关键代码。
使用粒子系统模拟烟花爆炸效果,分为发射、爆炸两个阶段。
javascript
import * as THREE from 'three';
class Firework {
constructor(scene) {
this.scene = scene;
this.particles = [];
this.gravity = 0.01;
}
// 创建单个粒子
createParticle(position, color, velocity, size = 2, lifetime = 100) {
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array([0, 0, 0]);
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const material = new THREE.PointsMaterial({
color: color,
size: size,
transparent: true,
blending: THREE.AdditiveBlending
});
const particle = new THREE.Points(geometry, material);
particle.position.copy(position);
particle.userData = {
velocity: velocity,
lifetime: lifetime,
age: 0
};
this.scene.add(particle);
this.particles.push(particle);
}
// 发射烟花
launch(position) {
const baseColor = new THREE.Color(
Math.random() * 0.5 + 0.5,
Math.random() * 0.5 + 0.5,
Math.random() * 0.5 + 0.5
);
// 发射轨迹粒子
for (let i = 0; i < 30; i++) {
const velocity = new THREE.Vector3(
(Math.random() - 0.5) * 0.2,
Math.random() * 0.5 + 0.5,
(Math.random() - 0.5) * 0.2
);
this.createParticle(
position.clone(),
baseColor,
velocity,
1.5,
30
);
}
// 延迟爆炸
setTimeout(() => {
this.explode(position, baseColor);
}, 1000);
}
// 爆炸效果
explode(position, baseColor) {
const particleCount = 100;
for (let i = 0; i < particleCount; i++) {
// 随机方向
const phi = Math.random() * Math.PI * 2;
const theta = Math.random() * Math.PI;
const radius = Math.random() * 0.5 + 0.1;
const velocity = new THREE.Vector3(
Math.sin(theta) * Math.cos(phi) * radius,
Math.sin(theta) * Math.sin(phi) * radius,
Math.cos(theta) * radius
);
// 颜色变化
const color = baseColor.clone();
color.offsetHSL(Math.random() * 0.2 - 0.1, 0, 0);
this.createParticle(
position.clone(),
color,
velocity,
Math.random() * 3 + 1,
60
);
}
}
// 更新粒子
update() {
for (let i = this.particles.length - 1; i >= 0; i--) {
const particle = this.particles[i];
particle.userData.age++;
particle.userData.velocity.y -= this.gravity;
particle.position.add(particle.userData.velocity);
// 粒子大小随生命周期减小
particle.material.size *= 0.98;
particle.material.opacity *= 0.97;
if (particle.userData.age > particle.userData.lifetime) {
this.scene.remove(particle);
particle.geometry.dispose();
particle.material.dispose();
this.particles.splice(i, 1);
}
}
}
}
// 使用示例
const firework = new Firework(scene);
firework.launch(new THREE.Vector3(0, 0, 0));
// 在动画循环中
function animate() {
firework.update();
requestAnimationFrame(animate);
}
javascript
class WeatherEffect {
constructor(scene, type = 'snow', count = 1000) {
this.scene = scene;
this.type = type;
this.particles = null;
this.count = count;
this.init();
}
init() {
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(this.count * 3);
const velocities = [];
const sizes = new Float32Array(this.count);
// 初始化粒子位置和速度
for (let i = 0; i < this.count; i++) {
const i3 = i * 3;
positions[i3] = (Math.random() - 0.5) * 100;
positions[i3 + 1] = Math.random() * 50;
positions[i3 + 2] = (Math.random() - 0.5) * 100;
if (this.type === 'snow') {
velocities.push({
x: (Math.random() - 0.5) * 0.1,
y: Math.random() * 0.5 + 0.2,
z: (Math.random() - 0.5) * 0.1
});
sizes[i] = Math.random() * 2 + 1;
} else if (this.type === 'rain') {
velocities.push({
x: (Math.random() - 0.5) * 0.2,
y: Math.random() * 2 + 3,
z: (Math.random() - 0.5) * 0.2
});
sizes[i] = Math.random() * 0.5 + 0.3;
}
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
const material = new THREE.PointsMaterial({
color: this.type === 'snow' ? 0xFFFFFF : 0x8888FF,
size: this.type === 'snow' ? 2 : 0.5,
transparent: true,
opacity: this.type === 'snow' ? 0.8 : 0.6,
blending: THREE.AdditiveBlending
});
this.particles = new THREE.Points(geometry, material);
this.particles.userData.velocities = velocities;
this.scene.add(this.particles);
}
update() {
const positions = this.particles.geometry.attributes.position.array;
const velocities = this.particles.userData.velocities;
for (let i = 0; i < this.count; i++) {
const i3 = i * 3;
positions[i3] += velocities[i].x;
positions[i3 + 1] += velocities[i].y;
positions[i3 + 2] += velocities[i].z;
// 边界重置
if (positions[i3 + 1] < -10) {
positions[i3] = (Math.random() - 0.5) * 100;
positions[i3 + 1] = 50;
positions[i3 + 2] = (Math.random() - 0.5) * 100;
}
}
this.particles.geometry.attributes.position.needsUpdate = true;
}
}
// 使用示例
const snow = new WeatherEffect(scene, 'snow', 2000);
const rain = new WeatherEffect(scene, 'rain', 3000);
function animate() {
snow.update();
rain.update();
}
javascript
class FireEffect {
constructor(scene, position, size = 1) {
this.scene = scene;
this.position = position;
this.size = size;
this.particles = [];
this.init();
}
init() {
const particleCount = 100;
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
const colors = new Float32Array(particleCount * 3);
const sizes = new Float32Array(particleCount);
for (let i = 0; i < particleCount; i++) {
const i3 = i * 3;
// 初始位置在火焰底部
const radius = Math.random() * 0.5 * this.size;
const angle = Math.random() * Math.PI * 2;
positions[i3] = Math.cos(angle) * radius;
positions[i3 + 1] = 0;
positions[i3 + 2] = Math.sin(angle) * radius;
// 颜色从黄色到红色
const color = new THREE.Color();
const hue = 0.1 + Math.random() * 0.1;
const saturation = 0.8 + Math.random() * 0.2;
const lightness = 0.5;
color.setHSL(hue, saturation, lightness);
colors[i3] = color.r;
colors[i3 + 1] = color.g;
colors[i3 + 2] = color.b;
sizes[i] = Math.random() * 2 + 1;
}
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 textureLoader = new THREE.TextureLoader();
const sprite = textureLoader.load('https://threejs.org/examples/textures/sprites/disc.png');
const material = new THREE.PointsMaterial({
size: 3,
map: sprite,
blending: THREE.AdditiveBlending,
depthWrite: false,
transparent: true,
vertexColors: true
});
this.points = new THREE.Points(geometry, material);
this.points.position.copy(this.position);
this.scene.add(this.points);
// 存储粒子数据
this.particlesData = [];
for (let i = 0; i < particleCount; i++) {
this.particlesData.push({
velocity: new THREE.Vector3(
(Math.random() - 0.5) * 0.1,
Math.random() * 0.2 + 0.1,
(Math.random() - 0.5) * 0.1
),
life: Math.random() * 1 + 0.5,
});
}
}
update() {
const positions = this.points.geometry.attributes.position.array;
const colors = this.points.geometry.attributes.color.array;
const sizes = this.points.geometry.attributes.size.array;
for (let i = 0; i < this.particlesData.length; i++) {
const i3 = i * 3;
const particle = this.particlesData[i];
// 更新位置
positions[i3] += particle.velocity.x;
positions[i3 + 1] += particle.velocity.y;
positions[i3 + 2] += particle.velocity.z;
// 减小生命周期
particle.life -= 0.01;
// 重置死亡的粒子
if (particle.life <= 0 || positions[i3 + 1] > 3 * this.size) {
positions[i3] = (Math.random() - 0.5) * this.size * 0.5;
positions[i3 + 1] = 0;
positions[i3 + 2] = (Math.random() - 0.5) * this.size * 0.5;
particle.velocity.set(
(Math.random() - 0.5) * 0.1,
Math.random() * 0.2 + 0.1,
(Math.random() - 0.5) * 0.1
);
particle.life = Math.random() * 1 + 0.5;
// 重置颜色
const hue = 0.05 + Math.random() * 0.1;
const color = new THREE.Color();
color.setHSL(hue, 1, 0.5);
colors[i3] = color.r;
colors[i3 + 1] = color.g;
colors[i3 + 2] = color.b;
}
// 更新颜色(从黄到红到透明)
const age = 1 - particle.life;
colors[i3 + 1] *= 0.95; // 减少绿色分量
colors[i3] = Math.min(1, colors[i3] * 1.02); // 增加红色分量
// 更新大小
sizes[i] = Math.max(0, sizes[i] * 0.98);
}
this.points.geometry.attributes.position.needsUpdate = true;
this.points.geometry.attributes.color.needsUpdate = true;
this.points.geometry.attributes.size.needsUpdate = true;
// 添加随机抖动
this.points.rotation.y += 0.001;
}
}
// 使用示例
const fire = new FireEffect(scene, new THREE.Vector3(0, 0, 0), 2);
function animate() {
fire.update();
}
javascript
// 1. 使用 InstancedMesh 优化大量粒子
const particleGeometry = new THREE.SphereGeometry(0.1, 8, 8);
const instancedMesh = new THREE.InstancedMesh(particleGeometry, material, 1000);
// 2. 使用着色器材质(ShaderMaterial)获得更好性能
const vertexShader = `
attribute float size;
attribute vec3 color;
varying vec3 vColor;
void main() {
vColor = color;
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
gl_PointSize = size * (300.0 / -mvPosition.z);
gl_Position = projectionMatrix * mvPosition;
}
`;
const fragmentShader = `
varying vec3 vColor;
void main() {
gl_FragColor = vec4(vColor, 1.0);
}
`;
javascript
// 添加鼠标交互
function addInteraction() {
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
window.addEventListener('click', (event) => {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
// 在点击位置创建特效
const intersects = raycaster.intersectObjects(groundMesh);
if (intersects.length > 0) {
firework.launch(intersects[0].point);
}
});
}
这些特效可以根据需要进行组合和调整,通过调整粒子数量、速度、颜色和生命周期等参数,可以创建出各种不同的视觉效果。记得在 Three.js 项目中合理管理资源,及时清理不再使用的几何体和材质。