模拟多粒子碰撞的高效方法?

16

我想编写一个小型程序,模拟许多粒子碰撞,首先在2D中开始(稍后我将扩展到3D),以便(在3D中)模拟收敛到Boltzmann分布的情况,并查看分布如何在2D中演变。

我还没有开始编程,所以请不要要求提供代码示例,这是一个相当通用的问题,应该帮助我开始。对于这个问题,我对其背后的物理学没有问题,问题在于我将不得不模拟至少200-500个粒子,以获得相当好的速度分布。我想实时完成。

现在,对于每个时间步长,我将首先更新所有粒子的位置,然后检查碰撞,以更新新的速度矢量。然而,这包括了很多检查,因为我必须查看每个单独的粒子是否与每个其他粒子发生碰撞。 我发现这篇文章与同样的问题相关,那里使用的方法也是我能想到的唯一方法。但是,我担心这在实时上效果不太好,因为它将涉及太多的碰撞检查。

现在:即使这种方法在性能上有效(达到40fps),有人可以想出避免不必要的碰撞检查的方法吗?

我的想法是将板子(或在3D中:空间)分成至少具有粒子直径尺寸的维度,然后实现一种方式,只有在网格中的两个粒子的中心处于相邻的正方形内时才检查碰撞...

我很乐意听到更多想法,因为我希望尽可能增加粒子数量,并仍然进行实时计算/模拟。

编辑:所有碰撞都是纯弹性碰撞,没有其他力对粒子进行作用。初始情况由用户选择的变量来确定,以选择随机起始位置和速度。

编辑2:我在这里找到了一篇关于模拟粒子碰撞的很好且非常有帮助的论文(此处)。希望它可以对一些对深度感兴趣的人有所帮助。


1
“过早优化是万恶之源” - 高德纳。你应该先编写程序,然后如果它不够快,再进行优化。 - neeKo
5
@NikoDrašković 我并不完全同意。重点在于开始某种做事方式没有意义,如果有更好的完全不同的方式,那么重新开始就没有意义了。正如我所说,我会尽量找到最有效的方法来模拟尽可能多的粒子。 - philkark
1
抱歉,我会稍微更新一下我的帖子。我指的是“至少100-500”。这实际上取决于方法。我只是不想进入某些优化导致代码混乱的情况,而是要从有用的方法开始,然后在可能的情况下对该方法进行优化... - philkark
@flebool 是的,这是关于纯弹性碰撞的。没有阻力、重力或任何其他力参与其中。 - philkark
谢谢你的提示,我会去查看。现在我需要去上课,在大约2.5小时后回来回答更多问题... - philkark
显示剩余6条评论
6个回答

9
如果你想一下,平面上移动的粒子实际上是一个三维系统,其中三个维度是x、y和时间(t)。
假设“时间步长”从t0到t1。对于每个粒子,根据当前粒子位置、速度和方向创建一个3D线段,从P0(x0,y0,t0)到P1(x1,y1,t1)。
将3D空间分区成3D网格,并将每个3D线段连接到它所穿过的单元格。
现在,应该检查每个网格单元。如果它与0或1个线段相连,则不需要进一步检查(将其标记为已检查)。如果它包含2个或更多线段,则需要检查它们之间的碰撞:计算3D碰撞点Pt,缩短两个线段以在此点结束(并删除不再跨越的单元格的链接),根据粒子的新方向/速度创建两个新线段从Pt到新计算的P1点。将这些新的线段添加到网格中并将单元格标记为已检查。将线段添加到网格会将所有交叉的单元格变为未检查状态。
当你的网格中没有未经检查的单元格时,你就解决了时间步长。
编辑:
对于3D粒子,请将上述解决方案调整为4D。
在这种情况下,八叉树是一种不错的3D空间分区网格形式,因为您可以将已检查/未检查状态“冒泡”以快速找到需要注意的单元格。

另一件事是,这在二维中效果很好。在三维中,两个粒子可以碰撞,但它们的轨迹可能穿过不同的单元格。轨迹是指它们中心点路径。 - gg349
@flebool 我可能会这样做:如果有两条以上的线相交于一个单元格,则在此计算迭代中,只有最靠近的两条线会发生碰撞,其他线可能会在下一个时间步骤中发生碰撞。 - philkark
1
@NicolasRepiquet 我并不直接看出你为什么需要八叉树,因为对于3D来说,显然所有的线都将同时存在,因此,3D线足以用于3D粒子模拟。不过这是一个好方法,谢谢! - philkark
@flebool,我其实也想发同样的帖子,因为我也在考虑这个问题... - philkark
@flebool 八叉树就是这样的:一个由高度单元格组成的网格,在需要时可以将每个单元格再次细分为高度单元格(递归地,无限循环)。但我仍然不明白为什么这种解决方案在四维上失败了! - Nicolas Repiquet
显示剩余6条评论

7
一个很好的空间划分的例子是考虑乒乓球游戏,以及检测球和球拍之间的碰撞。假设球拍在屏幕左上角,球在屏幕左下角附近...
--------------------
|▌                 |
|                  |
|                  |
|     ○            |
--------------------

每次球移动时都检查碰撞并非必要。相反,将游戏场地沿中心线分为两半。球是否在场地左侧?(简单的点在矩形内算法)
  Left       Right
         |
---------|----------
|▌       |         |
|        |         |
|        |         |
|       |         |
---------|----------
         |

如果答案是肯定的,那么再次将左侧水平分割,这样我们就有了一个左上和一个左下的分区。
   Left       Right
          |
 ---------|----------
 |▌       |         |
 |        |         |
-----------         |
 |        |         |
 |     ○  |         |
 ---------|----------
          |

这个球是否在屏幕左上角和球拍相同的位置? 如果不是,就不需要检查碰撞了!只有处于同一分区中的物体才需要相互检测碰撞。通过进行一系列简单(且廉价)的点与矩形内部的检查,您可以轻松避免进行更昂贵的形状/几何碰撞检查。

您可以继续将空间分割成越来越小的块,直到一个对象跨越两个分区。这是BSP的基本原理(一种在早期3D游戏(如Quake)中首创的技术)。关于二维和三维空间划分还有很多理论可以在网上找到。

http://en.wikipedia.org/wiki/Space_partitioning

在二维中,您通常会使用BSP或四叉树。在三维中,您通常会使用八叉树。但是基本原理仍然相同。

1

假设在时间t时,对于每个粒子,您都有:

P   position
V   speed

还有一个粒子A(i)和A(j)之间的N*(N-1)/2信息数组,其中i < j;您可以使用对称性来评估上三角矩阵,而不是完整的N*(N-1)网格。

    MAT[i][j] = { dx, dy, dz, sx, sy, sz }. 

这意味着对于粒子j而言,粒子j的距离由三个分量dx、dy和dz组成;以及一个乘以dt的delta-vee,即sx、sy、sz。

为了移动到t+dt时刻,您可以根据它们的速度暂时更新所有粒子的位置。

px[i] += dx[i]  // px,py,pz make up vector P; dx,dy,dz is vector V premultiplied by dt
py[i] += dy[i]  // Which means that we could use "particle 0" as a fixed origin
pz[i] += dz[i]  // except you can't collide with the origin, since it's virtual

然后您检查整个N *(N-1)/ 2数组,并暂时计算每对粒子之间的新相对距离。

dx1 = dx + sx
dy1 = dy + sy
dz1 = dz + sz
DN  = dx1*dx1+dy1*dy1+dz1*dz1  # This is the new distance

如果DN < D^2,其中D是粒子的直径,那么在刚刚过去的dt中发生了碰撞。

然后,您可以精确计算出发生碰撞的位置,即从旧距离平方D2(dx * dx + dy * dy + dz * dz)和新的DN计算出精确的d't:它是

d't = [(SQRT(D2)-D)/(SQRT(D2)-SQRT(DN))]*dt

(在速度覆盖距离SQRT(D2)-SQRT(DN)的时间dt内,减少距离从SQRT(D2)到D所需的时间)。这使得假设粒子j从粒子i的参考系中看到时,没有“超前”。

这是一个更繁重的计算,但只有在发生碰撞时才需要它。

知道d't和d"t = dt-d't后,您可以使用dx*d't/dt等重复对Pi和Pj的位置计算,并获得粒子i和j在碰撞瞬间的确切位置P;然后更新速度,再将其积分剩余的d"t并获得时间dt结束时的位置。

请注意,如果我们停在这里,此方法将在发生三粒子碰撞时出现问题,并且仅处理两粒子碰撞。

因此,我们不运行计算,而是标记(i,j)粒子在d't处发生了碰撞,在运行结束时,保存最小的d't以及发生碰撞的粒子。

例如,假设我们检查粒子25和110,并在0.7 dt处发现碰撞;然后我们在0.3 dt处发现110和139之间的碰撞。在0.3 dt之前没有任何碰撞。

我们进入碰撞更新阶段,“碰撞”110和139并更新它们的位置和速度。然后为每个(i,110)和(i,139)重复2 *(N-2)次计算。

我们将发现可能仍然存在与粒子25的碰撞,但现在是在0.5 dt处,也许,在0.9 dt处,例如,另一个粒子139和80之间的碰撞。 0.5 dt是新的最小值,因此我们在25和110之间重复碰撞计算,并且对于每个碰撞都会遭受轻微的“减速”算法。

因此,实施时现在唯一的风险是“幽灵碰撞”,即粒子在时间t-dt时距目标D>直径,而在时间t时距离D>直径在另一侧

为了避免这种情况的发生,您可以选择一个dt,以便在任何给定的dt中,粒子从未行进超过其直径的一半。实际上,您可以根据最快粒子的速度使用自适应dt。幽灵碰撞仍然可能发生;更进一步的改进是基于任意两个粒子之间的最近距离来减少dt。

这样,当碰撞不太可能发生时,算法的速度会大大加快,但在碰撞附近时,它的速度会明显变慢。如果两个粒子之间的最小距离(我们在循环中几乎不花费成本计算)使得最快的粒子(我们也几乎不花费成本找到)不能在少于五十个dt内覆盖它,那么速度将提高4900%。

无论如何,在没有碰撞的通用情况下,我们现在已经对每个粒子对进行了五次求和(实际上由于数组索引而更像三十四次),三次乘积和多次赋值。如果我们包括(k,k)对以考虑粒子更新本身,我们就可以得出迄今为止的成本的良好近似值。

这种方法的优点是O(N ^ 2)-它随着粒子数量而扩展-而不是O(M ^ 3)-它随着涉及的空间体积而扩展。

我期望在现代处理器上运行的C程序能够实时管理数万个粒子。

附言:这实际上与Nicolas Repiquet的方法非常相似,包括在多个碰撞的4D邻域减速的必要性。


1
你的公式中的D0是什么? 也许你想表达的不是(SQRT(D0)-SQRT(DN)),而是(SQRT(D2)+SQRT(DN))? 另外,在任何情况下,你都是在假设两条轨迹处于同一平面上推导的,对吗?但实际上在三维空间中并不是这样的。 - gg349
请注意,在每个迭代步骤中,您需要一个足够小的积分“dt”以避免错过碰撞,但又足够大以允许它们发生。这很棘手! - gg349
我的D符号搞混了;它们是过去dt和当前时间的距离平方,所以我计算它们之间的差异。我使用距离平方,因为通常我不需要距离,这可以节省很多SQRT。由于两个粒子碰撞,它们的向量可以假定在同一平面内(或多或少;如果粒子是宏观的,那么,呃)。至于棘手的问题,你说得太对了! - LSerni
对于这个问题我会进行更深入的研究并给出更准确的答复。但目前来看,我认为不需要在 "三重" 碰撞后重新计算距离,因为这种情况应该很少发生,我可以简单地忽略这个影响,并且在下一个时间步中像正常碰撞一样计算第二个三重碰撞 。 - philkark
除了这一步之外,我的想法(如我在问题中所提到的)与你的差不多,但是你的答案采用了良好的处理方式以增加准确性。 - philkark

1
你可以定义粒子之间的斥力,与距离平方成反比。在每次迭代中,计算所有粒子对之间的力,将作用于每个粒子的所有力相加,计算粒子加速度,然后是粒子速度,最后是粒子的新位置。这样处理会自然地处理碰撞。但是,处理粒子和墙壁之间的相互作用是另一个问题,必须以其他方式处理。

1
您可以沿着“分而治之”的思路考虑。这个想法是识别不互相影响的正交参数。例如,在2D(3D中为3轴)情况下,可以考虑沿2个轴拆分动量分量并独立计算碰撞/位置。另一种识别这些参数的方法是将垂直移动的粒子进行分组。因此,即使它们发生碰撞,在这些线上的净动量也不会改变。

我同意以上内容并非完全回答了您的问题,但它传达了一个基本思想,您可能在这里找到它有用。


很遗憾,您无法独立检查碰撞。只有使用所有坐标才能定义碰撞。 - gg349

1

在两个粒子(或粒子与墙之间)发生碰撞之前,积分是微不足道的。这里的方法是计算第一次碰撞的时间,然后进行积分,然后计算第二次碰撞的时间,以此类推。让我们将tw[i]定义为第i个粒子撞击第一堵墙所需的时间。虽然很容易计算,但您必须考虑球的直径。

计算两个粒子ij之间碰撞的时间tc[i,j]需要更多时间,并且是从它们之间距离d的时间研究中得出的:

d^2=Δx(t)^2+Δy(t)^2+Δz(t)^2

我们研究是否存在正数t使得d^2=D^2,其中D是粒子的直径(或者如果您希望它们不同,则是两个粒子半径之和)。现在,考虑右侧求和式的第一项,

Δx(t)^2=(x[i](t)-x[j](t))^2=

Δx(t)^2=(x[i](t0)-x[j](t0)+(u[i]-u[j])t)^2=

Δx(t)^2=(x[i](t0)-x[j](t0))^2+2(x[i](t0)-x[j](t0))(u[i]-u[j])t + (u[i]-u[j])^2t^2

新出现的术语定义了两个粒子在x坐标上的运动规律,

x[i](t)=x[i](t0)+u[i]t

x[j](t)=x[j](t0)+u[j]t

其中t0是初始配置的时间。设(u[i],v[i],w[i])为第i个粒子速度的三个分量。对于其他三个坐标也做同样处理并求和,我们得到一个关于t的二次多项式方程:

at^2+2bt+c=0

其中

a=(u[i]-u[j])^2+(v[i]-v[j])^2+(w[i]-w[j])^2

b=(x[i](t0)-x[j](t0))(u[i]-u[j]) + (y[i](t0)-y[j](t0))(v[i]-v[j]) + (z[i](t0)-z[j](t0))(w[i]-w[j])

c=(x[i](t0)-x[j](t0))^2 + (y[i](t0)-y[j](t0))^2 + (z[i](t0)-z[j](t0))^2-D^2

现在,有许多标准来评估实际解的存在性等等...如果您想要优化它,可以稍后评估。无论如何,您会得到tc[i,j],如果它是复数或负数,则将其设置为正无穷大。为了加快速度,请记住tc[i,j]是对称的,并且您还希望出于方便将tc[i,i]设置为正无穷大。

然后,您需要获取数组tw和矩阵tc的最小值tmin,并在时间tmin内进行时间积分。

现在,您需要从twtc的所有元素中减去tmin

如果第 i 个粒子与墙壁发生弹性碰撞,则只需翻转该粒子的速度,并仅为每个其他k重新计算tw [i]tc [i,k]

如果两个粒子之间发生碰撞,则需要为每个其他k重新计算tw [i],tw [j]tc [i,k],tc [j,k]。在三维空间中评估弹性碰撞并不容易,也许您可以使用这个。

http://www.atmos.illinois.edu/courses/atmos100/userdocs/3Dcollisions.html

关于过程如何扩展,您有一个初始开销为O(n^2)。然后两个时间步骤之间的集成是O(n),撞墙或碰撞需要O(n)重新计算。但真正重要的是,碰撞之间的平均时间如何随着n的增长而扩展。在统计物理学中应该有一个答案:-)
如果要根据时间绘制属性,请不要忘记添加进一步的中间时间步骤。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接