笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,国家专利发明人;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术详解》电子工业出版社等。

CSDN视频网址:http://edu.csdn.net/lecturer/144

接着上篇博客延迟着色出现的问题继续讲解,为了克服这些缺点(特别是混合),我们通常分割我们的渲染器为两个部分:一个是延迟渲染的部分,另一个是专门为了混合或者其他不适合延迟渲染管线的着色器效果而设计的的正向渲染的部分。为了展示这是如何工作的,我们将会使用正向渲染器渲染光源为一个小立方体,因为光照立方体会需要一个特殊的着色器(会输出一个光照颜色)。

现在我们想要渲染每一个光源为一个3D立方体,并放置在光源的位置上随着延迟渲染器一起发出光源的颜色。很明显,我们需要做的第一件事就是在延迟渲染方形之上正向渲染所有的光源,它会在延迟渲染管线的最后进行。所以我们只需要像正常情况下渲染立方体,只是会在我们完成延迟渲染操作之后进行。代码会像这样:

// 延迟渲染光照渲染阶段
[...]
RenderQuad();// 现在像正常情况一样正向渲染所有光立方体
shaderLightBox.Use();
glUniformMatrix4fv(locProjection, 1, GL_FALSE, glm::value_ptr(projection));
glUniformMatrix4fv(locView, 1, GL_FALSE, glm::value_ptr(view));
for (GLuint i = 0; i < lightPositions.size(); i++)
{model = glm::mat4();model = glm::translate(model, lightPositions[i]);model = glm::scale(model, glm::vec3(0.25f));glUniformMatrix4fv(locModel, 1, GL_FALSE, glm::value_ptr(model));glUniform3fv(locLightcolor, 1, &lightColors[i][0]);RenderCube();
}

然而,这些渲染出来的立方体并没有考虑到我们储存的延迟渲染器的几何深度(Depth)信息,并且结果是它被渲染在之前渲染过的物体之上,这并不是我们想要的结果。

我们需要做的就是首先复制出在几何渲染阶段中储存的深度信息,并输出到默认的帧缓冲的深度缓冲,然后我们才渲染光立方体。这样之后只有当它在之前渲染过的几何体上方的时候,光立方体的片段才会被渲染出来。我们可以使用glBlitFramebuffer复制一个帧缓冲的内容到另一个帧缓冲中,这个函数我们也在抗锯齿的博客中使用过,用来还原多重采样的帧缓冲。glBlitFramebuffer这个函数允许我们复制一个用户定义的帧缓冲区域到另一个用户定义的帧缓冲区域。

我们储存所有延迟渲染阶段中所有物体的深度信息在gBuffer这个FBO中。如果我们仅仅是简单复制它的深度缓冲内容到默认帧缓冲的深度缓冲中,那么光立方体就会像是场景中所有的几何体都是正向渲染出来的一样渲染出来。就像在抗锯齿博客中介绍的那样,我们需要指定一个帧缓冲为读帧缓冲(Read Framebuffer),并且类似地指定一个帧缓冲为写帧缓冲(Write Framebuffer):

glBindFramebuffer(GL_READ_FRAMEBUFFER, gBuffer);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); // 写入到默认帧缓冲
glBlitFramebuffer(0, 0, SCR_WIDTH, SCR_HEIGHT, 0, 0, SCR_WIDTH, SCR_HEIGHT, GL_DEPTH_BUFFER_BIT, GL_NEAREST
);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// 现在像之前一样渲染光立方体
[...] 

在这里我们复制整个读帧缓冲的深度缓冲信息到默认帧缓冲的深度缓冲,对于颜色缓冲和模板缓冲我们也可以这样处理。现在如果我们接下来再渲染光立方体,场景里的几何体将会看起来很真实了,而不只是简单地粘贴立方体到2D方形之上:


在这里我们复制整个读帧缓冲的深度缓冲信息到默认帧缓冲的深度缓冲,对于颜色缓冲和模板缓冲我们也可以这样处理。现在如果我们接下来再渲染光立方体,场景里的几何体将会看起来很真实了,而不只是简单地粘贴立方体到2D方形之上:

下面把片段着色器代码给读者展示如下:

#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;
layout (location = 2) in vec2 texCoords;uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;void main()
{gl_Position = projection * view * model * vec4(position, 1.0f);
}

片段着色器代码如下所示:

#version 330 core
layout (location = 0) out vec4 FragColor;uniform vec3 lightColor;void main()
{           FragColor = vec4(lightColor, 1.0);
}

延迟渲染一直被称赞的原因就是它能够渲染大量的光源而不消耗大量的性能。然而,延迟渲染它本身并不能支持非常大量的光源,因为我们仍然必须要对场景中每一个光源计算每一个片段的光照分量。真正让大量光源成为可能的是我们能够对延迟渲染管线引用的一个非常棒的优化:光体积(Light Volumes)

通常情况下,当我们渲染一个复杂光照场景下的片段着色器时,我们会计算场景中每一个光源的贡献,不管它们离这个片段有多远。很大一部分的光源根本就不会到达这个片段,所以为什么我们还要浪费这么多光照运算呢?

隐藏在光体积背后的想法就是计算光源的半径,或是体积,也就是光能够到达片段的范围。由于大部分光源都使用了某种形式的衰减(Attenuation),我们可以用它来计算光源能够到达的最大路程,或者说是半径。我们接下来只需要对那些在一个或多个光体积内的片段进行繁重的光照运算就行了。这可以给我们省下来很可观的计算量,因为我们现在只在需要的情况下计算光照。

这个方法的难点基本就是找出一个光源光体积的大小,或者是半径。

为了获取一个光源的体积半径,我们需要解一个对于一个我们认为是 黑暗(Dark) 的亮度(Brightness)的衰减方程,它可以是0.0,或者是更亮一点的但仍被认为黑暗的值,像是0.03。为了展示我们如何计算光源的体积半径,我们将会使用一个在投光物 这节中引入的一个更加复杂,但非常灵活的衰减方程:

我们现在想要在Flight等于0的前提下解这个方程,也就是说光在该距离完全是黑暗的。然而这个方程永远不会真正等于0.0,所以它没有解。所以,我们不会求表达式等于0.0时候的解,相反我们会求当亮度值靠近于0.0的解,这时候它还是能被看做是黑暗的。在这个教程的演示场景中,我们选择5/256作为一个合适的光照值;除以256是因为默认的8-bit帧缓冲可以每个分量显示这么多强度值(Intensity)。

我们要求的衰减方程会是这样:

在这里,Imax是光源最亮的颜色分量。我们之所以使用光源最亮的颜色分量是因为解光源最亮的强度值方程最好地反映了理想光体积半径。

从这里我们继续解方程:

最后的方程形成了ax2+bx+c=0的形式,我们可以用求根公式来解这个二次方程:

它给我们了一个通用公式从而允许我们计算xx的值,即光源的光体积半径,只要我们提供了一个常量,线性和二次项参数:

GLfloat constant  = 1.0;
GLfloat linear    = 0.7;
GLfloat quadratic = 1.8;
GLfloat lightMax  = std::fmaxf(std::fmaxf(lightColor.r, lightColor.g), lightColor.b);
GLfloat radius    = (-linear +  std::sqrtf(linear * linear - 4 * quadratic * (constant - (256.0 / 5.0) * lightMax))) / (2 * quadratic);  

它会返回一个大概在1.0到5.0范围内的半径值,它取决于光的最大强度。

对于场景中每一个光源,我们都计算它的半径,并仅在片段在光源的体积内部时才计算该光源的光照。下面是更新过的光照处理阶段片段着色器,它考虑到了计算出来的光体积。注意这种方法仅仅用作教学目的,在实际场景中是不可行的,我们会在后面讨论它:

struct Light {[...]float Radius;
}; void main()
{[...]for(int i = 0; i < NR_LIGHTS; ++i){// 计算光源和该片段间距离float distance = length(lights[i].Position - FragPos);if(distance < lights[i].Radius){// 执行大开销光照[...]}}
}

这次的结果和之前一模一样,但是这次物体只对所在光体积的光源计算光照。下面给出片段着色器代码如下:

#version 330 core
out vec4 FragColor;
in vec2 TexCoords;uniform sampler2D gPosition;
uniform sampler2D gNormal;
uniform sampler2D gAlbedoSpec;struct Light {vec3 Position;vec3 Color;float Linear;float Quadratic;float Radius;
};
const int NR_LIGHTS = 32;
uniform Light lights[NR_LIGHTS];
uniform vec3 viewPos;void main()
{             // Retrieve data from gbuffervec3 FragPos = texture(gPosition, TexCoords).rgb;vec3 Normal = texture(gNormal, TexCoords).rgb;vec3 Diffuse = texture(gAlbedoSpec, TexCoords).rgb;float Specular = texture(gAlbedoSpec, TexCoords).a;// Then calculate lighting as usualvec3 lighting  = Diffuse * 0.1; // hard-coded ambient componentvec3 viewDir  = normalize(viewPos - FragPos);for(int i = 0; i < NR_LIGHTS; ++i){// Calculate distance between light source and current fragmentfloat distance = length(lights[i].Position - FragPos);if(distance < lights[i].Radius){// Diffusevec3 lightDir = normalize(lights[i].Position - FragPos);vec3 diffuse = max(dot(Normal, lightDir), 0.0) * Diffuse * lights[i].Color;// Specularvec3 halfwayDir = normalize(lightDir + viewDir);  float spec = pow(max(dot(Normal, halfwayDir), 0.0), 16.0);vec3 specular = lights[i].Color * spec * Specular;// Attenuationfloat attenuation = 1.0 / (1.0 + lights[i].Linear * distance + lights[i].Quadratic * distance * distance);diffuse *= attenuation;specular *= attenuation;lighting += diffuse + specular;}}      FragColor = vec4(lighting, 1.0);
}

上面那个片段着色器在实际情况下不能真正地工作,并且它只演示了我们可以不知怎样能使用光体积减少光照运算。然而事实上,你的GPU和GLSL并不擅长优化循环和分支。这一缺陷的原因是GPU中着色器的运行是高度并行的,大部分的架构要求对于一个大的线程集合,GPU需要对它运行完全一样的着色器代码从而获得高效率。这通常意味着一个着色器运行时总是执行一个if语句所有的分支从而保证着色器运行都是一样的,这使得我们之前的半径检测优化完全变得无用,我们仍然在对所有光源计算光照!

使用光体积更好的方法是渲染一个实际的球体,并根据光体积的半径缩放。这些球的中心放置在光源的位置,由于它是根据光体积半径缩放的,这个球体正好覆盖了光的可视体积。这就是我们的技巧:我们使用大体相同的延迟片段着色器来渲染球体。因为球体产生了完全匹配于受影响像素的着色器调用,我们只渲染了受影响的像素而跳过其它的像素。下面这幅图展示了这一技巧:

它被应用在场景中每个光源上,并且所得的片段相加混合在一起。这个结果和之前场景是一样的,但这一次只渲染对于光源相关的片段。它有效地减少了从nr_objects * nr_lightsnr_objects + nr_lights的计算量,这使得多光源场景的渲染变得无比高效。这正是为什么延迟渲染非常适合渲染很大数量光源。

然而这个方法仍然有一个问题:面剔除(Face Culling)需要被启用(否则我们会渲染一个光效果两次),并且在它启用的时候用户可能进入一个光源的光体积,然而这样之后这个体积就不再被渲染了(由于背面剔除),这会使得光源的影响消失。这个问题可以通过一个模板缓冲技巧来解决。

渲染光体积确实会带来沉重的性能负担,虽然它通常比普通的延迟渲染更快,这仍然不是最好的优化。另外两个基于延迟渲染的更流行(并且更高效)的拓展叫做延迟光照(Deferred Lighting)和切片式延迟着色法(Tile-based Deferred Shading)。这些方法会很大程度上提高大量光源渲染的效率,并且也能允许一个相对高效的多重采样抗锯齿(MSAA)。然而受制于这篇博客的长度,我将会在之后的教程中介绍这些优化。

仅仅是延迟着色法它本身(没有光体积)已经是一个很大的优化了,每个像素仅仅运行一个单独的片段着色器,然而对于正向渲染,我们通常会对一个像素运行多次片段着色器。当然,延迟渲染确实带来一些缺点:大内存开销,没有MSAA和混合(仍需要正向渲染的配合)。

当你有一个很小的场景并且没有很多的光源时候,延迟渲染并不一定会更快一点,甚至有些时候由于开销超过了它的优点还会更慢。然而在一个更复杂的场景中,延迟渲染会快速变成一个重要的优化,特别是有了更先进的优化拓展的时候。

最后我仍然想指出,基本上所有能通过正向渲染完成的效果能够同样在延迟渲染场景中实现,这通常需要一些小的翻译步骤。举个例子,如果我们想要在延迟渲染器中使用法线贴图(Normal Mapping),我们需要改变几何渲染阶段着色器来输出一个世界空间法线(World-space Normal),它从法线贴图中提取出来(使用一个TBN矩阵)而不是表面法线,光照渲染阶段中的光照运算一点都不需要变。如果你想要让视差贴图工作,首先你需要在采样一个物体的漫反射,镜面,和法线纹理之前首先置换几何渲染阶段中的纹理坐标。一旦你了解了延迟渲染背后的理念,变得有创造力并不是什么难事。

OpenGL核心技术之延迟着色器提升版相关推荐

  1. OpenGL播放yuv数据流(着色器SHADER)-IOS(一)

    OpenGL播放yuv数据流(着色器SHADER)-IOS(一) 和windows平台的类似,只是用object-c编写的,着色器语言shader,rgb转yuv有些不同,具体看代码注释. //.h ...

  2. OpenGL shader class自定义着色器的实例

    OpenGL shader class自定义着色器 先上图,再解答. 完整主要的源代码 源代码剖析 先上图,再解答. 完整主要的源代码 #include <glad/glad.h> #in ...

  3. OpenGL Compute Shader计算着色器的实例

    OpenGL Compute Shader计算着色器 先上图,再解答. 完整主要的源代码 源代码剖析 先上图,再解答. 完整主要的源代码 // #define USE_GL3W #include &l ...

  4. OpenGL ES渲染管线与着色器

    转自:http://blog.csdn.net/kesalin/article/details/8223649 [OpenGL ES 02]OpenGL ES渲染管线与着色器 罗朝辉 (http:// ...

  5. OpenGL播放yuv数据流(着色器SHADER)-windows(一)

    OpenGL播放yuv数据流(着色器SHADER)-windows(一) 在写这篇文章之前首先要感谢老雷,http://blog.csdn.net/leixiaohua1020/article/det ...

  6. Opengl ES系列学习--顶点着色器

    本节我们继续来看一下<OPENGL ES 3.0编程指南 原书第2版(中文版)>书中第8章的内容,PDF下载地址:OPENGL ES 3.0编程指南 原书第2版(中文版),代码下载地址:O ...

  7. [OpenGL ES 02]OpenGL ES渲染管线与着色器

    http://blog.csdn.net/kesalin/article/details/8223649 罗朝辉 (http://blog.csdn.net/kesalin) 本文遵循"署名 ...

  8. OpenGL基础6:着色器

    如果想要完整的代码,可以用上一章最下面那份代码,然后进行局部替换就OK了 一.着色器结构 一个简单的着色器如下: #version 3.4 //版本号in type name1 //输入数据 out ...

  9. OpenGL(三)——着色器

    目录 一.前言 二.Shader 2 Shader 2.1 顶点着色器 2.2 片段着色器 三.APP 2 Shader 四.顶点颜色属性 五.着色器类C++ 一.前言 着色器Shader是运行在GP ...

  10. Opengl入门基础-shader着色器画方形并且填颜色

    文章目录 一.目的 二.结果 三.详细过程 下载 一.目的 opengl shader画方形并且填颜色 二.结果 成功画方形并用shader填充彩色 三.详细过程 https://blog.csdn. ...

最新文章

  1. 为什么说数据中心是5G最大的受益者?
  2. LINUX - /etc/init.d/nginx: line 51: kill: (29833) - No such process
  3. 全文索引 - Pomelo.EFCore.MySql
  4. nodejs 调用微服务器_无服务器NodeJS:构建下一个微服务的快速,廉价方法
  5. 西门子安装未找到ssf文件_V5.3安装时显示NO SSF FILE FOUND对话框,我该怎么解决啊? 谢谢!-工业支持中心-西门子中国...
  6. spark学习-58-Spark的EventLoggingListener
  7. 如何跨服务器复制表中数据
  8. 初学shell,今天遇到由wget下载到本地的网页源代码的乱码问题,无聊的写了一个转码的脚本...
  9. Redlock 算法:Redis 实现分布式锁(译)
  10. 金属粉末增材制造行业调研报告 - 市场现状分析与发展前景预测
  11. Unity3D之NGUI基础5.1:代码控制UISprite
  12. Git使用方法(二)-远程库与分支
  13. 《网络攻防第六周作业》
  14. ROS学习笔记(一)#ROS系统及RoboWare的安装
  15. 适合外贸建站的vatage主题教程
  16. 软件测试培训到底值不值得参加?
  17. Duilib使用wke显示echarts
  18. What's new in Xcode 8
  19. 微机原理与接口 极其基础知识点
  20. 适合前端新手的十大网站

热门文章

  1. 蔡高厅高等数学28-高阶导数
  2. R语言数据可视化-条形图
  3. 如何清除电脑桌面图标蓝底
  4. 有趣且有意义的数字,你想到了什么?请不吝留言
  5. Python GUI工具——取色器
  6. 阿里云OSS对象存储基础入门
  7. vue3获取url后面参数
  8. atmega128 单片机 20以内加减法训练机 做的过程
  9. Common Lisp 超规范(译文):5.数据和控制流
  10. 高速公路超速处罚:按照规定,在高速公路上行驶的机动车,超出本车道限速的10%则处200元的罚款;若超出50%,就要吊销驾驶证。请编写