这篇文章做为代码实现的先导手册,以全局的方式概览一下粒子系统的实现大纲.算法
对粒子进行模拟有两种基本方法:数组
这是两个彻底不一样的坐标系参考下的方法,并且这两种方法各自发展出了本身的方法派系.缓存
在刚体动力学中的应用:数据结构
由于在各自不一样的领域两种坐标系都能以最简方程式表达动力方程,因此两种坐标系下的方法并不打算合并统一.
若读者不太了解的话请思考,xyz坐标系和极坐标系的关系.架构
Particle-based method(拉格朗日方法)对比欧拉方法在粒子模拟领域
优势:并发
缺点:app
这篇文章会介绍如何实现一个粒子模拟系统.ide
咱们把实现过程分为三步:函数
计算积分位移是一个简单的过程, 咱们采集粒子的属性(位置,速度),而后根据每一个粒子的位置和速度计算下一个时间步长中粒子移动到的位置.
咱们使用欧拉方法(固定坐标系)根据粒子受到的力计算其速度 F=mvt .t为时间步长. 碰撞检测随后在这一环节进行计算.性能
若是粒子间没有交互(好比实现烟花,烟雾,火焰等游戏特效),那整个系统就太简单了.
固然咱们讨论的粒子间是有交互的,基本交互就是碰撞(固然还有粘滞,引力吸引等),咱们使用空间分割方法来进行碰撞检测(固然这也是惟一的高效碰撞检测方法,之后咱们单独讨论不一样碰撞算法的区别和实现)
但首先咱们认可一个基本事实:
*. 当距离越远做用力越小,距离越近做用力越大 (你有可能认为这不符合宏观现实,但请你考虑往分子中插入一个质子须要多大力?)
这样咱们就能够只计算给定粒子周围半径内其余粒子给它的协力.空间分割方法能高效的获取粒子的最近临(你能够暂且看成kd-tree)
咱们使用均分网格做为空间分割方法(固然也可使用其余的空间分割方法)
均分网格吧空间分割成一个个 cell. 为了简单起见咱们认为 cell的大小只能容下一个粒子.那么每一个粒子最多被空间中8个 cell所包围.咱们暂且认为粒子间没有穿透.那么每一个 cell最多最多只能接触到4个粒子.
当一个粒子刚好在一个 cell的中心的时候,咱们须要检测空间中 3x3x3-1=26个格子内是否包含其余粒子.因此咱们把粒子中心点位置的 cell的索引号做为粒子的索引号,这样根据每一个粒子的 cell编号就能够轻易的找到附近是否有其余粒子,由于 cell的编号是有排序的.
还有一种实现方式是把粒子半径中碰到的全部 cell编号都记录下来(更多细节被储存),这样碰撞处理所需计算量就减小了,但创建这个索引模型所需的开销就变大了,而根据实践结果而言,增长模型创建的开销比减小碰撞处理的时间来得更大,这是一种得不偿失的作法.
由于每一个时间步长(你能够暂且看成每帧,若是你对时间步长没有概念的话),网格数据都要进行更新,你可能想到在原有的数据结构中去更新,而非从新创建,但一般来讲更新模型并不比从新创建模型须要的时间更少.
在支持原子操做的GPU上(nvdia显卡计算能力超过1.1),咱们能够用相对简单的算法实现这个创建过程.原子操做容许多个threads同时更新 global内存块中的同一个数据,而没必要担忧其发生读写冲突和竞争.
咱们使用两个数组,存放在 global memory中:
updateGrid 这个 kernel函数负责计算并更新每个网格结构. GPU上每一个 thread跟踪计算一个粒子.
计算流程包括:
咱们限制每一个 cell设置最大包含粒子数,这样它就不会致使内存溢出问题,这是为了防范粒子穿透等状况发生时程序崩溃,也为了防止程序性能降低.(当多个粒子处在同一个 cell时,写入相关 cell的操做会进行序列化,这会致使程序性能的下降.),固然也能够分两步走:
区别在于第二步每一个 thread计算对象是 grid,因此不会存在排序写入的问题.
下图显示了一个例子:
另外一种方法使用排序而不是原子操做来创建网格结构模型.
一旦咱们创建起网格空间模型,咱们就可使用它来加速粒子之间的交互计算,咱们使用DEM 方法计算碰撞. 这种碰撞方法由两个粒子间的弹簧阻尼函数计算,弹簧力使两个粒子分开,阻尼力使粒子粘滞.
每一个粒子的计算首先在以前创建的空间模型内寻找附近 27个 cell中是否有其余粒子,若是存在其余粒子就进行碰撞的计算.若是没有其余粒子,就直接由粒子的速度计算下一时间的位置并刷新便可.
咱们能够看到若是咱们不创建空间网格模型,若是咱们有1万个粒子,则须要进行1W x 1W = 1亿次计算,而一旦创建空间模型只需进行 1W x 27 = 27万次计算便可,差了将近 400倍. N^2 和 27*N 的算法复杂度.
在大多数的硬件上, 4.2 排序法有更好的性能.由于排完序可直接进行碰撞检测,在列表中的顺序便是空间上的位置结构.
而使用原子操做的方法对粒子的分布更加敏感,以前咱们已经讨论过排队等待写入的问题.
在实践中可使用 texture memory 对 global array进行映射,来获取粒子的速度和位置数据,由于 texture缓存对二维读取是优化过的, 一个 warp(32条并行 threads)内的 threads刚好要读取 texture缓存中的数据地址很是相近的话, texture缓存会为读取操做提供最佳性能和带宽,这是 texture 缓存硬件设计上的优点所决定的. (使用 texture缓存为读取数据增长了45%的性能)
这更加说明了排序方法的优点,由于它就是按照位置来排序的.
CUDA的散列内存写入技术为建立动态数据结构提供了强力的支持.排序使得内存连续读取特性获得了充分的发挥,这一系列的组合使得GPU能够在一个实时的环境下模拟大型粒子系统. 咱们还可使用如下这些算法来进一步优化程序性能:
scattered-global-write 散列全局内存写入技术是CUDA引入的技术,一般来讲只有获取或者写入连续物理内存地址时,写入和读取操做才能被合并成为一个,由于处理器一个指令只能获取一个内存地址(好比SIMD),若是内存地址连续那么就能够一次性拷贝连续数据至内存(内存对齐后,intel的CPU芯片也支持最多4个 float4类型的 SIMD).但当地址不连续时由于只有一个地址,因此也只能一个个指令顺序进行读取,显然这种方式成为了其余部分的性能瓶颈. 散列全局内存写入就是为了解决那些不连续内存写入的问题,它把多个不连续的内存写入合并为一个命令,而后并发写入,解决了这个瓶颈,固然也多亏了Nvidia显卡的MIMD架构(多指令多数据流).
DEM Harada, T.: Real-Time Rigid Body Simulation on GPUs. GPU Gems 3. Addison Wesley, 2007
Satish, N., Harris, M., Garland, M., Designing Efficient Sorting Algorithms for Manycore GPUs, 2009.
Ian Buck and Tim Purcell, A Toolkit for Computation on GPUs, GPU Gems, Addison-Wesley, 2004