传统SPH方案的主要问题之一是时间步长限制。在原始的SPH中,我们首先从当前设置计算密度,使用EOS计算压强,应用压力梯度,然后运行时间积分。这个过程意味着只需要一定的压缩量就可以触发内核半径内的压力,从而延迟计算。因此,我们需要使用更小的时间步长(意味着更多的迭代),这在计算上是昂贵的。或者,我们可以使用不那么严格的EOS,然而,这个解决方案可能会引入类似弹簧的振荡。微调参数如声速或粘度可以帮助避免此类问题。然而,这并不是一个基本的解决方案,对用户来说也是不切实际的。Solenthaler 和Pajarola通过在SPH模拟中引入预测-校正器概念来解决这个问题。这种又称为预测校正不可压缩SPH(PCISPH),它是一种误差测量算法,假定测量值和期望密度的差值是误差。本篇文章总结我在《Fluid Engine Development》学到关于PCISPH的知识。

算法原理

1.计算合力,预测速度位置,碰撞检测或碰撞处理

2.计算密度后测量密度误差,利用密度误差更新压强

3.计算新压强梯度力

4.多次重复以上过程,得到使密度误差最小的修正力(压强梯度力),然后使用累积力进行下一步

由于它是通过累积校正力使最终状态处于不可压缩状态,而不是通过多个SPH步骤保证不可压缩,所以它不需要在意时间步长的问题

算法代码实现结构如下,

void CalfFluidEngine::PCISPHSolver3::accumulatePressureForce(double timeStepInSeconds)
{
auto particles = GetSphData();
const size_t numberOfParticles = particles->GetNumberOfParticles();
const double delta = computeDelta(timeStepInSeconds);
const double targetDensity = particles->GetDensity();
const double mass = particles->GetParticleMass(); auto& p = particles->GetPressures();
auto& d = particles->GetDensities();
auto& x = particles->GetPositions();
auto& v = particles->GetVelocities();
auto& f = particles->GetForces(); //Initialize for (unsigned int k = 0; k < _maxNumberOfIterations; ++k)
{
// Predict velocity and position // Resolve collisions // Compute pressure from density error // Compute pressure gradient force // Compute max density error
double maxDensityError = ......;
double densityErrorRatio = maxDensityError / targetDensity; if (std::fabs(densityErrorRatio) < _maxDensityErrorRatio){
break;
}
} //Accumlate pressure force
}

我们可以看到预测-校正本身是一个循环体,循环在密度误差小于指定阈值循环执行次数小于指定迭代次数时终止

而循环体内所做的就是不断进行修正压力,直到密度误差足够小

需要注意的是在执行这套算法前,粒子所受其他力包括粘度,重力已经全部计算完成,已全部累加到forces数组中

void accumulateForces(double timeIntervalInSeconds)
{
ParticleSystemSolver3::accumulateForces(timeIntervalInSeconds);
accumulateViscosityForce();
accumulatePressureForce(timeIntervalInSeconds);
}

这边给出完整的算法实现代码

void CalfFluidEngine::PCISPHSolver3::accumulatePressureForce(double timeStepInSeconds)
{
auto particles = GetSphData();
const size_t numberOfParticles = particles->GetNumberOfParticles();
const double delta = computeDelta(timeStepInSeconds);
const double targetDensity = particles->GetDensity();
const double mass = particles->GetParticleMass(); auto& p = particles->GetPressures();
auto& d = particles->GetDensities();
auto& x = particles->GetPositions();
auto& v = particles->GetVelocities();
auto& f = particles->GetForces(); //Initialize
std::vector<double> predictedDensities(numberOfParticles, 0.0); SphStandardKernel3 kernel(particles->GetKernelRadius()); tbb::parallel_for(
tbb::blocked_range<size_t>(0, numberOfParticles),
[&](const tbb::blocked_range<size_t> & b) {
for (size_t i = b.begin(); i != b.end(); ++i)
{
p[i] = 0.0;
_pressureForces[i] = Vector3D::zero;
_densityErrors[i] = 0.0;
predictedDensities[i] = d[i];
}
}); for (unsigned int k = 0; k < _maxNumberOfIterations; ++k)
{
// Predict velocity and position
tbb::parallel_for(
tbb::blocked_range<size_t>(0, numberOfParticles),
[&](const tbb::blocked_range<size_t> & b) {
for (size_t i = b.begin(); i != b.end(); ++i)
{
_tempVelocities[i] = v[i] +
(f[i] + _pressureForces[i]) / mass * timeStepInSeconds;
_tempPositions[i] = x[i] +
_tempVelocities[i] * timeStepInSeconds;
}
}); // Resolve collisions
ParticleSystemSolver3::resolveCollision(_tempPositions, _tempVelocities); // Compute pressure from density error
tbb::parallel_for(
tbb::blocked_range<size_t>(0, numberOfParticles),
[&](const tbb::blocked_range<size_t> & b) {
for (size_t i = b.begin(); i != b.end(); ++i)
{
double weightSum = 0.0;
const auto& neighbors = particles->GetNeighborLists()[i]; for (size_t j : neighbors) {
double dist = Vector3D::Distance(_tempPositions[j], _tempPositions[i]);
weightSum += kernel(dist);
} weightSum += kernel(0); double density = mass * weightSum;
double densityError = (density - targetDensity);
double pressure = delta * densityError; if (pressure < 0.0) {
pressure *= _negativePressureScale;
densityError *= _negativePressureScale;
} p[i] += pressure;
predictedDensities[i] = density;
_densityErrors[i] = densityError;
}
}); // Compute pressure gradient force
tbb::parallel_for(
tbb::blocked_range<size_t>(0, numberOfParticles),
[&](const tbb::blocked_range<size_t> & b) {
for (size_t i = b.begin(); i != b.end(); ++i)
{
_pressureForces[i] = Vector3D::zero;
}
}); SphSystemSolver3::accumulatePressureForce(x, predictedDensities, p, _pressureForces); // Compute max density error
double maxDensityError = 0.0;
for (size_t i = 0; i < numberOfParticles; ++i) {
maxDensityError = AbsMax(maxDensityError, _densityErrors[i]);
}
double densityErrorRatio = maxDensityError / targetDensity; if (std::fabs(densityErrorRatio) < _maxDensityErrorRatio){
break;
}
} //Accumlate pressure force
tbb::parallel_for(
tbb::blocked_range<size_t>(0, numberOfParticles),
[&](const tbb::blocked_range<size_t> & b) {
for (size_t i = b.begin(); i != b.end(); ++i)
{
f[i] += _pressureForces[i];
}
});
}

乍一看算法实现并不难,大部分原理都已经在之前的笔记提过,唯一需要额外关注的PCISPH利用密度误差更新压强的数学公式

压强更新公式

压强通过 \(p_{i}(t) += \delta \rho_{error}\)更新,这个公式是怎么来的呢,我们来推导一下。

首先回顾一下密度的计算公式 \(\rho = \sum_{j} m_{j}W(x_{ij},h)\) 其中 \(W(x_{ij},h)\)是光滑核函数,\(x_{ij}是x_{i},x_{j}间的距离\),因为粒子质量一致,所以 \(\rho = m\sum_{j} W(x_{ij})\)

设当前时间为t,则 $\rho_{t+\Delta t} = m\sum_{j} W(x_{i}(t+\Delta t) - x_{j}(t+\Delta t)) = m\sum_{j} W(x_{i}(t) +\Delta x_{i}(t) - x_{j}(t) - \Delta x_{j}(t) ) $

设 $r_{ij}(t) = x_{i}(t) - x_{j}(t),\Delta r_{ij} = \Delta x_{i}(t) - \Delta x_{j}(t) $, 则 $\rho_{t+\Delta t} = m\sum_{j} W(r_{ij} (t) + \Delta r_{ij}(t)) $

然后泰勒展开可得 \(\rho_{t+\Delta t} = m\sum_{j} W(r_{ij} (t) )+ \nabla W(r_{ij}(t)) \cdot \Delta r_{ij}(t) = \rho_{i}(t) + \Delta \rho_{i}(t)\)

可求得密度增量为$\Delta \rho_{i}(t) =m \sum_{j}\nabla W(r_{ij}(t)) \cdot \Delta r_{ij}(t)= m \sum_{j} \nabla W(r_{ij}(t)) \cdot (\Delta x_{i}(t) - \Delta x_{j}(t)) $

\(= m (\Delta x_{i}(t)\sum _{j}\nabla W_{ij}\)

$ - \sum_{j}\nabla W_{ij}\Delta x_{j}(t)) $

因为 $ \Delta x_{i} = \Delta t ^{2} \frac{F_{i}^{p}}{m}$

而压力\(f_{p}= m^{2} \sum_{j}(\frac{p_{i}}{\rho _{i} ^{2}} + \frac{p_{j}}{\rho _{j} ^{2}}) \nabla W(|x - x_{j}|)\),可得 \(F_{j =i}^p = m^2 \sum _{j } \frac{p_{i} + p_{i}}{\rho_{0}^{2}}\nabla W_{ij}\)

代入一下可得\(\Delta x_{i} = - \Delta t^{2} m \frac{2p_{i}}{\rho_{0}^2}\sum _{j} \nabla W_{ij}\),\(\Delta x_{j} = - \Delta t^{2} m \frac{2p_{i}}{\rho_{0}^2}\nabla W_{ij}\)

把上式代入密度增量公式,可得 $\Delta \rho_{i}(t) =\Delta t^{2} m^{2} \frac{2p_{i}}{\rho_{0}^2}(-\sum_{j}\nabla W_{ij} \cdot \sum_{j}\nabla W_{ij} - \sum_{j}(\nabla W_{ij} \cdot \nabla W_{ij})) $

把上式转换一下,可得压强计算公式 \(p_{i} = \frac{\Delta \rho_{i}(t) }{(-\sum_{j}\nabla W_{ij} \cdot \sum_{j}\nabla W_{ij} - \sum_{j}(\nabla W_{ij} \cdot \nabla W_{ij})\beta }\)

这串公式可以这么理解,当希望增量密度 \(\Delta \rho _{i}(t)\),需要施加压强pi

其中 \(\beta =\Delta t^{2} m^{2} \frac{2}{\rho_{0}^2}\)

设当前密度为 \(\rho_{i}^{*}\) 目标密度为 \(\rho_{0}\) 则\(\Delta \rho_{i}(t) = \rho_{0} - \rho_{i}^{*} = - \rho_{error}\)

设 \(p_{i} = \delta \rho_{error}\) 可得 \(\delta = \frac{-1}{(-\sum_{j}\nabla W_{ij} \cdot \sum_{j}\nabla W_{ij} - \sum_{j}(\nabla W_{ij} \cdot \nabla W_{ij}))\beta }\)

\(p_{i}(t) += \delta \rho_{error}\)

通过压强更新公式,我们可以不断的在矫正粒子密度使其接近不可压缩条件

代码实现如下

double CalfFluidEngine::PCISPHSolver3::computeDelta(double timeStepInSeconds)
{
auto particles = GetSphData();
const double kernelRadius = particles->GetKernelRadius(); std::vector<Vector3D> points;
BccLatticePointGenerator pointsGenerator;
Vector3D origin = Vector3D::zero;
BoundingBox3D sampleBound(origin, origin);
sampleBound.Expand(1.5 * kernelRadius); pointsGenerator.Generate(sampleBound, particles->GetTargetSpacing(), &points); SphSpikyKernel3 kernel(kernelRadius); double denom = 0;
Vector3D denom1 = Vector3D::zero;
double denom2 = 0; for (size_t i = 0; i < points.size(); ++i) {
const Vector3D& point = points[i];
double distanceSquared = point.SquareMagnitude(); if (distanceSquared < kernelRadius * kernelRadius) {
double distance = std::sqrt(distanceSquared);
Vector3D direction =
(distance > 0.0) ? point / distance : Vector3D::zero; // grad(Wij)
Vector3D gradWij = kernel.Gradient(distance, direction);
denom1 += gradWij;
denom2 += Vector3D::Dot(gradWij,gradWij);
}
} denom += -Vector3D::Dot(denom1,denom1) - denom2; return (std::fabs(denom) > 0.0) ?
-1 / (computeBeta(timeStepInSeconds) * denom) : 0;
} double CalfFluidEngine::PCISPHSolver3::computeBeta(double timeStepInSeconds)
{
auto particles = GetSphData();
double t = particles->GetParticleMass() * timeStepInSeconds
/ particles->GetDensity();
return 2.0 * t * t;
}

这边再一次列出计算密度误差的代码

// Compute pressure from density error
tbb::parallel_for(
tbb::blocked_range<size_t>(0, numberOfParticles),
[&](const tbb::blocked_range<size_t> & b) {
for (size_t i = b.begin(); i != b.end(); ++i)
{
double weightSum = 0.0;
const auto& neighbors = particles->GetNeighborLists()[i]; for (size_t j : neighbors) {
double dist = Vector3D::Distance(_tempPositions[j], _tempPositions[i]);
weightSum += kernel(dist);
} weightSum += kernel(0); double density = mass * weightSum;
double densityError = (density - targetDensity);
double pressure = delta * densityError; if (pressure < 0.0) {
pressure *= _negativePressureScale;
densityError *= _negativePressureScale;
} p[i] += pressure;
predictedDensities[i] = density;
_densityErrors[i] = densityError;
}
});

《Fluid Engine Development》 学习笔记4-预测校正不可压缩SPH-PCISPH的更多相关文章

  1. 《Fluid Engine Development》 学习笔记2-基础

    断断续续花了一个月,终于把这本书的一二两章啃了下来,理解流体模拟的理论似乎不难,无论是<Fluid Simulation for Computer Graphics>还是<计算流体力 ...

  2. 《Fluid Engine Development》 学习笔记1-求解线性方程组

    我个人对基于物理的动画很感兴趣,最近在尝试阅读<Fluid Engine Development>,由于内容涉及太多的数学问题,而单纯学习数学又过于枯燥,难以坚持学习(我中途放弃好多次了) ...

  3. ArcGIS案例学习笔记4_1_矢量校正

    ArcGIS案例学习笔记4_1_矢量校正 概述 计划时间:第四天上午 教程:Editing编辑教程 pdf 目的:矢量数据的空间校正 案例1:仿射变换 数据:Editing编辑数据/spatialAd ...

  4. 《Fluid Engine Development》 学习笔记3-光滑粒子流体动力学

    用粒子表示流体最热门的方法就是就是光滑粒子流体动力学(Smoothed Particle Hydrodynamics (SPH).) 这种方法模糊了流体的边界,用有限数量的粒子代表流体,该方法的基本思 ...

  5. Foundations of Qt Development 学习笔记 Part1 Tips1-50

    1. 信号函数调用的时候仅仅会发送出信号,所以不需要执行 ,所以对于信号声明就行,但是不需要进行定义. 2. 只有槽函数可以声明为public,private,或者是protected的,而信号不行. ...

  6. python学习笔记(23)——python压缩bin包

    说明(2017-12-25 10:43:20): 1. CZ写的压缩bin包代码,记下来以后好抄. # coding:utf-8 ''' Created on 2014年8月14日 @author: ...

  7. linux学习笔记-10.解压与压缩

    1.gzip压缩 gzip a.txt 2.解压 gunzip a.txt.gzgzip -d a.txt.gz 3.bzip2压缩 bzip2 a 4.解压 bunzip2 a.bz2bzip2 - ...

  8. Linux学习笔记—文件与文件系统的压缩与打包(转载)

    压缩文件的用途与技术 例如,计算机都是以byte单位来计量的,1byte占8bit.如果存储数字1,那么1byte就会空出7bit.采用一定的计算方式,压缩这些空间可以大大降低文件存储. Linux系 ...

  9. Linux学习笔记之AIX系统上压缩与解压文件

    0x00 概述 AIX机器真难用,一时半会还真适应不了.   0x01 压缩tar 命令格式: # tar -cvf (或xvf)+文件名+设备 C:是本地到其他设备 x:是其他设备到本地 r:是追加 ...

随机推荐

  1. MyBatis中jdbcType=INTEGER、VARCHAR作用

    Mapper.xml中 pid = #{pid,jdbcType=INTEGER} pid = #{pid} 都可以用 Mybatis中什么时候应该声明jdbcType? 当Mybatis不能自动识别 ...

  2. webpack 性能优化小结

    背景 如今前端工程化的概念早已经深入人心,选择一款合适的编译和资源管理工具已经成为了所有前端工程中的标配,而在诸多的构建工具中,webpack以其丰富的功能和灵活的配置而深受业内吹捧,逐步取代了gru ...

  3. Cogs 2221. [SDOI2016 Round1] 数字配对(二分图)

    [SDOI2016 Round1] 数字配对 ★★★ 输入文件:menci_pair.in 输出文件:menci_pair.out 简单对比 时间限制:1 s 内存限制:128 MB [题目描述] 有 ...

  4. python qq发消息

    # 原理是先将需要发送的文本放到剪贴板中,然后将剪贴板内容发送到qq窗口 # 之后模拟按键发送enter键发送消息 import win32gui import win32con import win ...

  5. 为http请求追加cookie值

    1.html中引入JQuery Cookie插件. 2.JS var expiresTime = new Date(); expiresTime.setTime(expiresTime.getTime ...

  6. HDU 6129 Just do it ——(找规律)

    思路见:http://blog.csdn.net/qq_32506797/article/details/77206167. 利用二进制讲m次转化成log次然后进行转移. 代码如下: #include ...

  7. [TJOI2019]甲苯先生的滚榜——非旋转treap

    题目链接: [TJOI2019]甲苯先生的滚榜 要求维护一个二维权值的集合并支持单点修改,用平衡树维护即可. 因为$n\le 10^6$但$m\le 10^5$,所以最多只有$10^5$个人被操作. ...

  8. Java 面向对象(六)

    抽象类和抽象方法 抽象方法 在方法前面添加了一个关键字 abstract 抽象方法的特点 (1)抽象方法是没有方法体的. (2)抽象方法必须得要定义在抽象类 或 接口当中 (在类前面添加上了一个abs ...

  9. CDH 安装的博客地址记录

    1. 集群环境配置 https://www.cnblogs.com/yinzhengjie/articles/11019333.html 2. 二进制方法安装 https://www.cnblogs. ...

  10. ubuntu系统调整时区和时间

    date: 2019-05-30  10:14:23 author:headsen  chen 个人原创博客,转录需要注明作者和出处. 1,安装ntpdate,同步标准时间 root@hk-confl ...