, lmlkity glg# 教你用 Three.js 做一个烟雾粒子系统,视觉效果太上头了!
在 WebGL 的可视化项目中,烟雾、火焰、光晕等特效一直是提升逼真度和沉浸感的关键元素。本文将带你一步步构建一个 动态烟雾粒子系统,基于 Three.js
实现。
![[smoke 1.gif]]
一、效果预览与核心思路
我们要实现的烟雾系统具备以下特性:
- 基于
Points
和PointsMaterial
渲染; - 每个粒子都有大小、透明度、缩放动画;
- 粒子随时间移动、扩散、逐渐消失;
- 自定义
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
:可设置为AdditiveBlending
或NormalBlending
,实现光晕或烟雾叠加效果;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)