首页 / Three.js 入门教程 / 粒子系统:用 Points 渲染成千上万个点

Three.js 入门教程

粒子系统:用 Points 渲染成千上万个点

本教程共 40 篇 · 第 32 篇 · 更新于 2026-08-14 · 约 8 分钟阅读

Three.jsPointsPointsMaterial粒子系统BufferGeometry纹理粒子

本节目标:学会用 Points 和 PointsMaterial 渲染成千上万个粒子,掌握 size、sizeAttenuation、纹理粒子和 BufferGeometry 自定义粒子位置,能做出星空、雪花、光点这类效果。

前面章节画的都是”面”:网格、球体、平面。这一章换一种东西:点。星空背景、飘雪、萤火虫、爆炸火花,本质都是成千上万个点。three.js 里做这件事的主角叫 Points。

Points 与 Mesh 的区别

Points 和 Mesh 一样是场景里的物体,画法却完全不同:

  • Mesh 用三角形围出表面,模型是一个个面;
  • Points 把每个顶点直接画成一个点,点与点之间不连线、不画面。

所以粒子系统的构成很简单:一个几何体提供顶点位置,一个点材质决定点的样子,new THREE.Points(geometry, material) 组合起来。之后的操作和 Mesh 几乎一样,scene.add(points) 就能看到效果。

Note

Points 继承自 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 的思路一模一样:

  1. 用 Float32Array 生成 count * 3 个数字,每 3 个一组,代表一个点的 x、y、z;
  2. 调用 geometry.setAttribute('position', ...) 把数组装进 BufferGeometry,属性名固定叫 position;
  3. 创建 PointsMaterial,设置颜色和点的大小;
  4. new THREE.Points(geometry, material) 加入场景。

PointsMaterial 的三个核心参数

点材质属性不多,先记住三个:

  • color:粒子颜色,默认白色;
  • size:点的大小,单位是像素,默认 1;
  • sizeAttenuation:是否随距离衰减,默认 true。

sizeAttenuation 值得多说一句。为 true 时,远处的点变小、近处的点变大,符合透视规律,星空、雪花都该开。改成 false,所有点永远一样大,适合数据可视化里的固定大小标记。

Tip

size 的单位是屏幕像素,不是 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,不用换图。

三个透明相关参数的作用:

  1. transparent: true——允许半透明,光斑边缘的渐隐才有效;
  2. depthWrite: false——粒子不写入深度缓冲,透明粒子互相叠加时不会互相遮挡;
  3. blending: THREE.AdditiveBlending——加色混合,亮的地方越叠越亮。
Note

加色混合是粒子最常用的混合模式,火焰、星光、霓虹都是它。代价是不做深度排序,粒子会透到深色物体前面。官方雪花示例(webgl_points_sprites)就用 5 张雪花纹理各渲染一层粒子,共一万个点。

Tip

transparent 和 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 控制点内像素。粒子系统的进阶之路,就是着色器之路。