44 · csm_PointSize:粒子尺寸与内置距离衰减
本章使用 CSM 6.4.0、Three.js r185 的 PointsMaterial。场景是一片随时间上下浮动、尺寸呼吸变化的光点;不需要灯光,也不需要为每个点创建 Mesh。
1. 它接管的是初始尺寸
csm_PointSize 是顶点阶段的 float,仅在 PointsMaterial 下声明和支持。默认是内置 uniform size,实际字符串替换为:
// 原语句 gl_PointSize = size;
gl_PointSize = csm_PointSize;
后面的内置逻辑并没有删除:
#ifdef USE_SIZEATTENUATION
bool isPerspective = isPerspectiveMatrix(projectionMatrix);
if (isPerspective) gl_PointSize *= (scale / -mvPosition.z);
#endif
因此“等价于 gl_PointSize”需要补充执行顺序:在透视相机且 sizeAttenuation 为 true 时,你写的尺寸还会被距离衰减。正交相机不执行这段透视分支。
2. 像素、size 与 DPR
最终 gl_PointSize 使用绘图缓冲像素。Three.js 上传 PointsMaterial 的 size 时会乘 renderer 的像素比,因此 csm_PointSize = size 与内置材质的尺寸单位保持一致。
如果完全替换成自定义数值,就接管了这次换算责任。例子使用 sizeAttenuation: false,把 4~12 CSS 像素乘以 renderer.getPixelRatio(),传给 GPU。这里必须使用渲染器实际像素比:外壳将高 DPR 限制为 2,直接使用 window.devicePixelRatio 会不一致。
只想按比例变化时,可以直接:
csm_PointSize = size * (0.5 + 0.5 * pulse);
此时不要再乘一次 DPR。若启用距离衰减,也不要在自定义代码里重复乘同样的相机距离系数。
3. 实际数据流
csm-points.js 创建一个 BufferGeometry,包含 400 个 position 和同样数量的 aCsmSeed 标量 attribute。每个顶点读取自己的 seed,时间则作为共享 uniform 更新:
float pulse = 0.5 + 0.5 * sin(uCsmPointsTime * 2.0 + aCsmSeed);
csm_Position.y += 0.7 * sin(uCsmPointsTime + aCsmSeed);
csm_PointSize = (4.0 + 8.0 * pulse) * uCsmPixelRatio;
attribute 决定各粒子的相位差,uniform 决定全体动画时间。几何只创建一次,渲染循环不上传所有位置。这里 csm_Position 仍走正常投影,位置和 mvPosition 一致;与直接覆盖 Raw 的方案不同。
4. 方形点怎样变成圆形
点光栅化生成一个方形区域,片元着色器可用内置 gl_PointCoord 获取该区域的 [0,1] 坐标:
void main() {
if (length(gl_PointCoord - vec2(0.5)) > 0.5) discard;
}
例子保留 PointsMaterial 的颜色输出,没有使用自定义 map,所以不存在手动采样与内置 map_particle_fragment 的叠加问题。片元 discard 去掉角落,剩余部分是不透明圆点;它不是柔和透明粒子。
若要柔边,应进一步设计 alpha、transparent、depthWrite 和排序策略,而不是只把边缘写成 alpha=0。多点共享一个 Points 时,Three.js 不会自动逐粒子进行透明排序。需要大量大尺寸发光片时,还应比较实例化 billboard quad 的可控性。
5. 范围与性能限制
硬件限制点的可用尺寸范围,可以在初始化时查询 renderer.getContext().getParameter(gl.ALIASED_POINT_SIZE_RANGE),不要每帧查询。超过上限的尺寸可能被截断;超大点用三角形 billboard 更稳妥。
本例预设包围球半径为 4,覆盖半径 3 与上下 0.7 的位移。粒子变大不意味着 CPU 包围球自动包含全部屏幕面积,边缘剔除仍需按需求留余量。CPU 拾取也仍基于原始几何,应明确交互近似范围。
本例没有投射阴影。Points 的阴影和柔透明轮廓属于额外设计,不应由 CSM 尺寸输出的存在推断为自动支持。
练习(暂不提供答案)
- 改为
csm_PointSize = size * pulse,说明还需不需要 DPR uniform。 - 开启 sizeAttenuation 并移动相机,预测前后排粒子的尺寸差异。
- 用第二个 attribute 控制每个粒子的基础尺寸,保留共享时间动画。
- 实现柔边光点,比较 depthWrite 的两个取值,并记录至少一种透明排序问题。