Three.js案例
smoke

, lmlkity glg# 教你用 Three.js 做一个烟雾粒子系统,视觉效果太上头了!

在 WebGL 的可视化项目中,烟雾、火焰、光晕等特效一直是提升逼真度和沉浸感的关键元素。本文将带你一步步构建一个 动态烟雾粒子系统,基于 Three.js 实现。 ![[smoke 1.gif]]

一、效果预览与核心思路

我们要实现的烟雾系统具备以下特性:

  • 基于 PointsPointsMaterial 渲染;
  • 每个粒子都有大小、透明度、缩放动画;
  • 粒子随时间移动、扩散、逐渐消失;
  • 自定义 Shader 支持控制每个粒子的透明度;
  • 加入随机运动轨迹与生命周期控制。

初始化基础场景

在构建粒子特效之前,我们首先需要搭建好一个基本的 Three.js 场景。包括相机、渲染器、光照、以及用户交互控制器。

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 3, 5);
camera.lookAt(0, 0, 0);

我们将相机稍微抬高一些,并正对原点,让后续生成的烟雾粒子处于最佳观察角度。接着设置渲染器背景为黑色,增强烟雾对比度:

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setClearColor(0x000000, 1);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

为方便调试,我们还加入了坐标网格辅助线,以及轨道控制器,便于旋转场景查看烟雾分布。


粒子材质设计

烟雾的核心,是贴图 + 透明度 + 动态变化。我们先准备一张带透明通道的 PNG 烟雾图: ![[Pasted image 20250515223525.png]]

const texture = new THREE.TextureLoader().load("/public/smoke.png");

然后定义材质,关键在于设置 transparent: true 与关闭深度写入,这样才能让多个粒子正常叠加并呈现柔和感:

const material = new THREE.PointsMaterial({
  size: 3,
  map: texture,
  color: 0xcccccc,
  transparent: true,
  opacity: 0.2,
  depthWrite: false,
});

为什么使用 PointsMaterial

1. 粒子系统天然适配 Points

Three.js 中用于实现粒子效果的基础对象是:

const points = new THREE.Points(geometry, material);

这个 Points 类会将 geometry 中的每一个顶点,渲染为一个可自定义的二维精灵(sprite),并且性能极高,非常适合大量重复对象(如烟雾、雨滴、火花等)。

2. PointsMaterial 是最匹配 Points 的材质类型

它专门设计用于配合 THREE.Points 使用,并提供以下粒子特性:

  • size:控制每个粒子的屏幕大小;
  • map:支持给每个粒子贴图(用于显示烟雾图像);
  • transparent:启用透明通道,使粒子边缘柔和;
  • blending:可设置为 AdditiveBlendingNormalBlending,实现光晕或烟雾叠加效果;
  • depthWrite: false:避免粒子在场景中彼此遮挡,表现出更自然的体积感。

这正好满足烟雾的几个关键需求:半透明、纹理贴图、混合叠加、屏幕空间大小


粒子几何体结构

Three.js 默认的 Points 系统虽然好用,但如果要实现烟雾扩散效果,我们还需要给粒子几何体添加额外属性:

  • a_opacity:控制粒子的逐渐消失;
  • a_size:定义粒子原始尺寸;
  • a_scale:用于动态放大,让烟雾扩散开来。
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.BufferAttribute(new Float32Array(0), 3));
geometry.setAttribute("a_opacity", new THREE.BufferAttribute(new Float32Array(0), 1));
geometry.setAttribute("a_size", new THREE.BufferAttribute(new Float32Array(0), 1));
geometry.setAttribute("a_scale", new THREE.BufferAttribute(new Float32Array(0), 1));

这些属性将在每帧中动态更新,从而控制粒子扩散与透明度衰减。


自定义 Shader

默认的粒子渲染方式对透明度的控制较为粗糙。我们通过 onBeforeCompile 钩子,注入自定义顶点与片元着色器代码,增强控制力:

shader.vertexShader = shader.vertexShader
  .replace(
    "void main() {",
    `
    attribute float a_opacity;
    attribute float a_size;
    attribute float a_scale;
    varying float v_opacity;
    void main() {
      v_opacity = a_opacity;
  `
  )
  .replace("gl_PointSize = size;", "gl_PointSize = a_size * a_scale;");

并在片元着色器中利用 v_opacity 乘以默认透明度,让粒子随时间逐渐淡出:

shader.fragmentShader = shader.fragmentShader
  .replace(
    "void main() {",
    `
    varying float v_opacity;
    void main() {
  `
  )
  .replace(
    "gl_FragColor = vec4(outgoingLight, diffuseColor.a);",
    "gl_FragColor = vec4(outgoingLight, diffuseColor.a * v_opacity);"
  );

粒子生成与生命周期管理

我们将所有粒子放入一个数组中,通过 addNewParticle() 定期生成新粒子,并在 updateGeometry() 中逐帧更新它们的位置、透明度与缩放状态:

const particles = [];
 
const addNewParticle = () => {
  particles.push({
    x: (Math.random() - 0.5) * 5,
    y: Math.random() * 2,
    z: (Math.random() - 0.5) * 5,
    size: 1 + Math.random() * 2,
    opacity: 0.7,
    scale: 0.2,
    speed: {
      x: (Math.random() - 0.5) * 0.05,
      y: 0.05 + Math.random() * 0.1,
      z: (Math.random() - 0.5) * 0.05,
    },
  });
};

updateGeometry 中,每帧让粒子稍微上升一点、变大一点、透明一点,就有了自然的“升腾感”:

p.opacity -= 0.003;
p.scale += 0.005;
p.y += p.speed.y * 0.2;

动画与渲染

我们将更新逻辑写入 animate() 循环中,每帧添加一个或多个新粒子,并重新计算几何体属性:

const animate = () => {
  requestAnimationFrame(animate);
 
  if (Math.random() > 0.5) {
    addNewParticle();
  }
 
  updateGeometry();
  controls.update();
  renderer.render(scene, camera);
};
animate();

这样每次进入场景,都能看到烟雾从地面升起、缓缓扩散,直到完全消失,带来一种不断“流动”的视觉效果。


代码

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