12 随机分布与运动积分:同一规则生成喷泉、雨和爆炸

前置知识:第 11 章的秒制更新、发射率累计与池化。
本章示例:圆盘采样对照 · 喷泉、雨、爆炸 · 分布源码 · 运动源码。
预期画面:相同随机数下,r = R√u 的圆盘分布较均匀,r = Ru 中心过密;切换喷泉、雨和爆炸后,同一个积分器呈现三种轨迹,风和重力对已出生的粒子也生效。

先看现象:随机并不自动意味着“均匀”

在圆盘页,两侧各显示默认 1200 个点,使用同一组随机数 u 和角度,只改变半径公式。左侧半径 R/2 的浅色内圈内比例会围绕 25% 波动,右侧围绕 50% 波动。反复点击“重新取样”,具体数值会变化,长期趋势不会。把点数增大,比例通常更接近各自的理论值。这里的“面积均匀”指任意等面积区域有相同的落点概率,不保证有限样本视觉上完全平均。

运动页的三种效果也并非三个积分器。粒子出生时的位置与初速度不同,之后都按“加速度改变速度,速度改变位置”更新。喷泉从下方向上发射、雨从顶部向下发射、爆炸从中心径向发射。调大向下加速度,喷泉更快落回、雨更快下坠、爆炸后期更明显下弯;调风可见三者整体横向偏移。

1. 先确定随机变量的空间与单位

Math.random() 给出近似均匀的 u ∈ [0,1),但“均匀的 u”不等于“变换后的点在面积上均匀”。需要先问要均匀的是哪种量:角度、半径、面积、体积、速度大小,还是出生时间。

简单区间 [a,b) 的均匀采样为 a + (b-a)u。二维方向可令 θ = 2πu,再用 (cos θ, sin θ) 得到单位方向。若要各方向等概率,角度应在完整圆周上均匀;但独立地均匀取 x,y 再归一化,并不会得到相同的角度分布。

示例圆盘用两个随机数:一个控制半径、一个控制角度。比较两种半径规则时复用相同的 u, θ,所以结果差异确实来自规则,而不是另一批样本。示例还用固定种子生成伪随机序列,便于重现;每点仍是不同样本,点击重新取样会推进序列。

2. 为什么圆盘面积均匀要用 √u

半径为 r 的内圆面积是 πr²,总圆盘面积是 πR²。面积均匀时,点落在半径 r 内的概率必须是面积比:

P(半径 ≤ r) = πr² / πR² = (r/R)²

令均匀随机数 u 等于这个累计概率,解得 r = R√u。再令 θ = 2πv,坐标为:

const u = Math.random();
const theta = Math.random() * Math.PI * 2;
const r = R * Math.sqrt( u );
const x = centerX + r * Math.cos( theta );
const y = centerY + r * Math.sin( theta );

代入 r = R/2 得 P = 1/4。若误用 r = Ru,则 P(r ≤ R/2) = P(u ≤ 1/2) = 1/2,一半样本挤进只占四分之一面积的内圈。这个推导也适用于圆环:内半径 Rmin、外半径 Rmax 时,r = √(Rmin² + u(Rmax²−Rmin²))。球体体积均匀则涉及立方根,但角度采样也要相应处理,不能只替换根号。

本页为几何分布对照使用 THREE.Points,位置数组只在取样或改变点数时重建,旧 BufferGeometry 调用 dispose()。这不是每帧移动粒子的 CPU 路线;第 13—15 章才集中讲动态属性数组。点材质的 size 在这里作为屏幕点大小,sizeAttenuation:false;r174 的 WebGL 路径已经处理像素比,自定义着色器写 gl_PointSize 时需单独考虑。

3. 初速度的随机分布塑造效果

对爆炸而言,方向常用 θ ∼ Uniform(0,2π),速度大小可以固定,也可在区间内随机。示例选择 speed = 1.45 + u × 1.0 世界单位/秒:

const theta = Math.random() * Math.PI * 2;
const speed = 1.45 + Math.random() * 1.0;
p.vx = Math.cos( theta ) * speed;
p.vy = Math.sin( theta ) * speed;

这使二维方向大致均匀,并让外圈略不整齐。速度大小若均匀采样,指的是“每段相等长度的速度区间同概率”,并非某个二维速度向量区域面积均匀。效果制作往往有意偏置:火焰集中向上、烟向上但扩散、雨的横向速度很小。随机性是可设计的分布,而不是所有参数都乘一次 Math.random()。

寿命和尺寸也有分布。喷泉示例寿命为 1.9—2.45 秒、出生 y=-1.45,雨的出生 x 横跨约 5.5 世界单位、y=1.7,爆炸固定从原点出生。切换模式时清空旧粒子,避免前一种运动配方与新一种混在一起。每次获取复用槽位都重新设置寿命、速度、位置、尺寸、颜色与透明度。

4. 运动积分:从连续规律到逐帧更新

连续运动可写为 dv/dt = a、dx/dt = v。示例用半隐式 Euler(semi-implicit Euler),先更新速度,再用新速度更新位置:

p.vx += ax * dt;
p.vy += ay * dt;
p.sprite.position.x += p.vx * dt;
p.sprite.position.y += p.vy * dt;

其中位置是世界单位,速度是世界单位/秒,ax、ay 是世界单位/秒²,dt 是秒。控件的“向下加速度”数值 g 被转成 ay = -g;“水平加速度”直接作为 ax。先速度后位置和显式 Euler 的先位置后速度都属近似法,但在相同时间步长下结果不同;不要在一段模拟中无意混用。

若加速度恒定,解析解可作基准:x(t)=x0+v0t+½at²、v(t)=v0+at。半隐式 Euler 的单步位置是 x1=x0+(v0+aΔt)Δt,解析式是 x0+v0Δt+½aΔt²,单步位置误差为 ½aΔt²。步长越大,累计轨迹越可能偏离;改变重力、碰撞或风场时,解析式未必方便,数值积分更灵活。

dt 限幅为 0.05 秒,以免页面后台恢复后一次大步跨过整个场景。代价是省略了部分墙钟时间。更严格的物理或碰撞系统通常采用固定子步、最大追赶次数及明确的暂停策略;单纯增加帧率或缩小步长不能自动修复不合适的出生分布。

5. 三套出生配方,共用一套更新

效果 出生位置 初速度 加速度与结果
喷泉 底部窄区域 向上,少量横向扰动 ay<0,先升后落;风使轨迹倾斜
雨 顶部横向铺开 向下,微小横向扰动 ay<0,越来越快;风使雨线斜落
爆炸 中心一点 角度均匀的径向速度 起初外扩,之后受重力下弯

喷泉每秒请求 55 个、雨每秒 76 个;爆炸一次请求 120 个,自动重复时等全部死亡后再等约 0.6 秒。固定池容量为 180。粒子寿命到期或当前位置已经低于 y=-1.95 时回收。这个地面阈值只是示例的删除边界,不是碰撞响应。调重力和风只改变之后的加速度,已经出生粒子的初速度不会重新抽样;点击“清空并重新开始”才让当前配方从头发射。

渲染仍是一粒子一个 Sprite:软圆贴图、加法混合、depthTest:true、depthWrite:false。这些设置有利于演示发光点,但不负责物理运动;真实雨滴可能需要拉伸形状,烟则可能改用普通 Alpha 混合并处理排序。第 13 章会把点批量放入一个 Points 对象,第 16 章再讨论着色器里的解析式运动与有状态更新的边界。

常见问题

为什么点数越多越接近 25%,但仍不是恰好 25%? 采样有统计波动。固定样本数只给出概率附近的结果,不保证每个子区域都获得精确比例。若艺术需求是严格数量,应分层或分区控制采样,而非仅靠独立随机。

为什么每粒子的大小和速度都随机,画面仍可能很整齐? 如果所有粒子从同一点同一时间出生,且角度或寿命变化很少,整体仍可形成规则边界。要分别设计出生时间、位置、方向、速度和生命周期曲线。

为什么改风后旧粒子也转向? 风在更新阶段读取,不是出生时写死。已出生粒子的速度逐帧增加 ax × dt;若只希望风影响新粒子,应在出生时设置初速度偏移,或给每粒子保存自己的加速度。

完成第 12 章练习。接着读第 13 章:THREE.Points 基础、第 14 章:Billboard 与纹理图集和第 15 章:动态 BufferGeometry。参考:BufferGeometry、Points。