Three.js案例
烟花

烟花

使用 Three.js 创建烟花粒子特效教程

今天,我们将使用 Three.js 来实现一个简单而美观的烟花粒子效果。烟花会在屏幕随机位置生成,粒子在爆炸后呈现出散射、下降、逐渐消散的动态效果。先来看一下效果。

第一步:搭建基础场景

在正式实现烟花效果前,我们需要一个可以显示内容的 Three.js 场景。以下代码实现了最基本的场景、相机和渲染器设置。

import * as THREE from "three";
 
// 场景设置
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
 
// 相机位置
camera.position.z = 50;

代码逻辑说明

  1. scene:创建一个场景对象,是 Three.js 中所有 3D 元素的容器。
  2. camera:创建透视相机,用于观察场景,参数:
    • 75:视角(FOV)。
    • window.innerWidth / window.innerHeight:宽高比。
    • 0.1 和 1000:相机的近、远平面。
  3. renderer:渲染器将 3D 场景绘制到浏览器中。
  4. 设置相机位置:通过 camera.position.z 拉远视角,确保看到整个场景。

第二步:定义烟花粒子类

在这一步,我们将设计一个 Firework 类,用于模拟每个烟花的粒子效果。
一个烟花包含以下逻辑:

  1. 初始属性
    • 粒子的位置(positions)、速度(velocities)以及颜色(colors)。
    • 设置粒子生命周期(life),用于控制粒子的消失。
  2. 初始化粒子:在烟花爆炸时,粒子会沿着随机方向散开。
  3. 更新粒子状态:粒子随着时间更新位置、缩小大小,并模拟重力作用。
  4. 销毁粒子:当粒子生命周期耗尽时,清除其资源。

1. 定义类和基础属性

class Firework {
  constructor(x, y, z) {
    this.geometry = new THREE.BufferGeometry(); // 几何对象存储粒子数据
    this.count = 10000; // 粒子数量
    this.positions = new Float32Array(this.count * 3); // 粒子位置
    this.velocities = []; // 粒子速度
    this.colors = new Float32Array(this.count * 3); // 粒子颜色
    this.sizes = new Float32Array(this.count); // 粒子大小
    this.life = new Float32Array(this.count); // 粒子生命周期
  }
}

代码逻辑说明

  1. positions:粒子的 3D 空间坐标(x, y, z)。
  2. velocities:粒子的速度向量,用于控制运动方向和速度。
  3. colors:粒子的颜色,以 RGB 格式表示,每个粒子独立设置。
  4. sizes:粒子的大小,用于动态变化。
  5. life:生命周期,用于控制粒子消散。

2. 初始化粒子数据

我们为每个粒子分配一个初始位置和随机运动方向。

for (let i = 0; i < this.count; i++) {
  const phi = Math.random() * Math.PI * 2; // 水平方向角度
  const theta = Math.random() * Math.PI; // 垂直方向角度
  const velocity = 2 + Math.random() * 2; // 随机速度
 
  // 计算速度向量
  this.velocities.push(
    velocity * Math.sin(theta) * Math.cos(phi),
    velocity * Math.sin(theta) * Math.sin(phi),
    velocity * Math.cos(theta)
  );
 
  // 设置初始位置
  this.positions[i * 3] = x;
  this.positions[i * 3 + 1] = y;
  this.positions[i * 3 + 2] = z;
 
  // 设置颜色为红色调
  this.colors[i * 3] = 1.0; // 红色
  this.colors[i * 3 + 1] = Math.random() * 0.2; // 随机绿色偏移
  this.colors[i * 3 + 2] = Math.random() * 0.2; // 随机蓝色偏移
 
  // 初始大小和生命周期
  this.sizes[i] = 0.3;
  this.life[i] = 1.0;
}

代码逻辑说明

  1. 随机方向:通过球面坐标计算粒子散射方向。
  2. 颜色随机性:让每个粒子的颜色略有不同,使整体效果更自然。
  3. 生命周期与大小:初始化每个粒子的生命周期和大小,稍后会动态更新。

3. 创建材质与几何

将粒子属性绑定到 Three.js 的 BufferGeometry 对象上,并为其定义材质。

this.geometry.setAttribute("position", new THREE.BufferAttribute(this.positions, 3));
this.geometry.setAttribute("color", new THREE.BufferAttribute(this.colors, 3));
this.geometry.setAttribute("size", new THREE.BufferAttribute(this.sizes, 1));
 
const material = new THREE.PointsMaterial({
  size: 0.3,
  vertexColors: true,
  blending: THREE.AdditiveBlending,
  transparent: true,
  opacity: 0.8,
});
 
this.points = new THREE.Points(this.geometry, material);
scene.add(this.points);

代码逻辑说明

  1. 几何属性:通过 setAttribute 绑定粒子的位置、颜色和大小。
  2. 粒子材质
    • vertexColors: true:允许粒子使用自定义颜色。
    • blending: THREE.AdditiveBlending:粒子叠加效果。
    • transparent: true:支持透明度设置。
  3. 添加到场景:通过 scene.add 将粒子效果添加到 Three.js 场景中。

第三步:更新粒子状态

在这一步,我们将为粒子添加运动、重力效果,并实现逐渐消失的逻辑。粒子在其生命周期内会不断更新位置、大小和透明度,直至完全消散。

1. 更新粒子的逻辑

我们在 Firework 类中定义一个 update 方法,用于逐帧更新粒子状态。粒子的行为包括:

  • 位置更新:根据速度调整粒子位置。
  • 重力效果:粒子会受到向下的重力作用。
  • 生命周期减少:粒子逐渐消散。
  • 尺寸变化:粒子大小随着生命周期减小。
update() {
  let alive = false; // 标记烟花是否仍然活跃
 
  for (let i = 0; i < this.count; i++) {
    if (this.life[i] > 0) {
      alive = true;
 
      // 更新位置
      this.positions[i * 3] += this.velocities[i * 3] * 0.1;
      this.positions[i * 3 + 1] += this.velocities[i * 3 + 1] * 0.1;
      this.positions[i * 3 + 2] += this.velocities[i * 3 + 2] * 0.1;
 
      // 添加重力效果
      this.velocities[i * 3 + 1] -= 0.05;
 
      // 减少生命周期
      this.life[i] -= 0.015;
 
      // 缩小粒子尺寸
      this.sizes[i] = this.life[i] * 0.3;
    }
  }
 
  // 更新几何数据
  this.geometry.attributes.position.needsUpdate = true;
  this.geometry.attributes.size.needsUpdate = true;
 
  return alive;
}
 

代码逻辑说明

  1. 位置更新:粒子会以其速度向指定方向移动,使用 positions[i * 3 + j] 更新每个轴的坐标。
  2. 重力效果:通过 velocities[i * 3 + 1] 对 y 轴速度施加一个固定的负值,模拟重力。
  3. 生命周期和大小
    • 每帧减少 life[i],模拟粒子的逐渐消失。
    • 粒子尺寸 sizes[i] 由生命周期决定,越接近结束越小。
  4. 几何更新:通过设置 needsUpdatetrue 通知 Three.js 更新粒子属性。

2. 清除已消散的粒子

当粒子完全消失后,我们需要将它从场景中移除,并释放相关的资源。
Firework 类中,定义一个 dispose 方法:

dispose() {
  scene.remove(this.points); // 从场景移除
  this.geometry.dispose();   // 释放几何资源
  this.points.material.dispose(); // 释放材质资源
}

第四步:管理烟花的生成与销毁

现在,我们有了基础场景和粒子类,接下来需要一个管理系统来:

  1. 随机生成烟花:在随机位置创建新的 Firework 实例。
  2. 逐帧更新所有烟花:调用 update 方法并移除已消散的烟花。
  3. 监听窗口变化:确保画布始终适配窗口大小。

1. 存储活跃烟花

创建一个数组 fireworks,用于存储当前所有的活跃烟花:

const fireworks = [];
 
// 随机生成烟花
function createRandomFirework() {
  const x = (Math.random() * 2 - 1) * 30; // 随机 x 坐标
  const y = (Math.random() * 2 - 1) * 25; // 随机 y 坐标
  fireworks.push(new Firework(x, y, 0)); // 将新烟花添加到数组
}

2. 动画循环

animate 函数中,每帧执行以下操作:

  1. 随机生成新的烟花。
  2. 更新现有烟花的状态。
  3. 移除消散的烟花。
  4. 渲染场景。
function animate() {
  requestAnimationFrame(animate);
 
  // 随机生成烟花
  if (Math.random() < 0.05) {
    createRandomFirework();
  }
 
  // 更新所有烟花
  for (let i = fireworks.length - 1; i >= 0; i--) {
    const alive = fireworks[i].update();
    if (!alive) {
      fireworks[i].dispose();
      fireworks.splice(i, 1); // 从数组移除已消散的烟花
    }
  }
 
  // 渲染场景
  renderer.render(scene, camera);
}

3. 窗口大小调整

为了适配不同设备,我们需要监听窗口大小变化并调整相机和渲染器:

function onWindowResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}
 
window.addEventListener("resize", onWindowResize, false);

第五步:启动动画

最后,启动动画循环:

animate();

此时,项目已经完成!运行代码后,你将看到屏幕上随机生成烟花,每个烟花都会散射出无数的粒子,粒子在运动过程中逐渐消失。

到此,我们就完成了烟花

代码

github

https://github.com/calmound/threejs-demo/tree/main/yanhua (opens in a new tab)

gitee

https://gitee.com/calmound/threejs-demo/tree/main/yanhua (opens in a new tab)