1
0
mirror of https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git synced 2025-08-23 04:35:28 +08:00
This commit is contained in:
Meow J
2015-07-18 18:54:32 +08:00
5 changed files with 526 additions and 0 deletions

View File

@@ -0,0 +1,103 @@
##面剔除Face culling
本文作者JoeyDeVries由Django翻译自http://learnopengl.com
尝试在头脑中想象一下有一个3D立方体你从任何一个方向去看他你最多可以一次看到多少个面。如果你的想象力不是过于丰富你最终最多能数出来的面是3个。你可以从一个立方体的任意位置和方向上去看它但是你永远不能看到多于3个面。所以我们为何还要去绘制那三个不会显示出来的3个面呢。如果我们可以以某种方式丢弃它们我们会提高像素着色器50%的性能!
!!! Important
我们所说的是大于50%而不是50%因为从一个角度只有2个或1个面能够被看到。这种情况下我们就能够节约50%以上的了。
![](http://learnopengl.com/img/advanced/faceculling_windingorder.png)
这的确是个好主意,但是有个问题需要我们去解决:我们如何知道某个面在观察者的视野中不会出现呢?如果我们去想象任何封闭的形状,它每个面有两面。一面面向用户,另一面背对用户。假如我们只渲染面向观察者的面会怎样?
这正是面剔除所做的face culling。OpenGL检查所有正面朝向front facing观察者的面并渲染它们而丢弃所有背面朝向back facing的面这样就节约了我们很多像素着色器的命令它们很昂贵。我们必须告诉OpenGL我们使用的哪个面是正面哪个面是反面。OpenGL使用一种聪明的手段解决这个问题——分析顶点数据的链接顺序winding order
25.1 顶点链接顺序winding order
当我们定义一系列的三角顶点时我们会把它们定义为一个特定的链接顺序它们可能是顺时针的或逆时针的。每个三角形由3个顶点组成我们从三角形的中间去看从而把这三个顶点指定一个链接顺序。
正如你所看到的那样我们先定义了顶点1接着我们定义顶点2或3这个不同的选择决定了这个三角形的链接顺序。下面的代码展示出这点
```c++
GLfloat vertices[] = {
//顺时针
vertices[0], // vertex 1
vertices[1], // vertex 2
vertices[2], // vertex 3
// 逆时针
vertices[0], // vertex 1
vertices[2], // vertex 3
vertices[1] // vertex 2
};
```
每三个顶点都形成了一个包含着链接顺序的基本三角形。OpenGL使用这个信息在渲染你的基本图形的时候决定这个三角形是三角形的正面还是三角形的背面。默认情况下逆时针的顶点链接顺序被定义为三角形的正面。
当定义你的顶点顺序时你如果定义能够看到的一个三角形那它一定是正面朝向的所以你定义的三角形应该是逆时针的就像你直接面向这个三角形。把所有的顶点指定成这样是件炫酷的事实际的顶点链接顺序是在像素化阶段rasterization stage计算的所以当顶点着色器已经运行后。顶点就能够在观察者的观察点被看到。
我们指定了它们以后,观察者面对的所有的三角形的顶点的链接顺序都是正确的,但是现在渲染的立方体另一面的三角形的顶点的链接顺序被反转。最终,我们所面对的三角形被视为正面朝向的三角形,后部的三角形被视为背面朝向的三角形。下图展示了这个效果:
![](http://learnopengl.com/img/advanced/faceculling_frontback.png)
在顶点数据中我们定义的是两个逆时针顺序的前面的三角是1、2、3后面剪的也是1、2、3如果我们从这个三角的前面去观察。然而从观察者的方面看后面的三角形是顺时针的如果我们仍以1、2、3的顺序以观察者当面的视野看的话。即使我们以逆时针顺序定义后面的三角形它现在还是变为顺时针。它正是我们打算剔除丢弃的不可见的面
###面剔除
在教程的开头我们说过OpenGL可以丢弃背面朝向的三角形。现在我们知道了如何设置顶点的链接顺序我们可以开始使用OpenGL默认关闭的面剔除选项了。
记住我们上街所使用的立方体的定点数据不是以逆时针顺序定义的。所以我更新了顶点数据,好去反应为一个逆时针链接顺序,你可以从这里复制它。把所有三角的顶点都定义为逆时针是一个很好的习惯。
开启OpenGL的GL_CULL_FACE选项就能开启面剔除功能
glEnable(GL_CULL_FACE);
从这儿以后所有的不是正面朝向的面都会被丢弃尝试飞入立方体看看里面什么面都看不见了。目前在渲染fragment上我们节约了50%的性能,但记住这只对像立方体这样的封闭形状有效。当我们绘制上个教程中那个草的时候,我们必须关闭面剔除,这是因为它的前、后面都必须是可见的。
OpenGL允许我们改变剔除面的类型。要是我们剔除正面而不是背面会怎样我们可以调用glCullFace来做这件事
```c++
glCullFace(GL_BACK);
```
glCullFace函数有三个可用的选项
* GL_BACK只剔除背面。
* GL_FRONT只剔除正面。
* GL_FRONT_AND_BACK剔除背面和正面。
glCullFace的初始值是GL_BACK。出剔除面以外我们还可以告诉OpenGL使用顺时针而不是逆时针来表示正面这通过glFrontFace来设置
```c++
glFrontFace(GL_CCW);
```
默认值是GL_CCW它代表逆时针GL_CW代表顺时针顺序。
我们可以做个小实验告诉OpenGL现在顺时针代表正面
```c++
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
glFrontFace(GL_CW);
```
最后的结果只有背面被渲染了:
![](http://learnopengl.com/img/advanced/faceculling_reverse.png)
要注意,你可以使用默认逆时针顺序剔除正面,来创建相同的效果:
```c
glEnable(GL_CULL_FACE);
glCullFace(GL_FRONT);
```
正如你所看到的那样面剔除是OpenGL提高效率的一个强大工具它使应用节省运算。你必须跟踪下来哪个物体可以使用面剔除哪些不能。
###练习
你可以自己重新定义一个顺时针的顶点顺序,然后用顺时针作为正面把它渲染出来吗:[解决方案](http://learnopengl.com/code_viewer.php?code=advanced/faceculling-exercise1)。

View File

@@ -0,0 +1,423 @@
##帧缓冲Framebuffers
本文作者JoeyDeVries由Django翻译自http://learnopengl.com
到目前为止我们使用了几种不同类型的屏幕缓冲颜色缓冲用于写入颜色值深度缓冲用于写入深度信息最后模板缓冲允许我们基于一些条件丢弃指定fragment。把这几种缓冲结合起来叫做帧缓冲译注framebuffer在维基百科有framebuffer的详细介绍能够帮助你更好的理解它被储存于内存的某处。OpenGL给了我们自己定义帧缓冲的自由我们可以选择性的定义自己的颜色缓冲、以及选择性地定义深度和模板缓冲。
我们目前所做的渲染操作都是是在默认的帧缓冲之上进行的。当你创建了你的窗口的时候默认帧缓冲就被创建和配置好了GLFW为我们做了这件事。通过创建我们自己的帧缓冲我们能够获得一种额外的渲染方式。
你不会立刻理解应用程序的帧缓的含义,但是把你的场景渲染到一个不同的帧缓冲中,可以使我们能够在场景中创建镜子这样的效果,或者做出一些炫酷的后处理特效。首先我们会讨论它们是如何工作的,然后我们将利用它们来实现一些炫酷的后处理效果。
###创建一个帧缓冲
就像OpenGL中其他对象一样我们可以使用一个叫做glGenFramebuffers的函数来创建一个帧缓冲对象简称FBO
```c++
GLuint fbo;
glGenFramebuffers(1, &fbo);
```
这种对象的创建和使用的方式我们已经见过不少了因此它们的使用方式也和之前我们见过的其他对象的使用方式相似首先我们要创建一个帧缓冲对象把它绑定到当前帧缓冲做一些操作然后解绑帧缓冲。我们使用glBindFramebuffer来绑定帧缓冲
```c++
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
```
绑定到GL_FRAMEBUFFER目标后接下来所有的读、写帧缓冲的操作都会影响到当前绑定的帧缓冲。也可以把帧缓冲分开绑定到读或写目标上分别使用GL_READ_FRAMEBUFFER或GL_DRAW_FRAMEBUFFER来做这件事。帧缓冲如果绑定到了GL_READ_FRAMEBUFFER就能被所有读取操作像glReadPixels这样的函数使用了绑定到GL_DRAW_FRAMEBUFFER上就被作为渲染、清空和其他写操作的目标。大多数时候你不必分开用通常把两个都绑定到GL_FRAMEBUFFER上就行。
很遗憾,现在我们不能使用自己的帧缓冲,因为还没做完呢。建构一个完整的帧缓冲必须满足以下条件:
* 我们必须往里面加入至少一个附件(颜色、深度、模板缓冲)。
* 其中至少有一个是颜色附件。
* 所有的附件都应该是已经完全做好的(已经存储在内存之中)。
* 每个缓冲都应该有同样数目的样本。
如果你不知道什么是样本也不用担心,我们会在后面的教程中讲到。
从上面的需求中你可以看到我们需要为帧缓冲创建一些附件把这个附件附加到帧缓冲上。当我们做完所有上面提到的条件的时候我们就可以用glCheckFramebufferStatus带上GL_FRAMEBUFFER这个参数来检查是否真的成功做到了。然后检查当前绑定的帧缓冲返回了这些规范中的哪个值。如果返回的是GL_FRAMEBUFFER_COMPLETE就对了
```c++
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)
// Execute victory dance
```
后续所有渲染操作将渲染到当前绑定的帧缓冲的附加缓冲中由于我们的帧缓冲不是默认的帧缓冲渲染命令对窗口的视频输出不会产生任何影响。出于这个原因它被称为离屏渲染off-screen rendering就是渲染到一个另外的缓冲中。为了让所有的渲染操作对主窗口产生影响我们必须通过绑定为0来使默认帧缓冲激活
```c++
glBindFramebuffer(GL_FRAMEBUFFER, 0);
```
当我们做完所有帧缓冲操作,不要忘记删除帧缓冲对象:
```c++
glDeleteFramebuffers(1, &fbo);
```
现在在执行完成检测前我们需要把一个或更多的附件附加到帧缓冲上。一个附件就是一个内存地址这个内存地址里面包含一个为帧缓冲准备的缓冲它可以是个图像。当创建一个附件的时候我们有两种方式可以采用纹理或渲染缓冲renderbuffer对象。
###纹理附件
当把一个纹理附加到帧缓冲上的时候,所有渲染命令会写入到纹理上,就像它是一个普通的颜色/深度或者模板缓冲一样。使用纹理的好处是,所有渲染操作的结果都会被储存为一个纹理图像,这样我们就可以简单的在着色器中使用了。
创建一个帧缓冲的纹理和创建普通纹理差不多:
```c++
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
```
这里主要的区别是我们把维度设置为屏幕代销尽管不是必须的我们还传递NULL作为纹理的data参数。对于这个纹理我们之分配内存而不去填充它。纹理填充会在渲染到帧缓冲的时候去做。同样要注意我们不关心放置方式或者mipmap的事因为在大多数时候都不会需要它们的。
如果你打算把整个屏幕渲染到一个或大或小的纹理上你需要用新的纹理的尺寸作为参数再次调用glViewport要在渲染到你的帧缓冲之前做好否则只有一小部分纹理或屏幕能够绘制到纹理上。
现在我们已经创建了一个纹理,最后一件要做的事情是把它附加到帧缓冲上:
```c++
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D, texture, 0);
```
glFramebufferTexture2D函数有以下参数
* target我们所创建的帧缓冲类型的目标绘制、读取或两者都有
* attachment我们所附加的附件的类型。现在我们附加的是一个颜色附件。需要注意最后的那个0是暗示我们可以附加1个以上颜色的附件。我们会在后面的教程中谈到。
* textarget你希望附加的纹理类型。
* texture附加的实际纹理。
* levelmipmap level。我们设置为0.
除颜色附件以外我们还可以附加一个深度和一个模板纹理到帧缓冲对象上。为了附加一个深度缓冲我们可以知道那个GL_DEPTH_ATTACHMENT作为附件类型。记住这时纹理格式和内部格式类型internalformat就成了GL_DEPTH_COMPONENT去反应深度缓冲的存储格式。附加一个模板缓冲你要使用GL_STENCIL_ATTACHMENT作为第二个参数把纹理格式指定为GL_STENCIL_INDEX。
也可以同时附加一个深度缓冲和一个模板缓冲为一个单独的纹理。这样纹理的每32位数值就包含了24位的深度信息和8位的模板信息。为了把一个深度和模板缓冲附加到一个单独纹理上我们使用GL_DEPTH_STENCIL_ATTACHMENT类型配置纹理格式以包含深度值和模板值的结合物。下面是一个附加了深度和模板缓冲为单一纹理的例子
```c++
glTexImage2D(
GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, 800, 600, 0,
GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, NULL
);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, texture, 0);
```
渲染缓冲对象附件Renderbuffer object attachments
在介绍了帧缓冲的可行附件类型——纹理后OpenGL引进了渲染缓冲对象Renderbuffer objects所以在过去那些美好时光里纹理是附件的唯一可用的类型。和纹理图像一样渲染缓冲对象也是一个缓冲它可以是一堆字节、整数、像素或者其他东西。渲染缓冲对象的一大优点是它以OpenGL原生渲染格式储存它的数据因此在离屏渲染到帧缓冲的时候这些数据就相当于被优化过的了。
渲染缓冲对象将所有渲染数据直接储存到它们的缓冲里而不会进行针对特定纹理格式的任何转换这样它们就成了一种快速可写的存储介质了。然而渲染缓冲对象通常是只写的要不能读取它们就像获取纹理不能写入纹理一样。可以用glReadPixels函数去读取函数返回一个当前绑定的帧缓冲的特定像素区域而不是直接返回附件本身。
因为它们的数据已经是原生格式了在写入或把它们的数据简单地到其他缓冲的时候非常快。当使用渲染缓冲对象时像切换缓冲这种操作变得异常高速。我们在没个渲染迭代末尾使用的那个glfwSwapBuffers函数同样以渲染缓冲对象实现我们简单地写入到一个渲染缓冲图像最后交换到另一个里。渲染缓冲对象对于这种操作来说很完美。
创建一个渲染缓冲对象和创建帧缓冲代码差不多:
```c++
GLuint rbo;
glGenRenderbuffers(1, &rbo);
```
相似地我们打算把渲染缓冲对象绑定这样所有后续渲染缓冲操作都会影响到当前的RBO
```c++
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
```
由于渲染缓冲对象通常是只写的它们经常作为深度和模板附件来使用由于大多数时候我们不需要从深度和模板缓冲中读取数据但仍关系深度和模板测试。我们就需要有深度和模板值提供给测试但不需要对这些值进行采样sample所以深度缓冲对象是完全符合的。当我们不去从这些缓冲中采样的时候渲染缓冲对象通常很合适因为它们等于是优化过的。
调用glRenderbufferStorage函数可以创建一个深度和模板渲染缓冲对象
```c
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);
```
创建一个渲染缓冲对象与创建纹理对象相似不同之处在于这个对象是专门被设计用于图像的而不是通用目的的数据缓冲比如纹理。这里我们选择GL_DEPTH24_STENCIL8作为内部格式它同时代表24位的深度和8位的模板缓冲。
最后一件还要做的事情是把帧缓冲对象附加上:
```c
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
```
在帧缓冲项目中,渲染缓冲对象可以提供一些优化,但更重要的是知道何时使用渲染缓冲对象,何时使用纹理。通常的规则是,如果你永远都不需要从特定的缓冲中进行采样,渲染缓冲对象对特定缓冲是更明智的选择。如果哪天需要从比如颜色或深度值这样的特定缓冲采样数据的话,你最好还是使用纹理附件。从执行效率角度考虑,它不会对效率有太大影响。
###渲染到纹理
现在我们知道了(一些)帧缓冲如何工作的,是时候把它们用起来了。我们会把场景渲染到一个颜色纹理上,这个纹理附加到一个我们创建的帧缓冲上,然后把纹理绘制到一个简单的四边形上,这个四边形铺满整个屏幕。输出的图像看似和没用帧缓冲一样,但是这次,它其实是直接打印到了一个单独的四边形上面。为什么这很有用呢?下一部分我们会看到原因。
第一件要做的事情是创建一个帧缓冲对象,并绑定它,这比较明了:
```c++
GLuint framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
```
下一步我们创建一个纹理图像,这是我们将要附加到帧缓冲的颜色附件。我们把纹理的尺寸设置为窗口的宽度和高度,并保持数据未初始化:
```c++
// Generate texture
GLuint texColorBuffer;
glGenTextures(1, &texColorBuffer);
glBindTexture(GL_TEXTURE_2D, texColorBuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);
// Attach it to currently bound framebuffer object
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texColorBuffer, 0);
```
我们同样打算要让OpenGL确定可以进行深度测试模板测试如果你用的话所以我们必须还要确保向帧缓冲中添加一个深度和模板附件。由于我们只采样颜色缓冲并不采样其他缓冲我们可以创建一个渲染缓冲对象来达到这个目的。记住当你不打算从指定缓冲采样的的时候它们是一个不错的选择。
创建一个渲染缓冲对象不太难。唯一一件要记住的事情是我们正在创建的是一个渲染缓冲对象的深度和模板附件。我们把它的内部给事设置为GL_DEPTH24_STENCIL8对于我们的目的来说这个精确度已经足够了。
```c++
GLuint rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
```
我们为渲染缓冲对象分配了足够的内存空间以后,我们可以解绑渲染缓冲。
接着,在做好帧缓冲之前,还有最后一步,我们把渲染缓冲对象附加到帧缓冲的深度和模板附件上:
```c
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
```
然后我们要检查帧缓冲是否真的做好了,如果没有,我们就打印一个错误消息。
```c++
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << endl;
glBindFramebuffer(GL_FRAMEBUFFER, 0);
```
还要保证解绑帧缓冲,这样我们才不会意外渲染到错误的帧缓冲上。
现在帧缓冲做好了,我们要做的全部就是渲染到帧缓冲的还冲上,而不是绑定到帧缓冲对象的默认缓冲。余下所有命令会影响到当前绑定的帧缓冲上。所有深度和模板操作同样会从当前绑定的帧缓冲的深度和模板附件中读取,当然,得是在它们可用的情况下。如果你遗漏了比如深度缓冲,所有深度测试就不会工作,因为当前绑定的帧缓冲里没有深度缓冲。
所以,为把场景绘制到一个单独的纹理,我们必须以下面步骤来做:
使用新的绑定为激活帧缓冲的帧缓冲,像往常那样渲染场景。
绑定到默认帧缓冲。
绘制一个四边形,让它平铺到整个屏幕上,用新的帧缓冲的颜色缓冲作为他的纹理。
我们使用在深度测试教程中同一个场景进行绘制,但是这次使用老气横秋的箱子纹理。
为了绘制四边形我们将会创建新的着色器。我们不打算引入任何花哨的变换矩阵,因为我们只提供已经是标准化设备坐标的顶点坐标,所以我们可以直接把它们作为顶点着色器的输出。顶点着色器看起来像这样:
```c++
#version 330 core
layout (location = 0) in vec2 position;
layout (location = 1) in vec2 texCoords;
out vec2 TexCoords;
void main()
{
gl_Position = vec4(position.x, position.y, 0.0f, 1.0f);
TexCoords = texCoords;
}
```
没有花哨的地方。像素着色器更简洁,因为我们做的唯一一件事是从纹理采样:
```c++
#version 330 core
in vec2 TexCoords;
out vec4 color;
uniform sampler2D screenTexture;
void main()
{
color = texture(screenTexture, TexCoords);
}
```
接着需要你为屏幕上的四边形创建和配置一个VAO。渲染迭代中帧缓冲处理会有下面的结构
```c++
// First pass
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // We're not using stencil buffer now
glEnable(GL_DEPTH_TEST);
DrawScene();
// Second pass
glBindFramebuffer(GL_FRAMEBUFFER, 0); // back to default
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
screenShader.Use();
glBindVertexArray(quadVAO);
glDisable(GL_DEPTH_TEST);
glBindTexture(GL_TEXTURE_2D, textureColorbuffer);
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);
```
只有很少的事情要说明。第一由于我们用的每个帧缓冲都有自己的一系列缓冲我们打算使用glClear设置的合适的位bits来清空这些缓冲。第二当渲染四边形的时候我们关闭深度测试因为我们不关系深度测试我们绘制的是一个简单的四边形当我们绘制普通场景时我们必须再次开启深度测试。
这里的确有很多地方会做错,所以如果你没有获得任何输出,尝试排查任何可能出现错误的地方,再次阅读教程中相关章节。如果每件事都做对了就一定能成功,你将会得到这样的输出:
![](http://learnopengl.com/img/advanced/framebuffers_screen_texture.png)
左侧展示了和深度测试教程中一样的输出结果,但是这次却是渲染到一个简单的四边形上的。如果我们以线框方式显示的话,那么显然,我们只是绘制了一个默认帧缓冲中单调的四边形。
你可以从这里得到应用的源码。
然而这有什么好处呢?好处就是我们现在可以自由的获取已经渲染场景中的任何像素,然后把它当作一个纹理图像了,我们可以在像素着色器中创建一些有意思的效果。所有这些有意思的效果统称为后处理特效。
###后处理
现在,整个场景渲染到了一个单独的纹理上,我们可以创建一些有趣的效果,只要简单操纵纹理数据就能做到。这部分,我们会向你展示一些流行的后处理特效,以及怎样添加一些创造性去创建出你自己的特效。
###反相
我们已经取得了渲染输出的每个颜色所以在像素着色器里返回这些颜色的反色并不难。我们得到屏幕纹理的颜色然后用1.0减去它:
```c++
void main()
{
color = vec4(vec3(1.0 - texture(screenTexture, TexCoords)), 1.0);
}
```
虽然反相是一种相对简单的后处理特效,但是已经很有趣了:
![image description](http://learnopengl.com/img/advanced/framebuffers_grayscale.png)
整个场景现在的颜色都反转了,只需在着色器中写一行代码就能做到,酷吧?
###灰度
另一个有意思的效果是移除所有除了黑白灰以外的颜色作用,是整个图像成为黑白的。实现它的简单的方式是获得所有颜色元素,然后将它们平均化:
```c++
void main()
{
color = texture(screenTexture, TexCoords);
float average = (color.r + color.g + color.b) / 3.0;
color = vec4(average, average, average, 1.0);
}
```
这已经创造出很赞的效果了,但是人眼趋向于对绿色更敏感,对蓝色感知比较弱,所以为了获得更精确的符合人体物理的结果,我们需要使用加权通道:
```c++
void main()
{
color = texture(screenTexture, TexCoords);
float average = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b;
color = vec4(average, average, average, 1.0);
}
```
![image description](http://learnopengl.com/img/advanced/framebuffers_grayscale.png)
###Kernel effects
在单独纹理图像上进行后处理的另一个好处是我们可以从纹理的其他部分进行采样。比如我们可以从当前纹理值的周围采样多个纹理值。创造性地把它们结合起来就能创造出有趣的效果了。
kernel是一个长得有点像一个小矩阵的数值数组它中间的值中心可以映射到一个像素上这个像素和这个像素周围的值再乘以kernel最后再把结果相加就能得到一个值。所以我们基本上就是给当前纹理坐标加上一个它四周的偏移量然后基于kernel把它们结合起来。下面是一个kernel的例子
![](http://learnopengl-cn.readthedocs.org/zh/latest/img/05_05framebuffers_ kernel_sample.png)
这个kernel表示一个像素周围八个像素乘以2它自己乘以-15。这个例子基本上就是把周围像素乘上2中间像素去乘以一个比较大的负数来进行平衡。
!!! Important
你在网上能找到的kernel的例子大多数都是所有值加起来等于1如果加起来不等于1就意味着这个纹理值比原来更大或者更小了。
kernel对于后处理来说非常管用因为用起来简单。网上能找到有很多实例为了能用上kernel我们还得改改像素着色器。这里假设每个kernel都是3×3实际上大多数都是3×3
```c++
const float offset = 1.0 / 300;
void main()
{
vec2 offsets[9] = vec2[](
vec2(-offset, offset), // top-left
vec2(0.0f, offset), // top-center
vec2(offset, offset), // top-right
vec2(-offset, 0.0f), // center-left
vec2(0.0f, 0.0f), // center-center
vec2(offset, 0.0f), // center-right
vec2(-offset, -offset), // bottom-left
vec2(0.0f, -offset), // bottom-center
vec2(offset, -offset) // bottom-right
);
float kernel[9] = float[](
-1, -1, -1,
-1, 9, -1,
-1, -1, -1
);
vec3 sampleTex[9];
for(int i = 0; i < 9; i++)
{
sampleTex[i] = vec3(texture(screenTexture, TexCoords.st + offsets[i]));
}
vec3 col;
for(int i = 0; i < 9; i++)
col += sampleTex[i] * kernel[i];
color = vec4(col, 1.0);
}
```
在像素着色器中我们先为每个四周的纹理坐标创建一个9个vec2偏移量的数组。偏移量是一个简单的常数你可以设置为自己喜欢的。接着我们定义kernel这里应该是一个锐化kernel它通过一种有趣的方式从所有周边的像素采样对每个颜色值进行锐化。最后在采样的时候我们把每个偏移量加到当前纹理坐标上然后用加在一起的kernel的值乘以这些纹理值。
这个锐化的kernel看起来像这样
![](http://learnopengl.com/img/advanced/framebuffers_sharpen.png)
这里创建的有趣的效果就好像你的玩家吞了某种麻醉剂产生的幻觉一样。
###Blur
A kernel that creates a blur effect is defined as follows:
![](http://learnopengl-cn.readthedocs.org/zh/latest/img/05_05framebuffers_ kernel_sample.png)
Because all values add up to 16, simply returning the combined sampled colors would result in an extremely bright color so we have to divide each value of the kernel by 16. The resulting kernel array would then become:
```c++
float kernel[9] = float[](
1.0 / 16, 2.0 / 16, 1.0 / 16,
2.0 / 16, 4.0 / 16, 2.0 / 16,
1.0 / 16, 2.0 / 16, 1.0 / 16
);
```
By changing the kernel float array in the fragment shader we're completely changing the post-processing effect we're after. It now looks something like this:
![](http://learnopengl.com/img/advanced/framebuffers_blur.png)
Such a blur effect creates interesting possibilities. We could vary the blur amount over time for example to create the effect of someone being drunk, or increase the blur whenever the main character is not wearing glasses. Blurring also give us a useful utility to smooth color values which we will use in later tutorials.
You can see that once we have such a little kernel implementation in place it is quite easy to create cool post-processing effects. Let's show you a last popular effect to finish this discussion.
###Edge detection
Below you can find an edge-detection kernel that is similar to the sharpen kernel:
![](http://learnopengl-cn.readthedocs.org/zh/latest/img/05_05_Edge_detection.png)
This kernel highlights all edges and darkens the rest, which is quite useful when we only care about edges in an image.
![](http://learnopengl.com/img/advanced/framebuffers_edge_detection.png)
It probably does not come as a surprise that kernels like this are used as image-manipulating tools/filters in tools like Photoshop. Because of a graphic card's ability to process fragments with extreme parallel capabilities, we can manipulate images on a per-pixel basis in real-time with relative ease. Image-editing tools therefore tend to use graphics cards more often for image-processing.
###Exercises
Can you use framebuffers to create a rear-view mirror? For this you'll have to draw your scene twice: one with the camera rotated 180 degrees and the other as normal. Try to create a small quad on the top of your screen to apply the mirror texture on: solution and visual result.
Play around with the kernel values and create your own interesting post-processing effects. Try searching the internet as well for other interesting kernels.

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
img/05_05_blur_sample.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB