SPH(光滑粒子流体动力学)流体模拟实现七:屏幕空间流体渲染(SSF)

之前都是用的Marching Cube重建流体表面的方法。近来为了做对比实验,尝试了屏幕空间流体渲染的方法。发现屏幕空间的方法不仅在效率上有极大的提升,效果也还是非常可观的,先上两张图:

本文方法来自NYIDIA的文章“Screen Space Fluid Rendering for Games”。本文的流体粒子运动模拟方法使用的是前文的PBF方法,我们已经根据PBF计算完成粒子运动。利用粒子完成屏幕空间渲染的主题思路如下:

  1. 利用点精灵的方式所有粒子。
  2. 根据绘制的点精灵记录其深度,其深度计算将点精灵视为球体,保存至深度缓存。
  3. 根据绘制的点精灵记录其厚度,其厚度计算同样将点精灵视为球体,保存至厚度缓存。厚度不能和深度使用MRT同时输出,因为厚度需要累加,因此我们要关闭深度测试,利用alpha加法混合,累加厚度。
  4. 利用双边高斯滤波对深度缓存进行滤波,用普通高斯滤波对厚度缓存滤波。并将其用MRT输出至两张帧缓存中。
  5. 利用过滤完的深度重建法线,并使用一张帧缓存保存法线。
  6. 使用法线以及厚度缓存,进行水的表面渲染。

其流程图如下:

该图为论文中的原图,我个人认为Thickness Image到Surface Shader这部之间应该还需要一步高斯滤波,并且可以放在Depth Smoothing步骤中同时进行。

接下来我们看具体实现步骤。

绘制点精灵

我们在渲染粒子时,使用几何着色器,在眼空间中将粒子渲染成正方形方片。我们之后所有的计算都在眼空间中进行。然后在片段着色器中,裁剪掉超出圆半径的片段,示例如下:

我们的顶点着色器和几何着色器的代码如下:

//顶点着色器----------------------------------------------------
#version 410 core
layout (location = 0) in vec3 position;uniform mat4 view;
uniform mat4 projection;
uniform mat4 model;
uniform float pointRadius;out float PointRadiusView;
out mat4 Projection;void main()
{vec4 viewPos=view*model*vec4(position,1.0f);PointRadiusView=length(view*model*vec4(pointRadius,0,0,0));Projection=projection;gl_Position=viewPos;
}//几何着色器----------------------------------------------------
#version 410 corelayout (points) in;
layout (triangle_strip,max_vertices = 6) out;in float PointRadiusView[];
in mat4 Projection[];
in mat4 View[];out float disCP;
out vec3 viewCenterPos;
out vec3 fragPos;
out mat4 Projectionf;
void main()
{Projectionf=Projection[0];disCP=PointRadiusView[0];viewCenterPos=gl_in[0].gl_Position.xyz;fragPos=vec3((gl_in[0].gl_Position+vec4(-PointRadiusView[0],-PointRadiusView[0],0,0)));gl_Position = Projection[0]*vec4(fragPos,1.0);EmitVertex();fragPos=vec3((gl_in[0].gl_Position+vec4(PointRadiusView[0],-PointRadiusView[0],0,0)));gl_Position = Projection[0]*vec4(fragPos,1.0);EmitVertex();fragPos=vec3((gl_in[0].gl_Position+vec4(-PointRadiusView[0],PointRadiusView[0],0,0)));gl_Position = Projection[0]*vec4(fragPos,1.0);EmitVertex();fragPos=vec3((gl_in[0].gl_Position+vec4(PointRadiusView[0],PointRadiusView[0],0,0)));gl_Position = Projection[0]*vec4(fragPos,1.0);EmitVertex();EndPrimitive();
}

生成深度缓存和厚度缓存

我们之前说过,由于厚度缓存需要关闭深度缓存,并采用加法混合的方式计算,因此厚度和深度缓存需要分布计算输出。在输出深度缓存时,我们需要注意圆片的不同位置深度是不同的,中间厚两边薄,因此中心的深度值较小。我们生成深度缓存片段着色器的代码如下:

#version 410 coreout float FVDepth;in float disCP;
in vec3 viewCenterPos;
in vec3 fragPos;
in mat4 Projectionf;uniform float NEAR;
uniform float FAR;void main(){float discp=distance(viewCenterPos,fragPos);vec4 fColor;if(discp>disCP){discard;}float height=sqrt(disCP*disCP-discp*discp);//深度float depthView=(fragPos.z+height);vec4 clip_space_pos=Projectionf*vec4(vec3(depthView),1.0);gl_FragDepth=(clip_space_pos.z/clip_space_pos.w)*0.5+0.5;FVDepth=-depthView;}

我们裁剪掉超出半径的部分,并利用圆公式,计算深度。我们这里记录眼空间下的深度,因为眼空间z值为负,我们为其取正,方便计算和可视化。我们这里用FVDepth记录线性深度,但仍给gl_FragDepth赋值透视投影变化后的深度,因为这样才能保证深度测试的进行。

厚度缓存需要关闭深度测试,并开启混合,我们的设置代码如下:

glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE);//加法混合
render(shader,wiThick);//将厚度绘制在厚度缓存中
glDisable(GL_BLEND);
glEnable(GL_DEPTH_TEST);

片段着色器代码为:

#version 410 coreout float FragColor;in float disCP;
in vec3 viewCenterPos;
in vec3 fragPos;uniform float NEAR;
uniform float FAR;void main(){float discp=distance(viewCenterPos,fragPos);vec4 fColor;if(discp>disCP){discard;}float height=sqrt(disCP*disCP-discp*discp);FragColor=2.0*height;
}

我们分别可视化深度缓存和厚度缓存,效果如下:

我们实际的深度和厚度是线性的,且大于1(屏幕输出的颜色会被钳制为0~1),我们根据摄像机远平面,对深度和厚度进行缩放后才能可视化。我们这里仅仅为了观察才对其缩放,实际帧缓存中是未缩放的值,帧缓存中保存的是和HDR一致的浮点型帧缓存,用于保存大于1的值。

我们可以发现,其具有强烈的颗粒感,因此我们之后需要对其做平滑处理。我们观察一下未平滑处理生成的最后效果:

滤波处理

使用MRT(多渲染目标)方法可以在一个着色器内平滑并输出深度和厚度缓存。对厚度缓存,我们直接采用高斯模糊的方法。但是深度缓存并不能直接使用高斯模糊,因为我们需要用深度信息重建法线,如果直接使用高斯模糊过滤深度信息会产生如下效果:

浮于上方的粒子会被平滑至同一表面内,因此我们需要值域(深度)信息。我们这里就使用双边过滤(Bilateral Filter)。双边滤波思维导图如下:

双边滤波公式如下:

其中是权重和,为空域高斯权值,为值域高斯权值。其不仅在空域对图像进行滤波,还考虑了值域的影响,我们的深度值正是如此,深度差距较大的值,经过双边滤波,依旧能保存其原本的深度信息。我们的滤波代码如下:

#version 410 corelayout (location = 0) out float Fdepth;
layout (location = 1) out float Fthickness;in vec2 aTexCoords;uniform sampler2D depthMap;
uniform sampler2D thickMap;
uniform vec2 blurDir;
uniform float filterRadius;
uniform float spatialScale;
uniform float rangeScale;const float NEAR=0.1f;
const float FAR=200.0f;void main(){vec2 depTexSize=textureSize(depthMap,0);vec2 thickTexSize=textureSize(thickMap,0);float depth=texture(depthMap,aTexCoords).r;float thickness=texture(thickMap,aTexCoords).r;float sumDep=0.0f,sumThick=0.0f;float wsumDep=0.0f,wsumThick=0.0f;for (float x=-filterRadius; x<=filterRadius; x+=1.0) {float sampleDepth=texture(depthMap,aTexCoords+x*blurDir).r;float sampleThick=texture(thickMap,aTexCoords+x*blurDir).r;//空域float r=x*spatialScale;float w=exp(-r*r);//值域float r2Dep=(sampleDepth-depth)*rangeScale;float gDep=exp(-r2Dep*r2Dep);//深度采用双边滤波,厚度只使用高斯滤波sumDep+=sampleDepth*w*gDep;sumThick+=sampleThick*w;wsumDep+=w*gDep;wsumThick+=w;}sumDep/=wsumDep;sumThick/=wsumThick;Fdepth=sumDep;Fthickness=(sumThick);
}

我们滤波后的效果如下:

为了提升速度,我们这里还采用了1D的多次滤波方法,代码如下:

bool firstBlur=1;
for (int i=0; i<blurNumber;i++) {//绘制模糊glBindFramebuffer(GL_FRAMEBUFFER,blurFrame[0]);glViewport(0, 0, downSample*winWidth, downSample*winHeight);glClearColor(0.0f, 0.0f, 0.0f, 1.0f);glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);wiBlur.use();if(firstBlur){wiBlur.setInt("depthMap", 1);wiBlur.setInt("thickMap", 2);firstBlur=0;}else{wiBlur.setInt("depthMap", 6);wiBlur.setInt("thickMap", 7);}wiBlur.setVec2("blurDir", glm::vec2(1.0/(downSample*winWidth),0.0f));wiBlur.setMat4("invP", glm::inverse(projection));renderWinPlane(wiBlur);wiBlur.close();glBindFramebuffer(GL_FRAMEBUFFER,blurFrame[1]);glViewport(0, 0, downSample*winWidth, downSample*winHeight);glClearColor(0.0f, 0.0f, 0.0f, 1.0f);glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);wiBlur.use();wiBlur.setInt("depthMap", 4);wiBlur.setInt("thickMap", 5);wiBlur.setVec2("blurDir", glm::vec2(0.0f,1.0/(downSample*winHeight)));renderWinPlane(wiBlur);wiBlur.close();
}

重构法线

我们已经得到双边滤波后的深度缓存了,我们接下来重构法线,我们这里的方法也很简单,直接用深度还原片眼空间的位置,然后利用位置求解法线,代码如下:

#version 410 coreout vec4 FragColor;in vec2 aTexCoords;uniform sampler2D depthMap;
uniform float NEAR;
uniform float FAR;
uniform float aspect;//近平面高:宽
uniform float nearHeight;//近平面高const float epo=1e-7;//裁剪空间转换为眼空间
vec3 uvToEye(vec2 texCoord,float depth){
//    vec4 clipSpacePos;
//    clipSpacePos.xy = texCoord.xy * 2.0f - 1.0f;
//    clipSpacePos.z = depth*2.0-1.0;
//    clipSpacePos.w = 1;
//    clipSpacePos = invMVP * clipSpacePos;
//    return clipSpacePos.xyz / clipSpacePos.w;vec2 deltaUV=(2.0*texCoord-vec2(1.0))*vec2(aspect,1.0);//计算近平面的平移向量vec2 deltaView=0.5*nearHeight*deltaUV*depth/NEAR;return vec3(vec2(deltaView),-depth);
}vec3 getEyePos(sampler2D depthTex,vec2 texCoord){float depth=(texture(depthTex,texCoord).r);return uvToEye(texCoord,depth);
}void main(){vec2 texSize=vec2(1.0/textureSize(depthMap,0).s,1.0/textureSize(depthMap,0).t);//深度float depthV=(texture(depthMap,aTexCoords).r);if (depthV>=FAR-1.0) {discard;}//获得当前点的view空间位置vec3 posEye=uvToEye(aTexCoords,depthV);//计算微分vec3 ddu=getEyePos(depthMap,aTexCoords+vec2(texSize.x,0))-posEye;vec3 ddub=posEye-getEyePos(depthMap,aTexCoords-vec2(texSize.x,0));if (abs(ddu.z)>abs(ddub.z)) {ddu=ddub;}vec3 ddv=getEyePos(depthMap,aTexCoords+vec2(0,texSize.y))-posEye;vec3 ddvb=posEye-getEyePos(depthMap,aTexCoords-vec2(0,texSize.y));if (abs(ddv.z)>abs(ddvb.z)) {ddv=ddvb;}//计算法线vec3 N=cross(ddu,ddv);N=normalize(N);//N=vec3(inverse(transpose(inverse(mat3(invMVP))))*N);FragColor=vec4(N,1.0);
}

这里由于我保存的深度是线性眼空间的深度,非透视投影计算后的深度,因此我没用投影矩阵的逆矩阵求解片段眼空间的位置,而是使用三角形的相似去求解眼空间的位置:

根据上图,我们只需要近平面的高度和宽高比就能计算片段在眼空间的位置了。近平面的高度为:

float nearHeight=2*NEAR_PLANE*glm::tan(glm::radians(m_camera.Zoom)/2.0);

之后我们对u和v方向求微分,两个微分向量的叉乘就是我们的法线了,值得注意的是我们使用两个相反的方向进行微分计算,选择绝对值分量更小的。这是因为如果使用单一的微分,会导致法线在物体边缘突变,而产生错误,示意图如下:

我们看到未经处理的左图,粒子和粒子之间有明显的黑色边缘,而处理后的右图则明显改善。

我们可视化法线,效果如下:

我们发现双边滤波对表面有着极大的平滑提升。

着色计算

所有我们需要的信息我们都获取了,我们接下来便可以进行着色计算了。我们这里只考虑反射部分和折射部分。折射部分又考虑厚度对透射率的影响。我们使用近似菲涅尔方程,计算反射分量:

反射部分使用反射向量索引天空盒的方式。

折射部分使用Beer定律计算透射率:

其中d为厚度,k为透射率控制常量。透射部分,我们根据折射采样2D背景图(该图被我们事先渲染至帧缓存内),折射采样公式为:

texture(bg2D,texcoords-N.xy*thickness*k)

其中N为法线,thickness为厚度,k为折射标量(越大折射越明显)。未能透射的部分,我们假定返回默认水体颜色,水体的着色代码如下:

#version 410 coreout vec4 FragColor;in vec2 aTexCoords;uniform float NEAR;
uniform float FAR;
uniform float aspect;//近平面高:宽
uniform float nearHeight;//近平面高
uniform float shininess;//高光参数
uniform sampler2D depthMap;
uniform sampler2D thicknessMap;
uniform sampler2D normalMap;
uniform samplerCube cubeMap;
uniform sampler2D cubeMap2D;
uniform vec3 lightPos;//指向光源(眼空间)
uniform mat4 view;const vec3 diffuseColor=vec3(0.5,0.5,0.5);
const vec3 specularColor=vec3(0.5f);
const vec3 waterRefrectColor=vec3(0.05,0.5,0.8);
const float epo=1e-2;
const vec3 waterF0=vec3(0.15f);
const float refrectScale=2.0;
const float waterK=0.05;//裁剪空间转换为眼空间
vec3 uvToEye(vec2 texCoord,float depth){vec2 deltaUV=(2.0*texCoord-vec2(1.0))*vec2(aspect,1.0);//计算近平面的平移向量vec2 deltaView=0.5*nearHeight*deltaUV*depth/NEAR;return vec3(vec2(deltaView),-depth);
}vec3 culFresnel(vec3 f0,float cosTheta){return f0+(1.0-f0)*pow(1.0-cosTheta,5.0);
}//眼空间光照计算
void main(){vec3 lightDir=normalize((mat3(view)*lightPos));float depth=texture(depthMap,aTexCoords).r;if (depth>=FAR-epo) {discard;}vec2 texSize=vec2(1.0/textureSize(normalMap,0).s,1.0/textureSize(normalMap,0).t);vec3 normal=texture(normalMap,aTexCoords).xyz;float thickness=texture(thicknessMap,aTexCoords).r;//phong漫反射(钳制后)vec3 ambient=diffuseColor*0.1;vec3 diffuse=ambient+diffuseColor*(max(dot(normal,lightDir),0.0));vec3 eyePos=uvToEye(aTexCoords,depth);vec3 veiwDir=normalize(-eyePos);//blinn-phong镜面反射vec3 halfDir=normalize(veiwDir+lightDir);vec3 specular=specularColor*pow(max(dot(normal, halfDir), 0.0), shininess);//fresnel反射分量vec3 Rfresnel=culFresnel(waterF0,max(dot(normal,veiwDir),0.0));vec3 RreflectDir=normalize(mat3(inverse(view))*reflect(-veiwDir,normal));//世界空间vec3 cubeReflect=texture(cubeMap,RreflectDir).rgb;//折射分量(t=p-b(N*P))vec3 Refrect=texture(cubeMap2D,aTexCoords-normal.xy*thickness/FAR*refrectScale).xyz;//透射率float transparency=exp(-thickness*waterK);Refrect=mix(waterRefrectColor,Refrect,transparency);FragColor=vec4(mix(Refrect,cubeReflect,Rfresnel),1.0);}

完成渲染效果如下:

整体效果还是比较好的,不过双边滤波还有有一些比较明显的缺点。比如单体粒子的边缘会过于扁平,导致我们看到上面的视频中飞溅的水滴边界明显。对比我们Marching Cube的重建表面效果如下:

SSF方法:

各向异性后的Marching Cube方法:

我们可以看到屏幕空间已经能带来极大的平滑效果,然而其不足就是单一粒子的表现不足。我接下来可能会去尝试更好的滤波方式,使用自适应卷积核半径,以及边界处理方式来得到更好的屏幕空间平滑效果

SPH(光滑粒子流体动力学)流体模拟实现七:屏幕空间流体渲染(SSF)相关推荐

  1. SPH(光滑粒子流体动力学)流体模拟实现六:Position Based Fluid(PBF)

    SPH(光滑粒子流体动力学)流体模拟实现六:Position Based Fluid(PBF) PBF方法和前篇提到的PCISPH方法类似,都属于迭代矫正法.PCISPH是通过迭代预测压力,通过压力变 ...

  2. SPH(光滑粒子流体动力学)流体模拟实现:算法总览

    流体模拟(一) 流体模拟算法总体流程: 流体现象广泛存在于自然界.日常生活以及工 业生产中,对流体的模拟即流体动画, 一直是基于物理的动画以及计算机图形学的重要研究内容.目前, 基于物理模拟的流体动画 ...

  3. SPH(光滑粒子流体动力学)流体模拟实现三:Marching Cube算法(1)

    流体模拟(三) Marching Cube算法(1) 我们在 实现流体表面重建时,需要事先在空间中划分网格,我们的流体系统正好已经完成了此项工作.其次利用Marching Cube算法计算出构成表面的 ...

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

    流体模拟(二) SPH算法实现2: 在前面一节我们完成了粒子缓存类,网格类和邻接表类.我们现在可以正式的整合在我们的流体系统类中了. 流体系统类 class FluidSystem{public:Fl ...

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

    流体模拟(二) SPH算法实现1: 由于我们计算每个粒子的状态时,都需要获得在它光滑核半径内(邻域内)的所有粒子信息.我们如果遍历每个粒子,计算欧式距离的话,那开销就过于庞大了.因此我们可以将我们的空 ...

  6. SPH(光滑粒子流体动力学)流体模拟实现二:SPH算法(3)-光滑核函数

    流体模拟(二) 光滑核函数: sph中涉及的光滑核可以理解为:在一定的光滑核半径内,所受的力受距离权重的影响,距离越近所受影响越大.其表现形式如图所示. 这里我们便可以将流体看成一个个粒子的集合,每一 ...

  7. SPH(光滑粒子流体动力学)流体模拟实现四:各向异性(Anisotropic)表面光滑(1)

    流体模拟四: 各向异性(Anisotropic)表面光滑(1) 表面的表示与定义 我们是以隐式表面来表示流体表面.定义一个足够大的参考域,它完全包含粒子表示的流体所在的空间D,即.流体表面隐含的定义为 ...

  8. SPH(光滑粒子流体动力学)流体模拟实现四:各向异性(Anisotropic)表面光滑(2)

    流体模拟四: 各向异性(Anisotropic)表面光滑(2) 理论整理清楚了,我们看一下代码实现,我们首先在我们之前的FluidSystem类中添加几个成员函数和对象: class FluidSys ...

  9. SPH(光滑粒子流体动力学)流体模拟实现三:Marching Cube算法(2)

    流体模拟(三) Marching Cube算法(2) 我们在之前的流体系统类里新加入一些函数和成员,用来引用我们的MC类,便可以获得生成有表面的流体模型了,效果如图: 添加简单的天空盒以及Phong氏 ...

最新文章

  1. 盛大 Everbox同步网盘,可以本地和云服务文件同步,还不错,推荐下面的注册地址...
  2. win11 wsl centos7换源aliyun阿里云命令记录
  3. IPv6终于要取代IPv4了!阿里云将全面提供IPv6服务
  4. 华为云媒体査勇:华为云在视频AI转码领域的技术实践
  5. Nagios Plugin for Cacti (npc插件) Download 下载
  6. Spring Security(02)——关于登录
  7. 抓linux肉鸡教程视频,超简单的菜鸟网吧抓肉鸡教程
  8. 使用fiddler代理,手机无法上网
  9. STM32F103 flash地址与数据存入时高低位的关系
  10. mysql表达式转字符串_[转载]MYSQL 字符串操作[]
  11. MySQL语法添加多个外码约束
  12. DDR4原理及硬件设计
  13. C#windows图书信息管理系统
  14. Ubuntu18.04 下虚拟机vm16pro 无法连接WIFI问题解决
  15. 头的各个部位示意图_上臂肌群图示:肱二头肌、肱三头肌、肱肌部位图解说明...
  16. 易基因 | 常用的6种DNA甲基化测序方法,你知道几个?
  17. 图像注意力机制汇总学习
  18. 题解 P5265 【模板】多项式反三角函数
  19. C语言的“短路”现象
  20. 生前一杯水,胜过坟前万吨灰

热门文章

  1. chrome鼠标手势_Chrome插件推荐——第一弹
  2. 用友uclient客户端下载手机_影院6080手机版-影院6080手机客户端下载
  3. 【java基础知识】java.util.LinkedHashMap cannot be cast to com.XXX.XXX
  4. wepy小程序踩坑-未发现相关 sass/less 编译器配置,请检查wepy.config.js文件
  5. npm run test报错
  6. 怎么用计算机连接电视,电视当电脑显示器怎么连接
  7. python打开控制台运行_如何在IPython控制台中默认运行文件而不是终端?
  8. css div下第一个span,CSS之div和span标签
  9. 如何测试软件的性能瓶颈,性能测试如何定位瓶颈
  10. c++ 模糊搜索 正则表达式_c++使用正则表达式提取关键字的方法