OpenGL-36-01SSAO

个人理解总结

思路解读

首先还是正常渲染场景,按照延迟渲染的方式将所有数据传入G缓冲中,然后在将所有数据传入ssao缓冲中,对齐进行ssao处理,然后在传入到模糊缓冲中,进行模糊处理,最后在通过光线渲染,将其渲染到平面上。显然,我们需要四个着色器程序。

SSAO处理

设置采样核心

简单的不多说了,这里是说明最重要的ssao处理

我们想要对于每个顶点去采样它的法线半球,但是对于每个顶点单独生成法线半球的采样核心是十分困难的。因此我们采用切线空间,在切线空间中,所有顶点的采样核心都是一样的。法向量都指向正z轴

对于一个单位半球,我们设定有64个样本值的采样核心

我们在切线空间中以-1.0到1.0为范围变换x和y方向,并以0.0和1.0为范围变换样本的z方向(如果以-1.0到1.0为范围,取样核心就变成球型了)。由于采样核心将会沿着表面法线对齐,所得的样本矢量将会在半球里。

std::uniform_real_distribution<GLfloat> randomFloats(0.0, 1.0);
//随机数种子,随时间变化
std::default_random_engine generator;
std::vector<glm::vec3> ssaoKernel;
for (unsigned int i = 0; i < 64; ++i)
{//方向随机化glm::vec3 sample(randomFloats(generator) * 2.0 - 1.0, randomFloats(generator) * 2.0 - 1.0, randomFloats(generator));//标准化sample = glm::normalize(sample);//长度随机化sample *= randomFloats(generator);//通过加速插值,让样本靠近原点float scale = float(i) / 64.0f;// scale samples s.t. they're more aligned to center of kernelscale = lerp(0.1f, 1.0f, scale * scale);sample *= scale;ssaoKernel.push_back(sample);
}

随机转动核心

通过引入一些随机性到采样核心上,我们可以大大减少获得不错结果所需的样本数量。我们可以对场景中每一个片段创建一个随机旋转向量,但这会很快将内存耗尽。所以,更好的方法是创建一个小的随机旋转向量纹理平铺在屏幕上。

而这就是所谓的噪声纹理

// generate noise texture
// ----------------------
std::vector<glm::vec3> ssaoNoise;
for (unsigned int i = 0; i < 16; i++)
{glm::vec3 noise(randomFloats(generator) * 2.0 - 1.0, randomFloats(generator) * 2.0 - 1.0, 0.0f); // rotate around z-axis (in tangent space)ssaoNoise.push_back(noise);
}

由于采样核心是沿着正z方向在切线空间内旋转,我们设定z分量为0.0,从而围绕z轴旋转。

我们接下来创建一个包含随机旋转向量的4x4纹理;记得设定它的封装方法为GL_REPEAT,从而保证它合适地平铺在屏幕上。

    unsigned int noiseTexture; glGenTextures(1, &noiseTexture);glBindTexture(GL_TEXTURE_2D, noiseTexture);glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, 4, 4, 0, GL_RGB, GL_FLOAT, &ssaoNoise[0]);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

SSAO着色器

创建对应的帧缓冲对象和其颜色附件。这里仅需要一个即可,因为其他数据都在G缓冲中,通过G缓冲,我们就可以将数据全部传去SSAO着色器中

在渲染完G缓冲后,我们在绑定SSAO缓冲,将G缓冲的纹理附件分别绑定,然后将它们输入到SSAO着色器中,渲染平面

glBindFramebuffer(GL_FRAMEBUFFER, ssaoFBO);
glClear(GL_COLOR_BUFFER_BIT);
shaderSSAO.use();
// Send kernel + rotation
for (unsigned int i = 0; i < 64; ++i)shaderSSAO.setVec3("samples[" + std::to_string(i) + "]", ssaoKernel[i]);
shaderSSAO.setMat4("projection", projection);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, gPosition);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, gNormal);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, noiseTexture);
renderQuad();
glBindFramebuffer(GL_FRAMEBUFFER, 0);

SSAO片段着色器,这里输入了顶点位置,法线以及之前定义的噪声纹理,还有外部设定的采样核心

TBN矩阵可以将切线空间中的顶点变化到观察空间,注意,我们在渲染到G缓冲时,所有的变量(顶点,法线)都已经变化到观察空间,我们基于观察空间生成的TBN矩阵可以将切线空间中的顶点变化到观察空间下

这里我们首先获得TBN矩阵

const vec2 noiseScale = vec2(800.0/4.0, 600.0/4.0);

注意,这里有一个noiseScale变量,原文解释:

我们想要将噪声纹理平铺(Tile)在屏幕上,但是由于TexCoords的取值在0.0和1.0之间,texNoise纹理将不会平铺。所以我们将通过屏幕分辨率除以噪声纹理大小的方式计算TexCoords的缩放大小,并在之后提取相关输入向量的时候使用。

这里我们首先采样噪声纹理,并进行标准化。获得旋转样本

然后在通过使用一个叫做Gramm-Schmidt处理(Gramm-Schmidt Process)的过程,我们创建了一个正交基(Orthogonal Basis),每一次它都会根据randomVec的值稍微倾斜。

最后通过叉乘获得副切线即可获得TBN矩阵

由于我们将texNoise的平铺参数设置为GL_REPEAT,随机的值将会在全屏不断重复。因此每次TBN都是不同的。因为我们使用了一个随机向量来构造切线向量,我们没必要有一个恰好沿着几何体表面的TBN矩阵,也就是不需要逐顶点切线(和双切)向量。

    vec3 fragPos = texture(gPosition, TexCoords).xyz;vec3 normal = normalize(texture(gNormal, TexCoords).rgb);vec3 randomVec = normalize(texture(texNoise, TexCoords * noiseScale).xyz);// create TBN change-of-basis matrix: from tangent-space to view-spacevec3 tangent = normalize(randomVec - normal * dot(randomVec, normal));vec3 bitangent = cross(normal, tangent);mat3 TBN = mat3(tangent, bitangent, normal);

然后就是重头戏

循环遍历采样核心的样本,每次将该样本转变到观察空间,然后将坐标位置叠加次样本(样本本质是方向向量,这里的半径其实就是采样核心的半球半径)即可获得样本的观察空间位置。首先将其设置为vec4,然后通过透视矩阵转化到裁剪空间,然后做透视除法得到标准化设备坐标,然后映射到[0,1]范围,然后通过这个坐标的xy值去从坐标纹理中采样z轴的值,这个值就是深度值。

这并没有完全结束,因为仍然还有一个小问题需要考虑。当检测一个靠近表面边缘的片段时,它将会考虑测试表面之下的表面的深度值;这些值将会(不正确地)影响遮蔽因子。我们可以通过引入一个范围检测从而解决这个问题

这里我们使用了GLSL的smoothstep函数,它非常光滑地在第一和第二个参数范围内插值了第三个参数。如果深度差因此最终取值在radius之间,它们的值将会光滑地根据下面这个曲线插值在0.0和1.0之间

最后就是比较,从样本深度值(sampleDepth)与该坐标的原深度值进行比较(这里做了一点偏移)如果样本深度值,较大,在添加到最终贡献因子。

    float occlusion = 0.0;for(int i = 0; i < kernelSize; ++i){// get sample positionvec3 samplePos = TBN * samples[i]; // from tangent to view-spacesamplePos = fragPos + samplePos * radius;// project sample position (to sample texture) (to get position on screen/texture)vec4 offset = vec4(samplePos, 1.0);offset = projection * offset; // from view to clip-spaceoffset.xyz /= offset.w; // perspective divideoffset.xyz = offset.xyz * 0.5 + 0.5; // transform to range 0.0 - 1.0// get sample depthfloat sampleDepth = texture(gPosition, offset.xy).z; // get depth value of kernel sample// range check & accumulatefloat rangeCheck = smoothstep(0.0, 1.0, radius / abs(fragPos.z - sampleDepth));occlusion += (sampleDepth >= samplePos.z + bias ? 1.0 : 0.0) * rangeCheck;}

最后一步,我们需要将遮蔽贡献根据核心的大小标准化,并输出结果。注意我们用1.0减去了遮蔽因子,以便直接使用遮蔽因子去缩放环境光照分量。

occlusion = 1.0 - (occlusion / kernelSize);
FragColor = occlusion;

这样一来,如果当前顶点周围的样本深度较多大于自身,颜色就会趋近于黑色。这样就能实现,背对着我们的那个面或者边缘线呈现趋近黑色,我们就能看到物体贴近的边缘

这样就完成了SSAO的处理

最后就简单了,在将SSAO帧缓冲的纹理输入模糊处理,(将帧缓冲恢复为默认帧缓冲,只有默认帧缓冲才会绘制在我们的屏幕上)在将模糊帧缓冲纹理输入光线处理,进行漫反射等计算,最后在绘制在屏幕上即可

// ------------------------------------
glBindFramebuffer(GL_FRAMEBUFFER, ssaoBlurFBO);
glClear(GL_COLOR_BUFFER_BIT);
shaderSSAOBlur.use();
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, ssaoColorBuffer);
renderQuad();
glBindFramebuffer(GL_FRAMEBUFFER, 0);glViewport(0,0,SCR_WIDTH*2,SCR_HEIGHT*2);
// 4. lighting pass: traditional deferred Blinn-Phong lighting with added screen-space ambient occlusion
// -----------------------------------------------------------------------------------------------------
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
shaderLightingPass.use();
// send light relevant uniforms
glm::vec3 lightPosView = glm::vec3(camera.GetViewMatrix() * glm::vec4(lightPos, 1.0));
shaderLightingPass.setVec3("light.Position", lightPosView);
shaderLightingPass.setVec3("light.Color", lightColor);
// Update attenuation parameters
const float linear    = 0.09f;
const float quadratic = 0.032f;
shaderLightingPass.setFloat("light.Linear", linear);
shaderLightingPass.setFloat("light.Quadratic", quadratic);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, gPosition);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, gNormal);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, gAlbedo);
glActiveTexture(GL_TEXTURE3); // add extra SSAO texture to lighting pass
glBindTexture(GL_TEXTURE_2D, ssaoColorBufferBlur);
renderQuad();

额外补充

片段着色器是逐像素的,因此在第一次渲染到G缓冲时,我们是使用正常的顶点渲染,此时需要深度缓冲,因为有像素是要被舍弃的,第二次,我们在将G缓冲输入到SSAO缓冲时,我们已经不在需要深度缓冲了,因此此时我们已经获得了所有的有效像素和这些像素上分别对应的观察空间位置,法线,颜色。而我们只所以还要输入透视矩阵,是因为,我们需要获取样本点的深度,而这些可能都是之前已经被舍弃的。第三次模糊就是对像素的模糊没什么可说的。第四次,光线渲染,虽然也是渲染在平面上,但是对于每个像素我们只要知道这个像素对应的观察空间位置,法线,颜色,观察空间下的光源位置以及其他必须变量,我们就能算出这个像素的颜色,直接输出即可。而不需要再次正常绘制顶点,因为我们已经渲染过一次了,通过顶点,我们已经舍弃了所有不需要的像素。

OpenGL-36-01SSAO相关推荐

  1. 跨代的对决 英特尔i7-6700HQ对比i7-4720HQ性能测试

    http://itianti.sinaapp.com/index.php/cpu 跨代的对决 英特尔i7-6700HQ对比i7-4720HQ性能测试 2015-10-13 19:46:31 来源:电脑 ...

  2. OpenGL基础36:天空盒

    前面的例子中,根本没有场景的感觉,除了地板和几块箱子,周围一片黑暗-- 一.立方体贴图 立方体贴图就是6个2D贴图,每个贴图都是立方体的一个面,当然这样的立方体贴图是一个整体,有自己特有的属性,可以使 ...

  3. Learn OpenGL (十二):投光物

    平行光 当一个光源处于很远的地方时,来自光源的每条光线就会近似于互相平行.不论物体和/或者观察者的位置,看起来好像所有的光都来自于同一个方向.当我们使用一个假设光源处于无限远处的模型时,它就被称为定向 ...

  4. Learn OpenGL (十一):光照贴图

    在着色器中使用漫反射贴图的方法和纹理教程中是完全一样的.但这次我们会将纹理储存为Material结构体中的一个sampler2D.我们将之前定义的vec3漫反射颜色向量替换为漫反射贴图. 注意samp ...

  5. Learn OpenGL (九):基础光照

    环境光照(Ambient Lighting):即使在黑暗的情况下,世界上通常也仍然有一些光亮(月亮.远处的光),所以物体几乎永远不会是完全黑暗的.为了模拟这个,我们会使用一个环境光照常量,它永远会给物 ...

  6. Learn OpenGL (八):颜色

    当我们在OpenGL中创建一个光源时,我们希望给光源一个颜色.在上一段中我们有一个白色的太阳,所以我们也将光源设置为白色.当我们把光源的颜色与物体的颜色值相乘,所得到的就是这个物体所反射的颜色(也就是 ...

  7. Learn OpenGL (七):摄像机

    1. 摄像机位置 获取摄像机位置很简单.摄像机位置简单来说就是世界空间中一个指向摄像机位置的向量.我们把摄像机位置设置为上一节中的那个相同的位置: glm::vec3 cameraPos = glm: ...

  8. Learn OpenGL (六):坐标系统

    为了将坐标从一个坐标系变换到另一个坐标系,我们需要用到几个变换矩阵,最重要的几个分别是模型(Model).观察(View).投影(Projection)三个矩阵.我们的顶点坐标起始于局部空间(Loca ...

  9. 教你实现GPUImage【OpenGL渲染原理】

    原文出处: 袁峥Seemygo(@袁峥Seemygo)    一.前言 本篇主要讲解GPUImage底层是如何渲染的,GPUImage底层使用的是OPENGL,操控GPU来实现屏幕展示 由于网上Ope ...

  10. NeHe OpenGL第三十五课:播放AVI

    NeHe OpenGL第三十五课:播放AVI 在OpenGL中播放AVI: 在OpenGL中如何播放AVI呢?利用Windows的API把每一帧作为纹理绑定到OpenGL中,虽然很慢,但它的效果不错. ...

最新文章

  1. 功能测试常用6种方法_建筑管道常用的8种连接方法
  2. python3菜鸟教程中文-Python3 数据结构
  3. CTFshow php特性 web135
  4. C语言合并排序实例代码
  5. Java的calendar类用法
  6. spring项目启动执行特定方法
  7. java maven module_java – Maven JDK9模块:无法解析module-info
  8. RestTemplate与Feign使用对比
  9. 给CentOS添加第三方源(RPMForge源)
  10. 思科全球云指数:2010-2015预测报告
  11. Spark集群新增节点方法
  12. [转载]AWS使用小记之EC2(Elastic Compute Cloud)
  13. linux的vmstat命令,Linux中vmstat命令起什么作用呢?
  14. 极化码 串并行译码的辨别(SC BP SCAN)硬判决和软输出
  15. c语言快速学习,怎么才能正确快速的学习c语言
  16. 罗马数字包含以下七种字符: `I, V, X, L,C,D M`
  17. Java开发之消息队列
  18. 一种基于HBase韵海量图片存储技术
  19. win7 虚拟wifi服务器,win7虚拟wifi设置
  20. 华东师范大学夏令营复习计划总结

热门文章

  1. Altium designer中蛇形线走法和操作说明
  2. w ndows无法连接到System,Windows无法连接到System Event Notification Service服务解决方法...
  3. 【计算机视觉】全景相机标定(MATLAB/opencv)
  4. 2021年全球零售电子商务软件收入大约6686.2百万美元,预计2028年达到12000百万美元,2022至2028期间,年复合增长率CAGR为9.4%
  5. PS--怎么取消之前选择的工具?
  6. sklearn中predict()与predict_proba()返回值意义
  7. vue3—reactive如何更改属性
  8. 寂寞与孤独是人生中两大财富
  9. JDK1.7扩容时为什么会产生并发死链问题
  10. IT技术外包公司值得去吗?