传统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. 动态 DP 总结

    目录 例题1:模拟赛题 代码: 例题2 例题3:带修改树上最大独立集. 代码: 注:部分参考 https://www.luogu.org/blog/gkxx-is-here/what-the-hell ...

  2. spring中少用的注解@primary解析

    这次看下spring中少见的注解@primary注解,例子 @Component public class MetalSinger implements Singer{ @Override publi ...

  3. python一些实用的小工具

    1  搭一个简易的本地局域网  python -m http.server 2 获取当前目录下的所有文件名 3 进度条效果 import sys,time for i in range(50): sy ...

  4. Best free and public DNS servers of 2019

    1. OpenDNSPrimary, secondary DNS servers: 208.67.222.222 and 208.67.220.220 2. CloudflarePrimary, se ...

  5. dashucoding记录2019.6.8

    WordPress网站 网址: https://cn.wordpress.org/ 阿里云市场 https://market.aliyun.com/products/53616009?spm=a2c4 ...

  6. gulp4配置多页面项目编译打包

    又开始公司的新项目了... 那当我们拿到公司新项目的时候我们需要做些什么呢? 下面就来分享一下我的工作步骤吧(仅使用于初学者,大神勿见怪- -,有不好的地方希望指出,十分感谢) 1. 整版浏览 这是一 ...

  7. 10月清北学堂培训 Day 2

    今天是杨溢鑫老师的讲授~ T1 物理题,不多说(其实是我物理不好qwq),注意考虑所有的情况,再就是公式要推对! #include<bits/stdc++.h> using namespa ...

  8. Pycharm使用常见问题

    Pycharm下载 下载链接:https://www.jetbrains.com/pycharm/download/ 分为专业版和社区版,社区版也能满足学习需求 Pycharm专业版激活 使用前请将& ...

  9. 正则re.complie作用

    封装一个原本重复使用的正则表达式 prog = re.compile(pattern) result = prog.match(string)

  10. selenium鼠标操作

    #-*- coding:utf-8 -*- import time from selenium import webdriver from selenium.webdriver.common.acti ...