粒子系统--喷泉 [OpenGL-Transformfeedback]

写在前面

时间飞逝,转眼大三上学期又这么结束了,距离大四的行将就木又近了一步→_→。这个学期收获颇多,其中最开心的事情就是爱上了计算机图形学,这大概是我目前上过的课最为认真的一门了→_→。同时将来有了更清晰的规划。好了废话不多说,我开这个博客是总结一些图形学方面的东西,接下几篇博客都是关于图形学粒子系统的一些个人总结,如有错误请指出,感激不尽!
本人采用现代OpenGL 3.3+版本的API,可编程管线。老一代的固定管线本人不再学习了,也没有必要了。这里贴出几处非常不错的现代OpenGL教程:
https://learnopengl.com/
中文翻译版https://learnopengl-cn.github.io/
http://ogldev.atspace.co.uk/index.html
中文翻译版http://wiki.jikexueyuan.com/project/modern-opengl-tutorial/tutorial1.html


喷泉粒子系统

  • 粒子系统概述
  • 基于GPU的渲染框架
  • 喷泉的物理模型
  • 喷泉粒子系统构建、
  • 实现效果演示

粒子系统概述

粒子系统的思想就是将物体看成很多个小粒子组成,这些粒子都有自己的属性,如位置、速度、颜色、形状、大小、年龄等。粒子局部来说它是随机的,不可预测的。但是很多的粒子聚集在一起遵循着某种物理规律,整体上形成一定的物理外观。粒子随着时间的变化不对运动,旧粒子生存期不断缩短,生存期到了就消亡,同时也有新粒子的不断产生,这样所有的粒子不断运动更新的过程就形成了一幅动态的画面 。

一般来说,粒子系统的模拟流程如下:
1. 生成新的粒子加入到粒子系统中 。
2. 给每个新的粒子赋予初始属性 。
3. 删除超过生存期或超过界限范围的粒子 。
4. 剩余的粒子按照运动规律或相关算法进行移动更新,并改变其属性 。
5. 绘制显示当前所有粒子 。

粒子系统的思想并不难理解,自然界本质都是有许许多多的小粒子组成的,例如河流,它由水分子组成,水分子之间的物理规律形成了河流的外部表现。

基于GPU的渲染框架

传统的粒子系统的模拟都是基于cpu的,就是说粒子系统的产生、更新、消亡都是通过cpu来进行,而gpu仅仅起着渲染的时候。很明显的瓶颈就出来了,粒子数量非常大的时候cpu就成了实时渲染的累赘。随着粒子数量增加,帧率降低的速度加快,超过一定的数量完全不能达到实时的要求了。而实际上对于粒子系统这样的模拟来说,它是非常适合并行处理的,因为粒子与粒子之间的联系并不强,它们的约束是基于统一的某种物理规律,当然在某些粒子系统一定程度上要考虑粒子之间的约束。

所以粒子系统这样的模拟框架,是非常适合的在gpu中进行计算的。现代OpenGL提供了一些特性我们正好可以拿来用!一是OpenGL的transform feedback,二是计算着色器. 本人采用transform feedback的特性,而计算着色器本人目前也不太了解。关于transform feedback的教程可以在这里阅读,这篇教程描述得非常详细http://wiki.jikexueyuan.com/project/modern-opengl-tutorial/tutorial28.html,通过简单的烟花粒子系统介绍了通过transform feedback构建粒子系统的流程,所以建议对transform feedback还不了解的同学认真地阅读这篇教程并实践,这个框架复用性很高。意思是制作不同的粒子系统,只需要修改一下粒子初始化函数和更新粒子属性的着色器。

在gpu中计算有个问题,粒子系统往往需要随机数生成函数,在glsl生成随机数的其实有,但是这里用更加快捷的方式,直接在cpu中创建一个一维纹理,往里面塞随机数。在shader中就可以通过纹理坐标采样这个纹理来生成随机数,此时纹理坐标就是种子,要注意保持每个粒子采样随机数的种子的唯一性,本人经常采用就是粒子的年龄。随机纹理生成如下:

void Fountain::InitRandomTexture(unsigned int size)
{//size为一维纹理大小
    srand(time(NULL));
    glm::vec3* pRandomData = new glm::vec3[size];
    for (int i = 0; i < size; i++)
    {
        pRandomData[i].x =float(rand()) / float(RAND_MAX);
        pRandomData[i].y =float(rand()) / float(RAND_MAX);
        pRandomData[i].z =float(rand()) / float(RAND_MAX);
    }
    glGenTextures(1, &mRandomTexture);
    glBindTexture(GL_TEXTURE_1D, mRandomTexture);
    glTexImage1D(GL_TEXTURE_1D, 0, GL_RGB, size, 0, GL_RGB, GL_FLOAT, pRandomData);
    glTexParameterf(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameterf(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    delete[] pRandomData;
    pRandomData = nullptr;
}

然后就是粒子的渲染,我这里直接使用了OpenGL的点精灵,说白了就是一个点,但是它始终面向着摄像机,类似于公告牌。通过gl_PointSize内建函数可以在顶点着色器中修改点的大小,gl_PointCoord可以在片元着色器中对纹理进行采样,少了很多麻烦。

喷泉的物理模型

接下就是介绍喷泉粒子的物理模型相关的知识,说实话实时渲染因为它要求的实时性,所以往往不会涉及到非常复杂的数学计算,所以不用太过害怕。

在描述流体运动中, N-S 方程往往被涉及 。 但是在解偏微分方程的时候必须要使用数值方法 ,所以它是非常耗时的 。 然而在一些特殊情况下,精确解是可。 其中一个例子是 Poiseuille 流,这是一个通能的过具有恒定横截面的直圆管的稳定层流 。 对于 Poi-seuille 流,可以得出的解析解如下:

Vy=Vmax[1(r/R)2]

其中R为圆管半径,r是粒子与中心轴之间的距离。 Vy为粒子初始速度向量中的y方向的速度, Vmax是自定义的一个速度最大值。这就是初始时粒子速度y方向的大小。接下来还有x和z方向的速度初始化,这很关键,因为这决定粒子运动轨迹。

想象一个伞状喷泉在你面前,喷泉正在朝着y轴方向喷涌,从xoz平面看去,你会看到一个圆形。也就是说在xoz平面,它360度全方位喷洒,这里暂不考虑风力等因素。所以xz方向上,它的速度方向的分量VxVz是在360度之间随机选择,或者说在-180.0f到180.0f之间随机。那么得出x和y方向的的速度初始分量如下:

Vx=sin(R1×Rand(a/2))×cos(R2×2×Pi)
Vz=sin(R1×Rand(a/2))×sin(R2×2×Pi)

是不是觉得这个公式很眼熟?其实这就是球体参数方程中的 xy部分!R1、R2为0到1之间的随机数,他们是不相同的,而a就是喷泉喷射伞的角度, Rand(a/2)的意思就是产生 -a/2a/2之间的随机数。上面公式的几何意义就是在一个圆锥体生成任意方向的粒子。

所以 sin(R1×Rand(a/2)) 就是的在喷泉的喷射伞角度内任选一个角度。

sin(R2×2×Pi)cos(R2×2×Pi) 就是在在xoz平面上任选一个角度,即圆的方程。

好了,接下来就是粒子运动属性的更新了,假设粒子只受重力加速度的影响,那么它的加速度向量为vec3(0.0,-9.81,0.0)。假设相邻帧之间的时间差为DeltaTimeSecs,注意转换成以秒为单位,那么粒子的速度变化量为

DeltaV=DeltaTimeSecsvec3(0.0,9.81,0.0);

相应的位移变化量为
DeltaP=Velocity0[0]DeltaTimeSecs;

所以当前帧粒子属性更新为:

        Position1 = Position0[0] + DeltaP;
        Velocity1 = Velocity0[0] + DeltaV;

其中Position0[0]为上一帧粒子位置,Velocity1为上一帧粒子速度。相应的要加入风力影响的话,只需要改变加速度向量的xz分量的值即可。

粒子碰到地面之后给予它一个轻微的反弹力碰撞检测,这样看起来更真实。反弹公式如下:

Velocity1=0.1(Velocity0[0]2dot(Velocity0[0],NORMAL)NORMAL)

NORMAL为碰撞表面的法向量, Velocity0[0]为粒子碰到地面时的速度,dot表示和点乘,Velocity1表示碰撞后的粒子速度。乘上0.1是因为如果不乘,那么就是无能量损失的反弹,你会看到粒子反弹得非常高。乘上0.1意义是使得它能量损失非常大,即仅仅是轻微的反弹.

喷泉粒子系统构建

粒子属性:这里用type分量来将粒子分成三类。第一类为发射器,这类粒子不进行运行,仅进行发射,每当发射器年龄到了,就会分裂出一个第二类粒子,注意是分裂,意思是发射器不会消失,分裂完后重置它的年龄!第二类粒子就是在空中喷射的粒子;第三类就是碰到地面,产生一个轻微反弹的粒子。

struct WaterParticle 
{
    float type;//粒子种类,发射器或第二级或第三级粒子
    glm::vec3 position;
    glm::vec3 velocity;
    float lifetimeMills;//年龄
};

粒子初始化:粒子的初始化其实就是产生发射器粒子,在一个原点的圆周内随机生成规定数量的粒子,模拟从圆管中喷涌。要注意给粒子的属性随机化,采用方法就是

value=(MAXVALUEMINVALUE)Rand()+MINVALUE
生成 [MIN_VALUE, MAX_VALUE]的随机数,即控制了值整体的范围,又保证了随机性。

void Fountain::GenInitLocation(WaterParticle particles[],int nums){
    srand(time(NULL));
    for(int x = 0;x < nums;x ++){
        glm::vec3 record(0.0f);
        //radius即为圆管半径
        record.x = (2.0f*float(rand())/float(RAND_MAX)-1.0f)*radius;
        record.z = (2.0f*float(rand())/float(RAND_MAX)-1.0f)*radius;
        //保证产生的粒子在圆周内
        while(sqrt(record.x*record.x+record.z*record.z)>radius){
            record.x = (2.0f*float(rand())/float(RAND_MAX)-1.0f)*radius;
            record.z = (2.0f*float(rand())/float(RAND_MAX)-1.0f)*radius;
        }
        record += center;//平移至喷泉中心
        particles[x].type = PARTICLE_TYPE_LAUNCHER;
        particles[x].position = record;
        particles[x].velocity = glm::vec3(0.0f);
        particles[x].lifetimeMills = (MAX_LAUNCH_F-MIN_LAUNCH_F)*(float(rand())/float(RAND_MAX))+MIN_LAUNCH_F;
    }    
}

粒子属性更新:在更新着色器中实现,这里是几何着色器。

#version 330 core
layout (points) in;
layout (points,max_vertices = 10) out;

in float Type0[];
in vec3 Position0[];
in vec3 Velocity0[];
in float Age0[];

out float Type1;
out vec3 Position1;
out vec3 Velocity1;
out float Age1;

uniform float gDeltaTimeMillis;//每帧时间变化量
uniform sampler1D gRandomTexture;//随机数纹理
uniform float MAX_LAUNCH;//最小发射时间
uniform float MIN_LAUNCH;//最大发射时间
uniform float angle;//喷泉伞的角度
uniform float R;//喷泉圆管的半径
uniform vec3 NORMAL;//地面法向量
uniform vec3 center;//喷泉中心

#define PARTICLE_TYPE_LAUNCHER 0.0f
#define PARTICLE_TYPE_SHELL 1.0f
#define PARTICLE_TYPE_SECONDARY 2.0f

vec3 GetRandomDir(float TexCoord);
vec3 Rand(float TexCoord);//随机数0到1
vec3 rand(float TexCoord);//随机数-1到1

void main()
{
    float Age = Age0[0] - gDeltaTimeMillis;//更新年龄
    if(Type0[0] == PARTICLE_TYPE_LAUNCHER){//第一类粒子
        if(Age <= 0 ){
            //年龄到了,发射第二级粒子
            Type1 = PARTICLE_TYPE_SHELL;
            Position1 = Position0[0];
            //以年龄为随机数的种子
            vec3 randNum = rand((Age0[0]/1000.0f));
            vec3 rand01 = rand(Age0[0]+1);
            //Y为-PI 到 PI之间的随机数
            float Y = rand01.x*3.14159;
            //P为-angle到angle的随机数,R为圆管半径
            float P = R*(angle*0.5);
            //粒子初始化速度,上面说了
            Velocity1 = (
                        vec3(
                        sin(P*randNum.x)*cos(Y),
                        0,
                        sin(P*randNum.z)*sin(Y)
                        ));
            Velocity1 = normalize(Velocity1);
            //y方向速度大一些,xz方向速度比较小
            Velocity1.y *= 12.0f;
            Velocity1.xz *= 5.0f;
            //Poi-seuille 流,center为喷泉中心
            float dist = sqrt(pow(Position1.x-center.x,2)+pow(Position1.z-center.z,2));
            Velocity1.y += 2.0f*(1-pow(dist/R,2));

            Age1 = Age0[0];
            EmitVertex();
            EndPrimitive();
            Age = (MAX_LAUNCH-MIN_LAUNCH)*Rand(Age0[0]+2).z + MIN_LAUNCH;
        }
        //发射器一直存在,从不消亡
        Type1 = PARTICLE_TYPE_LAUNCHER;
        Position1 = Position0[0];
        Velocity1 = Velocity0[0];
        Age1 = Age;
        EmitVertex();
        EndPrimitive();
        return ;
    }
    else{//第二类粒子
        //时间转换成秒
        float DeltaTimeSecs = gDeltaTimeMillis/1000.0f;
        //速度和位置变化量
        vec3 DeltaV = DeltaTimeSecs*vec3(0.0,-9.81,0.0);
        vec3 DeltaP = Velocity0[0]*DeltaTimeSecs;
        //这里简化了,y==0为碰到了地面
        if(Position0[0].y >= 0){//未碰到地面
            Type1 = Type0[0];
            Position1 = Position0[0] + DeltaP;
            Velocity1 = Velocity0[0] + DeltaV;
            Age1 = Age;
            EmitVertex();
            EndPrimitive();
            return ;
        }
        else{//碰到地面
            //反弹后再次碰到地面,消亡
            if(Type0[0] == PARTICLE_TYPE_SECONDARY)return;
            Type1 = PARTICLE_TYPE_SECONDARY;
            //反弹公式更新粒子速度
            Velocity1 = Velocity0[0] - 2*dot(Velocity0[0],NORMAL)*NORMAL;
            Velocity1 *= 0.2;
            Position1 = Position0[0];
            Position1.y = 6;
            Age1 = Age;
            EmitVertex();
            EndPrimitive();
            return ;
        }
    }
}

vec3 GetRandomDir(float TexCoord)
{
    vec3 Dir = texture(gRandomTexture,TexCoord).xyz;
    Dir -= vec3(0.5,0.5,0.5);
    return Dir;
}

vec3 Rand(float TexCoord){//随机0-1
    vec3 ret = texture(gRandomTexture,TexCoord).xyz;
    return ret; 
}

vec3 rand(float TexCoord){//随机-1 - 1
    vec3 ret = texture(gRandomTexture,TexCoord).xyz;
    ret -= vec3(0.5,0.5,0.5);
    return ret*2.0;
}

实现效果演示

在本人Ubuntu kylin 16.04上,借用工具glew、glm、glfw实现效果如下:
喷泉动图

评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值