SPH(光滑粒子流体动力学)流体模拟实现五:PCISPH

SPH(光滑粒子流体动力学)流体模拟实现五:PCISPH

 

我们知道真实的液体是不可压缩的,但我们在计算机中离散的计算流体运动,在一定的时间步长内,用标准的SPH方法求解,在在粒子聚集处容易发生挤压,造成压缩。有两种常用的方法模拟不可压缩性:1.在WCSPH(弱可压缩SPH)中,利用刚性状态方程(EOS)建模压力。2.通过求解压力泊松方程实现不可压缩性。但这两种方法都有很昂贵的计算费用。

文章“Predictive-Corrective Incompressible SPH”中,提出了一种预测矫正的方法,来使粒子达到不可压缩性,其性能上相比传统两种方法,更加的高效。

 

PCISPH模型

SPH概述

在拉格朗日粒子描述下, 控制流体运动的偏微分方程 Navier-Stokes 方程可表示为:

SPH方法的核心思想是以离散化粒子的形式来表征连续的场, 并对场量使用积分近似的方式进行计算。位置在xi粒子i的场量:

第i个粒子的密度计算公式为:

压力场直接从Navier-Stokes方程式推导而得:

PCISPH算法

在PCISPH方法中,速度和位置会及时更新,并估计新的粒子密度。然后,对于每个粒子,计算出参考密度的预测变化,并用于更新压力值,压力值又进入压力的重新计算。此过程一直迭代到收敛,即直到所有粒子密度波动均小于用户定义的阈值η(例如1%)。在完成矫正后,我们再更新速度和位置。详细算法流程图如下:

该算法总体思路如下:

1.计算每个粒子的邻居信息,并记录在邻接表内。

2.计算出了压力之外的所有其他力(黏力,重力)。

3.执行矫正循环:执行1).,2).,3).。

   1).预测所有粒子新的的速度和位置。

   2).预测所有粒子新的密度,以及计算新密度和旧密度之间的差值。

   3).预测所有粒子的压力。

4.完成矫正后,利用矫正的压力计算粒子速度和位置。

 

压强导数

我们的算法是根据预测的密度计算新的压力值,因此我们需要找到它们之间变化的关系。我们的目的是找到一个压强p,该压强以这样一种方式改变粒子的位置,即预测的密度与参考密度相对应。 

对于给定的内核平滑长度h,使用SPH密度求和公式计算时间点t + 1处的密度:

其中d_{ij}(t)=x_{i}(t)-x_{j}(t)\triangle d_{ij}(t)=\triangle x_{i}(t)-\triangle x_{j}(t)。假定\triangle d_{ij}(t)非常小,我们可以用一阶泰勒展开近似:

带入\rho _{I}(t+1)得:

我们令,因此密度增量公式为:

我们假设邻居具有相等的压强\widetilde{p_{i}}且密度为静止密度\rho _{0}(根据不可压缩性条件),则结果是:

PCISPH算法在每次迭代校正时只矫正d流体粒子的压力,通过迭代计算出的压力让粒子之间不至于靠的过近(粒子之间距离太近可以理解为流体可压缩)。在只考虑压力F_{i}^{p}的情况下,根据时间积分可以计算出粒子在该压力作用下的位移:

粒子i在获得粒子j的压力同时,也会对领居粒子j施加反作用力,因此:

同样,由于粒子i导致粒子j产生的位移为:

将位移增量带入密度增量公式:

因此\widetilde{p_{i}}为:

其中\beta为:

\widetilde{p_{i}}公式表示了我们迭代过程中,需要不断变化\triangle \rho _{i}(t)的密度值,从而满足粒子不可压缩性。而改变\triangle \rho _{i}(t)的密度需要改变\widetilde{p_{i}}的压强。我们每次预测的粒子密度为:\rho _{i}^{*}。预测密度和静止密度之间的误差为:\rho _{err}^{*}=\rho _{i}^{*}-\rho _{0}。而我们的目标是让粒子密度为\rho _{0},这需要的密度改变量为-\rho _{err}^{*}。因此,我们需要施加的压强值为:

这个压强计算公式在邻居粒子数目不足的时候会导致计算错误,解决办法是进行一次预计算,即在流体粒子周围充满邻居粒子的情况下计算。我们可以直接在初始化流体时计算一次系数\delta即可:

因此,我们的压强改变量\widetilde{p_{i}}为:

由于只要不满足不可压缩条件,我们就重复进行预测校正步骤,因此,我们需要在迭代中不断矫正压强:

 

算法实现

我们之前提到,在计算系数\delta时需要在流体初始化时计算。因此我们使用函数_computeGradWValues()和_computeDensityErrorFactor()计算该系数:

    void FluidSystem::_init(unsigned int maxPointCounts, const fBox3 &wallBox, const fBox3 &initFluidBox, const glm::vec3 &gravity){        
        m_pointBuffer.reset(maxPointCounts);
        m_sphWallBox=wallBox;
        m_gravityDir=gravity;
        
        //根据粒子间距计算粒子质量
        m_pointMass=m_restDensity*pow(m_pointDistance,3.0);
        //设定流体块
        _addFluidVolume(initFluidBox, m_pointDistance/m_unitScale);

        //MarchingCube算法属性
        m_mcMesh=rxMCMesh();
        m_gridContainer.init(wallBox, m_unitScale, m_smoothRadius*2.0, 1.0,m_rexSize,m_gridScale);//设置网格尺寸(2r)
        m_field=new float[(m_rexSize[0]+1)*(m_rexSize[1]+1)*(m_rexSize[2]+1)]();
        posData=std::vector<float>(3*m_pointBuffer.size(),0);
        m_hPosW.resize(3*m_pointBuffer.size(),0.0);
        
        m_gridContainer.insertParticles(&m_pointBuffer);//每帧刷新粒子位置
        m_neighborTable.reset(m_pointBuffer.size());//重置邻接表
        
        //计算W的梯度
        _computeGradWValues();
        //计算因子
        _computeDensityErrorFactor();
    }

其中_computeGradWValues()代码如下:

void FluidSystem::_computeGradWValues(){
        float h2=m_smoothRadius*m_smoothRadius;//h^2
        const int numP=m_pointBuffer.size();
        for (int i=0; i<numP; i++) {
            Point *pi=m_pointBuffer.get(i);
            pi->sum_grad_w=glm::vec3(0.0f);
            pi->sum_grad_w_dot=0.0f;
        }
        for (int i=0; i<numP; i++) {
            Point *pi=m_pointBuffer.get(i);
            pi->kernel_self=_computeNeighbor(i);
            m_neighborTable.point_commit();
            int neighborCounts=m_neighborTable.getNeighborCounts(i);
            //预测密度计算
            for (int j=0; j<neighborCounts; j++) {
                unsigned int neighborIndex;
                float r;
                m_neighborTable.getNeighborInfo(i, j, neighborIndex, r);
                Point* pj=m_pointBuffer.get(neighborIndex);
                //需要用预测的位置去重新计算距离和核值
                glm::vec3 pi_pj=(pi->pos-pj->pos)*m_unitScale;
                float r2=pi_pj.x*pi_pj.x+pi_pj.y*pi_pj.y+pi_pj.z*pi_pj.z;
                if(h2>r2){
                    float h2_r2=h2-r2;
                    r=pow(r2,0.5f);
                    float h=m_smoothRadius;
                    
                    glm::vec3 gradVec=(pi->pos-pj->pos)*m_kernelSpiky/r*(h-r)*(h-r);
                    pi->sum_grad_w+=gradVec;
                    pj->sum_grad_w-=gradVec;
                    
                    pi->sum_grad_w_dot+=glm::dot(gradVec,gradVec);
                    pj->sum_grad_w_dot+=glm::dot(-1.0f*gradVec,-1.0f*gradVec);
                    
                }
            }
        }
    }

我们在其中计算所有粒子的

之后我们在函数_computeDensityErrorFactor()中计算系数\delta,代码如下:

void FluidSystem::_computeDensityErrorFactor(){
        float h2=m_smoothRadius*m_smoothRadius;
        int maxNeighborIndex = 0;
        int maxNeighs = 0;
        const int numParticles = m_pointBuffer.size();
        for (int i=0; i<numParticles; i++) {
            Point *pi=m_pointBuffer.get(i);
            int neighborCounts=m_neighborTable.getNeighborCounts(i);
            int numNeighbors=0;
            //预测密度计算
            for (int j=0; j<neighborCounts; j++) {
                unsigned int neighborIndex;
                float r;
                m_neighborTable.getNeighborInfo(i, j, neighborIndex, r);
                Point* pj=m_pointBuffer.get(neighborIndex);
                //需要用预测的位置去重新计算距离和核值
                glm::vec3 pi_pj=(pi->pos-pj->pos)*m_unitScale;
                float r2=pi_pj.x*pi_pj.x+pi_pj.y*pi_pj.y+pi_pj.z*pi_pj.z;
                if(h2>r2){
                    numNeighbors++;
                }
            }
            //获取邻居最多的粒子,和邻居个数
            if (numNeighbors>maxNeighs) {
                maxNeighs=numNeighbors;
                maxNeighborIndex=i;
            }
        }
        //获取邻居最多的粒子
        Point* pmax=m_pointBuffer.get(maxNeighborIndex);
        
        //计算新的压力根据密度误差
        float restVol=m_pointMass/m_restDensity;
        float preFactor=2.0*restVol*restVol*m_deltaTime*m_deltaTime;
        float gradWTerm=glm::dot(pmax->sum_grad_w*(-1.0f),pmax->sum_grad_w)-pmax->sum_grad_w_dot;
        float divisor=preFactor*gradWTerm;
        m_densityErrorFactor=-1.0/divisor;
        const float factor = m_deltaTime / 0.0001f;
        float densityErrorFactorParameter = 0.05 * factor * factor;
        m_densityErrorFactor*=1.0*densityErrorFactorParameter;
            
    }

值得注意的是,我们还在\delta系数之前乘上densityErrorFactorParameter影响系数,该系数受\Delta t影响。如果不乘上该系数就会出错,论文里并没有明确提出。

计算完成预计算的\delta系数后,我们的tick()函数如下:

    void FluidSystem::tick(bool suf){
        //求解领居结构
        m_gridContainer.insertParticles(&m_pointBuffer);//每帧刷新粒子位置
        m_neighborTable.reset(m_pointBuffer.size());//重置邻接表
        
        //计算外力
        _computerExternalForces();
        //预测矫正
        _predictionCorrection();
        //更新速度和位置
        _updatePosAndVel();
        
        //用于构建表面
        glm::vec3 tem=m_sphWallBox.min-glm::vec3(1.0);
        if(suf){
            CalImplicitFieldDevice(m_rexSize, tem, glm::vec3((1.0/m_gridScale)/m_gridContainer.getDelta()), m_field);
            clearSuf();//清空表面数据
            m_mcMesh.CreateMeshV(m_field, tem, (1.0/m_gridScale)/m_gridContainer.getDelta(), m_rexSize, m_thre, m_vrts, m_nrms, m_face);
        }
    }

我们按照算法流程,首先求解领居结构,这在之前的章节里介绍过,这里就不多提了。

然后计算除了压力的所有外力,该函数_computerExternalForces()代码如下:

    void FluidSystem::_computerExternalForces(){
        float h2=m_smoothRadius*m_smoothRadius;//h^2
        for(int i=0;i<m_pointBuffer.size();i++){
            Point* pi=m_pointBuffer.get(i);
            pi->forces=glm::vec3(0.0);
            
            //邻居粒子装载
            pi->kernel_self=_computeNeighbor(i);
            m_neighborTable.point_commit();
            
            //外力计算
            int neighborCounts=m_neighborTable.getNeighborCounts(i);
            const float restVolume=m_pointMass/m_restDensity;
            for(unsigned int j=0;j<neighborCounts;j++){
                unsigned int neighborIndex;
                float r;
                m_neighborTable.getNeighborInfo(i, j, neighborIndex, r);
                Point* pj=m_pointBuffer.get(neighborIndex);
                float h_r=m_smoothRadius-r;

                //F_Viscosity
                //m_kernelViscosity = 45.0f/(3.141592f * h^6);
                float vterm=m_kernelViscosity*m_viscosity*h_r*restVolume*restVolume;
                pi->forces-=(pi->velocity-pj->velocity)*vterm;
            }
            //F_gravity
            pi->forces+=m_gravityDir*m_pointMass;
            
            //F_boundary
            pi->forces+=_boundaryForce(pi)*m_pointMass;
            
            //初始化矫正因子
            pi->correction_pressure=0.0f;
            pi->correction_pressure_froce=glm::vec3(0.0);
        }
    }

计算完成外力后,进入矫正预测环节,该函数_predictionCorrection()代码为:

void FluidSystem::_predictionCorrection(){
    _density_error_too_large=true;
    int iteration=0;
    while ((iteration<minLoops)||((_density_error_too_large)&&(iteration<maxLoops))) {
        for(int i=0;i<m_pointBuffer.size();i++){
            Point* p=m_pointBuffer.get(i);
            _predictPositionAndVelocity(p);
        }
        
        //循环终止条件(所有粒子的最大预测密度)
        _max_predicted_density=0.0;
        
        for(int i=0;i<m_pointBuffer.size();i++){
            _computePredictedDensityAndPressure(i);
        }
        
        //循环终止条件
        float densityErrorInPercent=max(0.1f*_max_predicted_density-100.0f,0.0f);
        float maxDensityErrorAllowed=1.0f;
        
        //如果密度误差小于终止条件,则终止循环(小于密度误差阈值)
        if(densityErrorInPercent<maxDensityErrorAllowed)
            _density_error_too_large=false;
        
        for (int i=0; i<m_pointBuffer.size(); i++) {
            _computeCorrectivePressureForce(i);
        }
        iteration++;
    }
}

其中我们设置最小循环次数为3,最大循环次数为50。在矫正过程中,我们继续细分为3个步骤:

   1).预测所有粒子的速度和位置,该函数_predictPositionAndVelocity(p)代码为:

void FluidSystem::_predictPositionAndVelocity(Point* p){
    // v' = v + delta_t * a
    // a = F / m
    // v' = v + delta_t * F * V / m
    // v' = v + delta_t * F * 1/density
    
    //计算预测速度和位置
    p->predicted_velocity=p->velocity+(p->forces+p->correction_pressure_froce)*m_deltaTime/m_pointMass;
    p->predicted_position=p->pos+p->predicted_velocity*m_deltaTime;
    
    //碰撞处理
    _collisionHandling(p->predicted_position,p->predicted_velocity);
    
    //初始化预测密度
    p->predicted_density=0.0;
}

    值得注意的是,因为是预测的速度,所以需要好用矫正后的压力去计算。

   2).计算预测的密度和压力,该函数_computePredictedDensityAndPressure(i)的代码为:

void FluidSystem::_computePredictedDensityAndPressure(int i){
    float h2=m_smoothRadius*m_smoothRadius;
    Point* pi=m_pointBuffer.get(i);
    int neighborCounts=m_neighborTable.getNeighborCounts(i);
    pi->predicted_density=pi->kernel_self*m_pointMass;
    //预测密度计算
    for (int j=0; j<neighborCounts; j++) {
        unsigned int neighborIndex;
        float r;
        m_neighborTable.getNeighborInfo(i, j, neighborIndex, r);
        Point* pj=m_pointBuffer.get(neighborIndex);
        //需要用预测的位置去重新计算距离和核值
        glm::vec3 pi_pj=(pi->predicted_position-pj->predicted_position)*m_unitScale;
        float r2=pi_pj.x*pi_pj.x+pi_pj.y*pi_pj.y+pi_pj.z*pi_pj.z;
        if(h2>r2){
            float h2_r2=h2-r2;
            pi->predicted_density+=m_kernelPoly6*pow(h2_r2, 3.0)*m_pointMass;
        }
    }
    auto sss=pi->predicted_density;
    //计算密度误差,仅考虑正向误差
    pi->density_error=max(pi->predicted_density-m_restDensity,0.0f);
    
    //更新压力,只矫正正压力,densityErrorFactor被预先计算并用作常数
    pi->correction_pressure+=max(pi->density_error*m_densityErrorFactor,0.0f);
    
    _max_predicted_density=max(_max_predicted_density,pi->predicted_density);
    
}

    值得注意的是,我们这里都只考虑正向的误差,即压缩的密度误差和压强误差,拉伸的我们不做考虑。

   3).计算矫正后的压力。该函数_computeCorrectivePressureForce(i)代码为:

void FluidSystem::_computeCorrectivePressureForce(int i){
    float h2=m_smoothRadius*m_smoothRadius;
    Point* pi=m_pointBuffer.get(i);
    int neighborCounts=m_neighborTable.getNeighborCounts(i);
    pi->correction_pressure_froce=glm::vec3(0.0f);
    
    float densSq=m_restDensity*m_restDensity;
    float pi_pres=pi->correction_pressure;
    
    //更新压力
    for (int j=0; j<neighborCounts; j++) {
        unsigned int neighborIndex;
        float r;
        m_neighborTable.getNeighborInfo(i, j, neighborIndex, r);
        Point* pj=m_pointBuffer.get(neighborIndex);
        //需要用预测的位置去重新计算距离和核值
        glm::vec3 pi_pj=(pi->pos-pj->pos)*m_unitScale;
        float r2=pi_pj.x*pi_pj.x+pi_pj.y*pi_pj.y+pi_pj.z*pi_pj.z;
        if(h2>r2){
            r=pow(r2,0.5);
            float h=m_smoothRadius;
            float pj_pres=pj->correction_pressure;
            glm::vec3 gradientKer=pi_pj*m_kernelSpiky/r*(h-r)*(h-r);
            float grad=pi_pres/densSq+pj_pres/(m_restDensity*m_restDensity);
            pi->correction_pressure_froce-=m_pointMass*m_pointMass*grad*gradientKer;
        }
    }
}

这里,我们的预测矫正迭代代码就结束了,我们推出迭代后,利用矫正好的压力求解新的速度和位置,该函数_updatePosAndVel()代码如下:

void FluidSystem::_updatePosAndVel(){
    for(int i=0;i<m_pointBuffer.size();i++){
        Point* pi=m_pointBuffer.get(i);
        pi->forces+=pi->correction_pressure_froce;
        const float invMass=1.0/m_pointMass;
        
//        //Symplectic Euler Integration
//        pi->velocity+=pi->forces*invMass*m_deltaTime;
//        pi->pos+=pi->velocity*m_deltaTime/m_unitScale;
        
        //Leapfrog integration
        glm::vec3 vnext = pi->velocity + pi->forces*invMass*m_deltaTime; // v(t+1/2) = v(t-1/2) + a(t) dt
        pi->velocity_eval = (pi->velocity + vnext)*0.5f;  //v(t+1) = [v(t-1/2) + v(t+1/2)] * 0.5
        pi->velocity = vnext;
        pi->pos += vnext*m_deltaTime/m_unitScale;  // p(t+1) = p(t) + v(t+1/2) dt
        
        //弹入位置数据
        posData[3*i]=pi->pos.x;
        posData[3*i+1]=pi->pos.y;
        posData[3*i+2]=pi->pos.z;

    }
}

完成结果图:

对比普通SPH,我们发现在挤压处,PCISPH有明显的矫正。普通SPH效果如下:

  • 15
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 15
    评论
光滑颗粒流体动力学SPH)是一种计算流体力学方法,用于模拟粒子流体中的运动。 SPH源程序是指实现SPH算法的计算机代码。 SPH源程序通常由一系列子程序组成,包括初始化粒子的位置和速度,计算粒子之间的相互作用力和压力,更新粒子的位置和速度等。 在SPH源程序中,粒子被假设为具有质量和体积的不可压缩流体颗粒。每个粒子的状态由其位置、速度和其他物理属性(如密度和压力)来描述。通过在流体中采样一系列离散的粒子,并将它们的运动和相互作用计算在内,SPH源程序能够模拟流体的行为。 SPH源程序使用核函数来近似描述粒子之间的相互作用力。核函数决定了粒子之间相互作用的强度和范围。这种相互作用通过计算每对粒子之间的力来实现。该力可以通过使用基本的物理定律,如牛顿运动定律和连续介质力学,来确定。 SPH源程序中还包含了一些数值计算方法和技巧,以提高模拟的准确性和效率。其中一种常见的技术是使用粒子的密度和压力来计算粒子之间的相互作用力,并使用计算流体力学中的迭代方法来更新粒子的位置和速度。 总而言之,光滑颗粒流体动力学SPH)源程序是一种计算流体力学方法的实现,用于模拟粒子流体中运动的行为。通过近似描述粒子之间的相互作用力,使用核函数和基本的物理定律,SPH源程序能够模拟真实流体动力学行为。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值