Page 42-43(目标与物理模型)
内容:目标是用弹性碰撞定律模拟
N个运动粒子的行为。物理模型是“硬球模型”:
粒子是圆盘,有位置、速度、质量和半径。
只存在弹性碰撞(粒子之间、粒子与墙壁)。
没有其他外力(重力、摩擦力忽略)。
科学意义:这个模型可以推导出宏观性质(温度、压力)与微观运动的关系(麦克斯韦-玻尔兹曼分布、布朗运动)。
你的关注点:这是一个动态系统,你需要在时间轴上处理粒子状态的变化。Page 44-47(时间驱动模拟——为什么它不行)
第44-45页:展示了一个简单的
Ball类,每次移动固定时间步长dt,遇到墙壁就反弹。但缺少粒子间的碰撞检测。
第46页:时间驱动模拟的流程:离散时间步长dt,每一步都检查所有粒子对是否重叠,如果重叠则回退到碰撞时刻,更新速度,继续。
第47页(关键缺陷):
问题1:每一步需要检查
~N²/2对粒子,如果dt很小,计算量爆炸。问题2:如果
dt太大,两个粒子可能在两次检查之间穿过彼此而没有重叠,导致完全错过碰撞。
结论:时间驱动模拟在粒子数量大或需要高精度时不可行。Page 48(事件驱动模拟——核心思想)
物理策略:
只在“有事情发生”时前进时间(两次碰撞之间,所有粒子做匀速直线运动)。
维护一个按时间排序的优先队列,里面存放所有未来的潜在碰撞事件。
每次从队列中取出时间最小的事件(即下一次碰撞),处理它,然后预测这次碰撞可能引发的新事件,重新插入队列。
你的关注点:这正是在 DFT/逻辑仿真中处理“信号翻转事件”的经典模式。优先队列在这里扮演“时间调度器”的角色。
Page 49(粒子-墙壁碰撞预测)
物理公式:计算粒子到墙壁的距离,除以速度在垂直方向的分量,得到碰撞时间。
PPT上给出了公式,但物理细节不重要,你只需要知道:timeToHitWall()返回一个double值,表示“从现在开始,多少时间后会撞墙”。Page 50-52(粒子-粒子碰撞预测与解析)
PPT 给出了完整的向量公式(相对速度、相对位置、判别式
d),用于计算两个圆盘何时相撞,以及碰撞后的速度变化。
但是,PPT 在第51页用大字标注了:"Important note: This is high-school physics, so we won't be testing you on it!"(这是高中物理,不会考你。)
所以这些公式你完全不需要记忆。你只需要知道:
timeToHit(Particle that)返回两个粒子碰撞的时间(如果它们正在远离,则返回无穷大);bounceOff(Particle that)更新两个粒子的速度向量。Page 53-54(Particle 数据类型的骨架)
展示了
timeToHit和bounceOff的实现代码。代码较长,但核心逻辑已经被上面的公式覆盖。物理细节同样不需要深究。Page 55(事件驱动模拟的初始化和主循环)——这一页是算法的核心
初始化:
把所有粒子与墙壁的潜在碰撞加入优先队列。
把所有粒子与粒子的潜在碰撞加入优先队列。
这些事件都是“潜在”的——因为如果某个碰撞先发生了,后续的事件可能就无效了。
主循环:
从优先队列中取出最小时间的事件(
t)。检查事件是否已失效:如果该事件涉及的粒子自事件创建以来已经发生过其他碰撞,则这个事件就是无效的,直接丢弃。
将所有粒子按直线轨迹移动到时间
t。更新碰撞粒子的速度(调用
bounceOff)。只针对刚碰撞的粒子,重新预测它们未来可能的新碰撞(粒子-墙壁、粒子-粒子),并插入优先队列。
关键物理事实:为什么只预测“刚碰撞的粒子”?因为其他粒子的轨迹没有改变,它们未来的碰撞时间依然有效。只有速度发生了变化的粒子,才会改变未来事件的时钟。这正是“事件驱动”高效的原因——你只更新受影响的那一小部分。
Page 56(Event 数据类型)
约定:
两个粒子都不为空 → 粒子-粒子碰撞。
一个粒子不为空 → 粒子-墙壁碰撞。
两个都为空 →重绘事件(用来刷新屏幕动画)。
Event类还存储了事件创建时,两个粒子的碰撞计数器(countA和countB)。用来检测事件是否失效(如果当前粒子计数器与存储的值不同,说明粒子在事件被处理前已经撞过了)。Page 57-58(CollisionSystem 框架代码)
predict(a)方法:为给定粒子a,计算它与所有其他粒子的碰撞时间,以及与墙壁的碰撞时间,将它们作为新事件插入优先队列。simulate()方法:就是 Page 55 描述的主循环。Page 59-62(模拟可视化示例)
展示了几种不同的粒子碰撞模拟动画截图:
随机粒子
台球布局(
billiards.txt)大质量粒子撞击小粒子群
这些只是运行结果展示,没有新的算法内容。这一节你需要记住的唯一算法机制:
“当事件发生时,只重新计算受影响的粒子的未来事件,其他事件保持不变。用碰撞计数器标记事件的有效性,避免在事件触发前从堆中删除它们。”
一、运动公式推导
Q:我想看看这页的“高中物理”都是怎么来的
A:
1. 物理建模:把两个粒子简化为“一个粒子”的运动
你把粒子
i和j的速度、位置相减,得到相对速度Δv和相对位置Δr。
物理含义:你可以想象粒子
i静止不动,粒子j以Δv运动。它们能否相撞,取决于Δv是否指向Δr(即二者是否在接近)。2. 碰撞条件:它们中心距离等于
σ
3. 展开成标准二次方程
4. PPT 中的判别式
d就是二次方程的判别式b² - 4ac
5. 为什么条件
Δv · Δr >= 0直接返回∞?如果
Δv · Δr >= 0:
Δv · Δr是相对速度在相对位置方向上的投影。如果
>= 0,说明两个粒子正在远离或相对静止。它们当前的距离不会减小,因此无论判据d如何,t只能是负值(过去的时间)或无解。所以直接返回无限大(未来不会发生碰撞)。6. 最终取根公式
二、关于PQ的用法,初始化
Q:好吧,让我们来探讨一下这里的PQ是怎么使用的。首先我具有如此之多的粒子,并且他们中有一些是静止的,有一些是具有初始速度的,被放置在一个单位面积的空间中。那么这个时候pq里面会有什么呢?你之前说pq里装的是可能的碰撞,但是在这个初始化场景下,我们并不知道哪些是可能的碰撞呀?简要讲一讲。
A:在初始化时,PQ 里会被填入“所有理论上可能发生的碰撞”,即使我们不知道它们是否真的会发生。这个“强行计算并插入,后续通过失效检测过滤掉无效事件”的设计,是这个算法能够避免复杂事件管理的关键。
1. 初始化阶段:强制计算所有潜在的“第一次碰撞”
在模拟开始的那一刻(
t=0),所有粒子的位置和速度都是已知的。虽然我们不知道谁会先撞上谁,但我们可以假设没有其他粒子干扰,计算每个粒子与所有其他粒子的“理论首次碰撞时间”。具体操作:
粒子-粒子:对于每一对粒子
(i, j),调用timeToHit(particles[j]),计算出它们在假设直线运动下首次碰撞的时间dt。如果dt是有限值(不是无穷大),就将这个事件(t=dt, a=i, b=j)插入优先队列。粒子-墙壁:对于每个粒子
i,调用timeToHitVerticalWall()和timeToHitHorizontalWall(),分别计算它撞到四面墙的时间,如果有限,也插入优先队列。物理后果:初始化阶段的计算量是
O(N²)(因为要检查所有粒子对)。对于粒子数量庞大的模拟,这确实是一次性开销,但也正是这个模型能正确运行的代价。2. 为什么可以“盲目地”把所有可能的碰撞都塞进去?
因为你不知道哪些碰撞会被“截胡”。比如粒子 A 和 B 预计在
t=5时相撞,但粒子 A 在t=2时先撞了墙,速度改变了,那么 A 和 B 在t=5的碰撞就永远不会发生。这个算法的处理方式是:它不会在
t=2时去堆里删除那个失效的(A,B, t=5)事件。它只是在t=2处理完墙碰撞后,重新计算 A 和 B 的新碰撞时间,并把新事件插入堆中。当时间推进到t=5时,从堆顶弹出(A,B, t=5),然后检查它是否失效:
检查 A 的碰撞计数器(
countA)是否自该事件创建以来增加过。如果增加了(意味着 A 在
t=2撞过墙),则(A,B)事件失效,直接丢弃,不处理。如果没有增加,则事件有效,处理真正的碰撞。
结论:PQ 在初始化阶段装载的是“在没有干扰情况下,所有可能发生的碰撞候选事件”。这种“预测所有可能,忽略失效事件”的策略,是事件驱动模拟能够用堆高效处理的核心原因——你永远不需要在堆中删除任意元素,只需要靠计数器自然淘汰它们。这正是你之前看到的“惰性删除(Lazy Deletion)”或“幽灵事件”模式在粒子碰撞模拟中的具体体现。