SHP(光滑粒子流体动力学)流体模拟实现二:SPH算法(4)-算法实现2

流体模拟(二)

SPH算法实现2:

在前面一节我们完成了粒子缓存类,网格类和邻接表类。我们现在可以正式的整合在我们的流体系统类中了。

流体系统类

    class FluidSystem{
    public:
        FluidSystem();
        void init(unsigned short maxPointCounts,
                  const glm::vec3& wallBox_min,const glm::vec3& wallBox_max,
                  const glm::vec3& initFluidBox_min,const glm::vec3& initFluidBox_max,
                  const glm::vec3& gravity){
            _init(maxPointCounts, fBox3(wallBox_min,wallBox_max),
                  fBox3(initFluidBox_min,initFluidBox_max), gravity);
        }
        //获取点的尺寸(字节)
        unsigned int getPointStride()const {return sizeof(Point);}
        //获取点的数量
        unsigned int getPointCounts()const{return m_pointBuffer.size();}
        //获取流体点缓存
        const Point* getPointBuf()const{return m_pointBuffer.get(0);}
        //逻辑桢
        void tick();
        
        ~FluidSystem();
    private:
        //初始化系统
        void _init(unsigned short maxPointCounts,const fBox3& wallBox,
                   const fBox3& initFluidBox, const glm::vec3& gravity);
        //计算密度,压力以及相邻关系
        void _computerPressure();
        //计算加速度
        void _computerForce();
        //移动粒子
        void _advance();
        //创建初始液体块
        void _addFluidVolume(const fBox3& fluidBox,float spacing);
        
        //数据成员
        PointBuffer m_pointBuffer;
        GridContainer m_gridContainer;
        NeighborTable m_neighborTable;

        //点位置缓存数据(x,y,z)
        std::vector<float>posData;
        
        //SPH光滑核
        float m_kernelPoly6;
        float m_kernelSpiky;
        float m_kernelViscosity;
        
        //其他参数
        float m_pointDistance;//半径
        float m_unitScale;//尺寸单位
        float m_viscosity;//粘性
        float m_restDensity;//静态密度
        float m_pointMass;//质量
        float m_smoothRadius;//光滑核半径
        float m_gasConstantK;//气体常量k
        float m_boundaryStiffness;//边界刚性
        float m_boundaryDampening;//边界阻尼
        float m_speedLimiting;//速度限制
        glm::vec3 m_gravityDir;//重力矢量
        
        int m_rexSize[3];//网格尺寸
        
        fBox3 m_sphWallBox;
    };

目前流体系统还是比较简单的,设定我们SPH需要的各种属性,并建立我们上节创建的三种类成员。算法主要在初始化函数_init,密度和压力计算函数_computerPressure,加速度计算函数_computerForce和移动粒子函数_advance。我们的动画在tick帧函数中进行。最后我们计算后的粒子位置在每帧计算后更新posData缓存数据。

然后是我们的实现代码:

 FluidSystem::FluidSystem(){
        m_unitScale            = 0.004f;            // 尺寸单位
        m_viscosity            = 1.0f;                // 粘度
        m_restDensity        = 1000.f;            // 密度
        m_pointMass            = 0.0006f;            // 粒子质量
        m_gasConstantK        = 1.0f;                // 理想气体方程常量
        m_smoothRadius        = 0.01f;            // 光滑核半径
        m_pointDistance       =0.0;
 
        m_rexSize[0]=0;
        m_rexSize[1]=0;
        m_rexSize[2]=0;
        
        
        m_boundaryStiffness = 10000.f;
        m_boundaryDampening = 256;
        m_speedLimiting        =    200;

        //Poly6 Kernel
        m_kernelPoly6 = 315.0f/(64.0f * 3.141592f * pow(m_smoothRadius, 9));
        //Spiky Kernel
        m_kernelSpiky = -45.0f/(3.141592f * pow(m_smoothRadius, 6));
        //Viscosity Kernel
        m_kernelViscosity = 45.0f/(3.141592f * pow(m_smoothRadius, 6));
    }

    FluidSystem::~FluidSystem()
    {
    }
    //构造流体中的点
    void FluidSystem::_addFluidVolume(const fBox3 &fluidBox, float spacing){
        for(float z=fluidBox.max.z; z>=fluidBox.min.z; z-=spacing)
        {
            for(float y=fluidBox.min.y; y<=fluidBox.max.y; y+=spacing)
            {
                for(float x=fluidBox.min.x; x<=fluidBox.max.x; x+=spacing)
                {
                    Point* p = m_pointBuffer.addPointReuse();
                    p->pos=glm::vec3(x,y,z);
                }
            }
        }
    }

    void FluidSystem::_init(unsigned short maxPointCounts, const fBox3 &wallBox, const fBox3 &initFluidBox, const glm::vec3 &gravity){
        m_pointBuffer.reset(maxPointCounts);
        m_sphWallBox=wallBox;
        m_gravityDir=gravity;
        m_pointDistance=pow(m_pointMass/m_restDensity, 1.0/3.0);//计算粒子间距
        _addFluidVolume(initFluidBox, m_pointDistance/m_unitScale);
        m_gridContainer.init(wallBox, m_unitScale, m_smoothRadius*2.0, 1.0,m_rexSize);//设置网格尺寸(2r)
        posData=std::vector<float>(3*m_pointBuffer.size(),0);
  
    }

在构造函数里,我们给属性赋初值,并根据公式3.6:

公式3.12:

公式3.17:

计算该三者的光滑核系数。_addFluidVolume函数,我们用box3创建流体粒子块,并计算粒子位置然,后都加入粒子缓存。在_init函数里,我们初始化流体演示的容器,重力。然后根据粒子质量和密度计算出半径(即粒子间距),然后加入流体块,初始化空间网格。最后初始化posData的粒子位置缓存。

然后我们看计算压力,密度以及相邻关系代码:

void FluidSystem::_computerPressure(){
        float h2=m_smoothRadius*m_smoothRadius;//h^2
        m_neighborTable.reset(m_pointBuffer.size());//重置邻接表
        for(unsigned int i=0;i<m_pointBuffer.size();i++){
            Point* pi=m_pointBuffer.get(i);
            float sum=0.0;
            m_neighborTable.point_prepare(i);
            int gridCell[8];
            m_gridContainer.findCells(pi->pos, m_smoothRadius/m_unitScale, gridCell);
            for(int cell=0;cell<8;cell++){
                if (gridCell[cell]==-1) continue;
                int pndx=m_gridContainer.getGridData(gridCell[cell]);
                while (pndx!=-1) {
                    Point* pj=m_pointBuffer.get(pndx);
                    if(pj==pi)sum+=pow(h2,3.0);
                    else{
                        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;
                            sum+=pow(h2_r2, 3.0);
                            if(!m_neighborTable.point_add_neighbor(pndx, glm::sqrt(r2)))
                                goto NEIGHBOR_FULL;
                        }
                    }
                    pndx=pj->next;
                }
            }
        NEIGHBOR_FULL:
            m_neighborTable.point_commit();
            pi->density=m_kernelPoly6*m_pointMass*sum;
            pi->pressure=(pi->density-m_restDensity)*m_gasConstantK;
        }
    }

在计算邻接关系之前我们先重置邻接表。然后遍历粒子缓存中所有的粒子。每次遍历时首先获取粒子pi,并做初始化处理,之后我们设置一个数组gridCell去获取当前粒子的周围网格,用于我们处理邻域粒子。在每个网格内,我们通过next遍历该网格内的粒子直到next为-1。我们获取距离在光滑核半径内的领域粒子,通过公式3.7:

计算最终pi粒子的密度。然后通过公式3.10:

计算最终pi粒子受到的压力。最后将邻接粒子信息存入邻接表。

我们再来看计算加速度的代码实现:

    void FluidSystem::_computerForce(){
        float h2=m_smoothRadius*m_smoothRadius;
        for(unsigned int i=0;i<m_pointBuffer.size();i++){
            Point* pi=m_pointBuffer.get(i);
            glm::vec3 accel_sum=glm::vec3(0.0);
            int neighborCounts=m_neighborTable.getNeighborCounts(i);
            for(unsigned int j=0;j<neighborCounts;j++){
                unsigned short neighborIndex;
                float r;
                m_neighborTable.getNeighborInfo(i, j, neighborIndex, r);
                Point* pj=m_pointBuffer.get(neighborIndex);
                glm::vec3 ri_rj=(pi->pos-pj->pos)*m_unitScale;
                float h_r=m_smoothRadius-r;
                float pterm=-m_pointMass*m_kernelSpiky*h_r*h_r*
                (pi->pressure+pj->pressure)/(2.f * pi->density * pj->density);
                accel_sum+=ri_rj*pterm/r;

                float vterm=m_kernelViscosity*m_viscosity*h_r*
                m_pointMass/(pi->density * pj->density);

                accel_sum+=(pj->velocity_eval - pi->velocity_eval)*vterm;
            }
            pi->accel=accel_sum;
        }
    }

    

在计算加速度时,我们同样遍历粒子缓存中的每个粒子,并通过该粒子索引访问邻接表内的邻域粒子,通过对每个邻域粒子计算公式3.19:

这里的重力g我们在移动粒子中加入。这样遍历完所有的邻域粒子,我们的最终加速度也就计算完成了。

接下来便可以看粒子移动的代码:

void FluidSystem::_advance(){
        float deltaTime=0.003;
        float SL2=m_speedLimiting*m_speedLimiting;
        //posData.clear();
        for (unsigned int i=0; i<m_pointBuffer.size(); i++) {
            Point* p=m_pointBuffer.get(i);
            glm::vec3 accel=p->accel;//获取p的加速度
            float accel2=accel.x*accel.x+accel.y*accel.y+accel.z*accel.z;

            if(accel2>SL2)//速度限制
                accel*=m_speedLimiting/glm::sqrt(accel2);

            float diff;
            //边界情况
            // Z方向边界
            diff = 1 * m_unitScale - (p->pos.z - m_sphWallBox.min.z)*m_unitScale;
            if (diff > 0.0f )
            {
                glm::vec3 norm(0, 0, 1.0);
                float adj = m_boundaryStiffness * diff - m_boundaryDampening * glm::dot ( norm,p->velocity_eval );
                accel.x += adj * norm.x;
                accel.y += adj * norm.y;
                accel.z += adj * norm.z;
            }

            diff = 1 * m_unitScale - (m_sphWallBox.max.z - p->pos.z)*m_unitScale;
            if (diff > 0.0f)
            {
                glm::vec3 norm( 0, 0, -1.0);
                float adj = m_boundaryStiffness * diff - m_boundaryDampening *glm::dot ( norm,p->velocity_eval );
                accel.x += adj * norm.x;
                accel.y += adj * norm.y;
                accel.z += adj * norm.z;
            }

            //X方向边界
            diff = 1 * m_unitScale - (p->pos.x - m_sphWallBox.min.x)*m_unitScale;
            if (diff > 0.0f )
            {
                glm::vec3 norm(1.0, 0, 0);
                float adj = m_boundaryStiffness * diff - m_boundaryDampening * glm::dot ( norm,p->velocity_eval ) ;
                accel.x += adj * norm.x;
                accel.y += adj * norm.y;
                accel.z += adj * norm.z;
            }

            diff = 1 * m_unitScale - (m_sphWallBox.max.x - p->pos.x)*m_unitScale;
            if (diff > 0.0f)
            {
                glm::vec3 norm(-1.0, 0, 0);
                float adj = m_boundaryStiffness * diff - m_boundaryDampening * glm::dot ( norm,p->velocity_eval );
                accel.x += adj * norm.x;
                accel.y += adj * norm.y;
                accel.z += adj * norm.z;
            }

            //Y方向边界
            diff = 1 * m_unitScale - ( p->pos.y - m_sphWallBox.min.y )*m_unitScale;
            if (diff > 0.0f)
            {
                glm::vec3 norm(0, 1.0, 0);
                float adj = m_boundaryStiffness * diff - m_boundaryDampening * glm::dot ( norm,p->velocity_eval );
                accel.x += adj * norm.x;
                accel.y += adj * norm.y;
                accel.z += adj * norm.z;
            }
            diff = 1 * m_unitScale - ( m_sphWallBox.max.y - p->pos.y )*m_unitScale;
            if (diff > 0.0f)
            {
                glm::vec3 norm(0, -1.0, 0);
                float adj = m_boundaryStiffness * diff - m_boundaryDampening * glm::dot ( norm,p->velocity_eval );
                accel.x += adj * norm.x;
                accel.y += adj * norm.y;
                accel.z += adj * norm.z;
            }

            // 重力作用
            accel += m_gravityDir;

            // 位置计算----------------------------
            glm::vec3 vnext = p->velocity + accel*deltaTime;            // v(t+1/2) = v(t-1/2) + a(t) dt
            p->velocity_eval = (p->velocity + vnext)*0.5f;                // v(t+1) = [v(t-1/2) + v(t+1/2)] * 0.5        used to compute forces later
            p->velocity = vnext;
            p->pos += vnext*deltaTime/m_unitScale;        // p(t+1) = p(t) + v(t+1/2) dt

            //弹入位置数据
            posData[3*i]=p->pos.x;
            posData[3*i+1]=p->pos.y;
            posData[3*i+2]=p->pos.z;
        }
    }

我们每隔0.03秒计算位置,防止速度太快我们还设置了速度限制。然后同样遍历粒子缓存中的每个粒子,在获取每个粒子时对我们的流体容器做简单的碰撞计算,然后考虑重力作用。最后根据加速度和速度计算最后的粒子位置,最后将该点的位置储存在位置缓存中。

然后就是我们的帧动画函数:

    void FluidSystem::tick(){
        m_gridContainer.insertParticles(&m_pointBuffer);//每帧刷新粒子位置
        _computerPressure();
        _computerForce();
        _advance();
    }

每次线刷新粒子位置,然后对其计算压力等属性,然后计算加速度,最后计算最终位置,然后重复该循环便可以获得我们的流体动画了。

我们通过opengl来绘制该效果,每次只要VBO里保存我们更新的粒子位置,便能直接绘出效果,效果图如下:

到这里我们的SPH的实现也就结束了,可是光看到这些粒子,怎么把它变成流体呢,我们下一节会探讨一下如何给其加表面。

发布了22 篇原创文章 · 获赞 22 · 访问量 2589

猜你喜欢

转载自blog.csdn.net/qq_39300235/article/details/102672535
今日推荐