粒子系统:用 Points 渲染成千上万个点
本教程共 40 篇 · 第 32 篇 · 更新于 2026-08-14 · 约 8 分钟阅读
本节目标:学会用 Points 和 PointsMaterial 渲染成千上万个粒子,掌握 size、sizeAttenuation、纹理粒子和 BufferGeometry 自定义粒子位置,能做出星空、雪花、光点这类效果。
前面章节画的都是”面”:网格、球体、平面。这一章换一种东西:点。星空背景、飘雪、萤火虫、爆炸火花,本质都是成千上万个点。three.js 里做这件事的主角叫 Points。
Points 与 Mesh 的区别
Points 和 Mesh 一样是场景里的物体,画法却完全不同:
- Mesh 用三角形围出表面,模型是一个个面;
- Points 把每个顶点直接画成一个点,点与点之间不连线、不画面。
所以粒子系统的构成很简单:一个几何体提供顶点位置,一个点材质决定点的样子,new THREE.Points(geometry, material) 组合起来。之后的操作和 Mesh 几乎一样,scene.add(points) 就能看到效果。
NotePoints 继承自 Object3D。position、rotation、scale、visible 这些变换属性全都可用,可以像操作普通物体一样旋转、移动整片粒子。
第一个粒子示例
先看一个能直接运行的完整例子:2000 个随机分布的粒子,鼠标拖动可以旋转视角。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>32 粒子系统</title>
<style>
body { margin: 0; overflow: hidden; background: #0a0a1a; }
</style>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.185.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.185.0/examples/jsm/"
}
}
</script>
</head>
<body>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// 1. 生成 2000 个随机位置
const count = 2000;
const positions = new Float32Array(count * 3);
for (let i = 0; i < count * 3; i += 3) {
positions[i] = (Math.random() - 0.5) * 40;
positions[i + 1] = (Math.random() - 0.5) * 40;
positions[i + 2] = (Math.random() - 0.5) * 40;
}
// 2. 位置数据装进几何体
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
// 3. 粒子材质
const material = new THREE.PointsMaterial({
color: 0x66ccff,
size: 0.4,
sizeAttenuation: true
});
// 4. 用 Points 组合几何体和材质
const points = new THREE.Points(geometry, material);
const scene = new THREE.Scene();
scene.add(points);
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 200);
camera.position.z = 25;
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
renderer.setAnimationLoop(() => {
points.rotation.y += 0.002; // 整片粒子云缓慢旋转
controls.update();
renderer.render(scene, camera);
});
</script>
</body>
</html>
代码分四步,和创建 Mesh 的思路一模一样:
- 用 Float32Array 生成
count * 3个数字,每 3 个一组,代表一个点的 x、y、z; - 调用
geometry.setAttribute('position', ...)把数组装进 BufferGeometry,属性名固定叫 position; - 创建 PointsMaterial,设置颜色和点的大小;
new THREE.Points(geometry, material)加入场景。
PointsMaterial 的三个核心参数
点材质属性不多,先记住三个:
color:粒子颜色,默认白色;size:点的大小,单位是像素,默认 1;sizeAttenuation:是否随距离衰减,默认 true。
sizeAttenuation 值得多说一句。为 true 时,远处的点变小、近处的点变大,符合透视规律,星空、雪花都该开。改成 false,所有点永远一样大,适合数据可视化里的固定大小标记。
Tipsize 的单位是屏幕像素,不是 3D 单位。相机拉远,粒子本身不会缩小,只有 sizeAttenuation 开着才会。另外硬件对点大小有上限(
gl.ALIASED_POINT_SIZE_RANGE):上限因设备而异,桌面常见 255~1024 像素,部分移动设备低至 64,写超大 size 会被截断。
用 BufferGeometry 自定义粒子位置
粒子系统的灵魂在几何体。想让粒子组成什么形状,就往 position 数组里填什么坐标。比如球壳:
const count = 3000;
const positions = 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 r = 10;
positions[i * 3] = r * Math.sin(phi) * Math.cos(theta);
positions[i * 3 + 1] = r * Math.cos(phi);
positions[i * 3 + 2] = r * Math.sin(phi) * Math.sin(theta);
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
球坐标公式:给定半径 r、水平角 theta、垂直角 phi,就能算出球面上的一点。循环里生成 3000 组角度,得到 3000 个均匀分布在球面上的点。把 r 换成 Math.random() * 10,就是实心球云。
Note如果运行中直接改 position 数组的内容(比如做粒子流动),改完要标记
geometry.attributes.position.needsUpdate = true,渲染器才知道数据变了。
让粒子动起来
动整片粒子最简单,改 Points 的变换属性就行:
points.rotation.y += 0.002; // 每帧转一点
points.position.x = Math.sin(clock.getElapsedTime()) * 2;
这比逐顶点改坐标便宜得多,也够做大多数”粒子云漂浮”效果。逐顶点动画(每个粒子按自己的速度飞)要等第 33 章的着色器,那是粒子系统的进阶玩法。 注意旋转的中心:Points 的 rotation 绕场景原点转。想绕某个点旋转,把 Points 放进 Group,移动 Group 到目标点,再旋转 Points 自身;想让粒子云绕自己的中心转,生成位置数据时把中心放在原点。旋转和位移结合,能做出行星环、漩涡这类效果。
纹理粒子:把方形点变成光斑
默认粒子是纯色小方块,观感一般。给 map 属性贴一张图,粒子就变成图的样子:圆形光斑、雪花、星星都行。
// 用 canvas 画一张圆形渐变图,当粒子纹理
const canvas = document.createElement('canvas');
canvas.width = 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.5, 'rgba(255, 255, 255, 0.6)');
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 64, 64);
const texture = new THREE.CanvasTexture(canvas);
const material = new THREE.PointsMaterial({
size: 1.2,
map: texture, // 粒子贴图
color: 0xff8844, // 贴图会被 color 染色
transparent: true, // 半透明边缘需要
depthWrite: false, // 关闭深度写入,避免透明排序问题
blending: THREE.AdditiveBlending // 加色混合
});
纹理贴图会和 color 相乘:白贴图乘橙色,得到橙色光斑。想换颜色就改 color,不用换图。
三个透明相关参数的作用:
transparent: true——允许半透明,光斑边缘的渐隐才有效;depthWrite: false——粒子不写入深度缓冲,透明粒子互相叠加时不会互相遮挡;blending: THREE.AdditiveBlending——加色混合,亮的地方越叠越亮。
Note加色混合是粒子最常用的混合模式,火焰、星光、霓虹都是它。代价是不做深度排序,粒子会透到深色物体前面。官方雪花示例(webgl_points_sprites)就用 5 张雪花纹理各渲染一层粒子,共一万个点。
Tiptransparent 和 alphaTest 都能处理粒子贴图的透明边缘。transparent 做柔和渐隐(开销大一点),alphaTest 做硬裁剪(快,适合边缘锐利的贴图)。粒子贴图边缘干脆时优先 alphaTest,性能更好。
粒子突然消失了?
粒子动画有个高频坑:运行中改了 position 数组,粒子飞出几何体原始的包围盒,被视锥剔除(frustum culling)整片消失。症状是”粒子转着转着突然没了”。
points.frustumCulled = false; // 关闭视锥剔除
动态改顶点坐标的粒子,先关掉这个属性。另外 Points 支持 raycast,第 23 章的 Raycaster 可以直接拾取粒子,命中精度按点的大小计算。
一万个点也只有一次绘制
粒子系统快的秘诀:所有粒子共用一个几何体、一个材质,渲染时只产生一次 draw call。一万个粒子是一次绘制,一万个独立的小球网格是一万次绘制。GPU 画点比画三角形便宜得多,所以星空、尘埃这类数量大、个体小的效果都交给 Points。
Tip想给每个粒子单独的大小、颜色(比如渐变星空),PointsMaterial 不够用。第 33 章的 ShaderMaterial 才是答案:顶点着色器里用 gl_PointSize 控制大小,片元着色器里用 gl_PointCoord 控制点内像素。粒子系统的进阶之路,就是着色器之路。