2、第一个粒子系统:创建Points对象、使用PointsMaterial、设置粒子大小和颜色

好,咱们直接动手。这一章的目标很简单——在屏幕上撒一把粒子,让它们亮起来。

你可能会想,粒子系统是不是很复杂?其实不然。Three.js 把最核心的部分封装成了两个东西:PointsPointsMaterial。说白了,Points 就是一堆点的集合,PointsMaterial 就是控制这些点长什么样的材质。

2.1 创建 Points 对象

先看代码,再解释。

// 1. 创建几何体,存放粒子位置
const geometry = new THREE.BufferGeometry();
const count = 1000; // 粒子数量

// 2. 生成随机位置
const positions = new Float32Array(count * 3);
for (let i = 0; i < count * 3; i += 3) {
    positions[i] = (Math.random() - 0.5) * 10;     // x
    positions[i + 1] = (Math.random() - 0.5) * 10; // y
    positions[i + 2] = (Math.random() - 0.5) * 10; // z
}

// 3. 把位置数据塞进几何体
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));

// 4. 创建材质
const material = new THREE.PointsMaterial({
    color: 0x88ccff,
    size: 0.1
});

// 5. 合成 Points 对象
const points = new THREE.Points(geometry, material);
scene.add(points);

嗯,这里有个关键点——BufferGeometry。我刚开始学的时候,总觉得这东西很绕。其实你就把它想象成一个「数据容器」,专门用来存顶点信息的。

为什么用 Float32Array?因为 GPU 只认这种紧凑的二进制数据。你用普通数组传进去,Three.js 还得帮你转一遍,多此一举。

我的习惯:粒子数量少于 5000 时,直接用 Math.random() 生成位置。超过这个数,我会用一些噪声算法(比如 Perlin Noise)来分布,避免粒子扎堆。

2.2 使用 PointsMaterial

PointsMaterial 和普通的 MeshMaterial 不一样。它没有光照计算,没有阴影,说白了就是「纯色点」。但正因为它简单,所以性能极好。

常用的属性就这几个:

属性 类型 说明
color Color 粒子颜色,支持十六进制或 Color 对象
size Number 粒子大小,单位是像素(默认)
sizeAttenuation Boolean 是否根据距离衰减大小,默认 true
opacity Number 透明度,0~1
transparent Boolean 启用透明度,必须设为 true 才能用 opacity

你想想看,如果 sizeAttenuation 设为 false,远处的粒子和近处的一样大,看起来就像贴在屏幕上一样,很假。所以我一般保持默认 true。

2.3 设置粒子大小和颜色

大小和颜色是最直观的两个参数。但这里有个坑——size 的单位是什么?

默认情况下,size 的单位是「像素」。也就是说 size: 0.1 并不是 0.1 个世界单位,而是 0.1 个像素。这会导致一个问题:在不同分辨率的屏幕上,粒子看起来大小不一样。

我曾经在 4K 屏上调试好的粒子大小,换到 1080p 的笔记本上,粒子变得巨大无比……嗯,后来我学乖了,要么用 sizeAttenuation 配合相机距离来调,要么干脆用纹理贴图来控制大小。

颜色方面,你可以给所有粒子统一颜色:

const material = new THREE.PointsMaterial({
    color: 0xff6600, // 橙色
    size: 0.2
});

也可以给每个粒子单独的颜色。这需要额外传一个 color 属性:

const colors = new Float32Array(count * 3);
for (let i = 0; i < count * 3; i += 3) {
    colors[i] = Math.random();     // r
    colors[i + 1] = Math.random(); // g
    colors[i + 2] = Math.random(); // b
}
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));

const material = new THREE.PointsMaterial({
    size: 0.15,
    vertexColors: true // 启用顶点颜色
});
注意:启用 vertexColors 后,material.color 会被忽略。每个粒子的颜色完全由 geometry 中的 color 属性决定。

2.4 避坑指南

  • 粒子不显示? 检查一下相机位置。粒子默认在原点附近,如果相机离得太远或者看向别处,当然看不到。
  • 粒子太大或太小? 调 size 的同时,别忘了 sizeAttenuation。如果关闭了衰减,远处的粒子会显得特别大。
  • 颜色不对? 确认 color 用的是十六进制格式(0xrrggbb),而不是 CSS 字符串('#ff6600')。

我记得有一次,一个学员跑来问我:「老师,我的粒子全是黑色的!」我一看代码,他把 color 写成了 '#ff0000'。Three.js 的 Color 构造函数虽然能解析字符串,但 PointsMaterial 的 color 属性期望的是数值类型。这种小细节,不注意就会卡半天。

2.5 完整示例

最后,给你一个可以直接跑起来的完整代码。复制到你的项目里,看看效果:

import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.z = 5;

const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 粒子系统
const geometry = new THREE.BufferGeometry();
const count = 2000;
const positions = new Float32Array(count * 3);
for (let i = 0; i < count * 3; i += 3) {
    positions[i] = (Math.random() - 0.5) * 20;
    positions[i + 1] = (Math.random() - 0.5) * 20;
    positions[i + 2] = (Math.random() - 0.5) * 20;
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));

const material = new THREE.PointsMaterial({
    color: 0x44aaff,
    size: 0.15,
    sizeAttenuation: true
});

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

// 控制器
new OrbitControls(camera, renderer.domElement);

function animate() {
    requestAnimationFrame(animate);
    points.rotation.y += 0.001;
    renderer.render(scene, camera);
}
animate();

运行起来,你会看到一片蓝色的星云在缓缓旋转。这就是你的第一个粒子系统。

下一章,我们会给粒子加上纹理,让它们不再是单调的圆点,而是星星、火花、或者任何你想要的形状。