第 15 课:综合实战——粒子系统
为什么选 Points
星空、烟花、萤火虫都需要很多独立的小图形。给每个点创建一个 Mesh 会增加对象数和 draw call;一个 BufferGeometry 加一个 THREE.Points 更适合这些例子。JavaScript 只在初始化时创建 position、aSize、aPhase、aLife、aVelocity 数组并上传 GPU。渲染循环更新统一时间,不逐帧重建几何体。每粒子数据适合 attribute,全体共享数据适合 uniform。
顶点到片元的数据流
顶点 Shader 从 attribute 取得一个点的起点、尺寸、出生相位、寿命和速度。它在局部空间算出当前位置,经 modelViewMatrix 与 projectionMatrix 变换后写入 gl_Position,并用 gl_PointSize 设置屏幕像素大小。vAlpha、vSeed 作为 varying 传到片元;点精灵的片元可读 gl_PointCoord,以中心距离做柔边圆点。由于一个点通常只有一个顶点,这里的 varying 在整个点精灵内基本保持同一粒子值,真正的点内变化来自 gl_PointCoord。
第 1 题先显示纯色点。第 2 题把 sin(uTime * 1.5 + aPhase * 2π) 加到 Y:相位不同的点不会齐刷刷同时升降。第 3 题让 aSize 决定大小;近大远小的缩放由观察空间 Z 控制,但最终仍应给大小设合理范围,避免巨大点片造成 overdraw。
生命周期、烟花和噪声路径
生命周期用 fract(time / life + phase) 变成 0–1 周期。相位分散会让画面中同时存在新生和消退粒子。星空用寿命或相位调亮度;若需要更自然的变化,增加独立速度和色温。烟花用解析运动 position = velocity * age + 0.5 * gravity * age²。练习里的重力系数直接写成 -0.28 * age²,对应 Y 向下的加速度 -0.56 世界单位每秒平方。按“重新发射”只更新 uLaunch,顶点重新从中心计算运动,无需改写每个顶点缓冲。
萤火虫沿一维平滑噪声函数构造的 X/Y 两条路径移动:取相邻格点的伪随机值,用平滑插值连接,可避免每格边界跳变。不同相位让轨迹独立;另一个正弦函数控制呼吸亮度。这里的路径适合视觉效果,不是物理速度积分。需要碰撞或复杂群体行为时,可改用 CPU 更新少量粒子,或在更多粒子时使用 GPU 模拟纹理。
性能、资源与故障
透明柔边点使用 additive blending 和 depthWrite: false,可让发光叠加,但重叠区域会变亮。点越大、密度越高,片元 overdraw 越多;粒子数不是唯一成本指标。限制像素比、粒子尺寸和视野内数量,再固定设备与画布尺寸比较性能。若粒子出现方块边,检查 gl_PointCoord 圆形遮罩;若透明排序异常,检查混合模式和深度写入;若点击重发射无反应,检查 uLaunch 与 uTime 是否同一时间基准。
练习与验收
| 难度 | 题目 | 可观察验收 |
|---|---|---|
| ⭐ | Points 纯色粒子 | 单个 Points 对象显示数百个青色点 |
| ⭐⭐ | 时间驱动上下浮动 | 上下往复;暂停后位置冻结 |
| ⭐⭐ | 随机大小 attribute | 大小明显不一;aSize 决定点尺寸 |
| ⭐⭐⭐ | 随机生命周期 attribute | 同时有亮点和暗点,数秒后继续循环 |
| ⭐⭐⭐ | 闪烁星空 | 深色背景、独立闪烁,无粒子连线 |
| ⭐⭐⭐ | 烟花爆发与重力 | 每次点击重新从中心扩散、下落并淡出 |
| ⭐⭐⭐⭐ | 噪声路径萤火虫 | 路径连续且呼吸不同步,运行时无突然跳点 |