【OpenGL】学习笔记#5
一、帧缓冲
什么是帧缓冲?可以理解为GPU在渲染前预先准备的一个区域,之后将把它渲染成屏幕上的像素。但是,帧缓冲本身并不储存数据,仅仅储存指向数据的指针。所以,帧缓冲需要绑定几个缓冲区,我们特殊地称它们为附件:颜色附件,深度缓冲附件,模板缓冲附件。需要注意的是,一个完整的帧缓冲必须包括一个颜色附件。
除了这种分类之外,附件还可以分为纹理附件和渲染缓冲对象(RBO,Render Buffer Object)。其中,纹理附件就是颜色附件,RBO分为深度缓冲附件和模板缓冲附件。让我们来列一个树形图。
帧缓冲--[纹理附件]--颜色附件
|
[渲染缓冲附件]--深度缓冲附件
|
--模板缓冲附件
为什么要讲到帧缓冲呢?因为帧缓冲是后期处理相当重要的部分。例如阴影,模糊,反相等后期处理都要依靠帧缓冲来实现。打个比方,帧缓冲就好像是拍出来的一张照片,可以让我们PS。
此处的PS便是着色器。OpenGL提供了可以让我们编辑的帧缓冲,好伟大!我们可以把一张帧缓冲保存的2D纹理或深度缓冲获得为一个句柄,在后面直接使用,塞进着色器里。
说了这么多,来看看代码吧,让我们以用着色器编辑一个帧缓冲为例子:
首先生成并绑定一个帧缓冲:
GLuint FramebufferName = 0; glGenFramebuffers(1, &FramebufferName); glBindFramebuffer(GL_FRAMEBUFFER, FramebufferName);
生成一个纹理:
GLuint renderedTexture; glGenTextures(1, &renderedTexture);
状态机不解释:
glBindTexture(GL_TEXTURE_2D, renderedTexture);
由于帧缓冲必须包含一个颜色附件,所以接下来对之前绑定的纹理填充一个空的图像(最后一个参数0代表空,empty):
glTexImage2D(GL_TEXTURE_2D, 0,GL_RGB, windowWidth, windowHeight, 0,GL_RGB, GL_UNSIGNED_BYTE, 0);
可 怜 的 过 滤(性能upup),设置过滤模式:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
由于我们要渲染一个3D模型,所以要进行深度测试,所以来生成并绑定一个渲染缓冲区,将要作为我们的RBO:
GLuint depthrenderbuffer; glGenRenderbuffers(1, &depthrenderbuffer); glBindRenderbuffer(GL_RENDERBUFFER, depthrenderbuffer);
由于OpenGL只知道这是个渲染缓冲区,对其中的数据格式和大小全然不知,所以我们得告诉它,使用glRenderbufferStorage来指定RBO的数据格式和大小,此处指定为深度缓冲区:
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, windowWidth, windowHeight);
对于此函数,第一个参数必须填GL_RENDERBUFFER,第二个指定了类型为深度缓冲,还可以是GL_RGB,GL_RGBA,剩下的两个参数指定了数据长宽,这里设置为屏幕大小。
接下来开始为帧缓冲绑定附件,将此深度缓冲绑定为深度附件:
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthrenderbuffer);
第二个参数指定了类型为深度附件。
然后绑定纹理附件,这个比较特殊,先看代码:
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, renderedTexture, 0);
可以看到,第二个参数并不是指定的类型,而是GL_COLOR_ATTACHMENT0,这意味着什么呢,这意味着颜色附件可以有多个,0代表的是颜色附件的位置。同时我们也发现RBO只能有一个,因为它直接指定了类型。
所以,我们就需要指定接下来渲染时需要渲染到哪些颜色附件上,这次我们只渲染到GL_COLOR_ATTACHMENT0上:
GLenum DrawBuffers[1] = {GL_COLOR_ATTACHMENT0}; glDrawBuffers(1, DrawBuffers);
不需要多说了吧。
终于,帧缓冲的前置操作完成了。这时需要检查你的帧缓冲是否完整,否则会有很可怕的错误:
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) return false;
经常check是好习惯呦~
由于我们要把帧缓冲的图像经过处理绘制到屏幕上,所以一会渲染的时候将要分两个步骤:第一步,按照往常的操作渲染,但是在渲染前把渲染目标绑定到我们自定义的帧缓冲上,此为离屏渲染。然后,通过颜色附件获得渲染得到的深度缓冲和纹理,再把它们传到着色器里处理,作为2D纹理渲染到一个屏幕大小的四边形上。
那么,下面开始定义四边形:
static const GLfloat g_quad_vertex_buffer_data[] = { -1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 0.0f, -1.0f, 1.0f, 0.0f, -1.0f, 1.0f, 0.0f, 1.0f, -1.0f, 0.0f, 1.0f, 1.0f, 0.0f, }; GLuint quad_vertexbuffer; glGenBuffers(1, &quad_vertexbuffer); glBindBuffer(GL_ARRAY_BUFFER, quad_vertexbuffer); glBufferData(GL_ARRAY_BUFFER, sizeof(g_quad_vertex_buffer_data), g_quad_vertex_buffer_data, GL_STATIC_DRAW);
定义顶点缓冲,里面包括的是一个2D充满屏幕的四边形的顶点,注意此时顶点依然是3维的格式,只不过Z=0,相当于2D。
好了,一切都准备好了,下面进入主循环:
首先把渲染目标绑定到我们定义的帧缓冲里:
glBindFramebuffer(GL_FRAMEBUFFER, FramebufferName);
这里是渲染操作...
glUseProgram(programID);
(这里使用通常的着色器)
这里是渲染操作...
然后重头戏来了,开始渲染四边形,先把渲染目标绑定到默认帧缓冲:这个帧缓冲将会直接被GPU提取渲染:
glBindFramebuffer(GL_FRAMEBUFFER, 0);
这里是渲染操作...
glUseProgram(quad_programID);
(使用第二个着色器,这个着色器只负责渲染一个2D纹理并稍稍处理)
这里是渲染操作...
怎么样,这样就完成了呢,小朋友们学会了吗?(狗头)
咳咳,说了这么多,第二个着色器长啥样呢,仅仅渲染一个2D纹理的着色器,来看吧:
#version 330 core // Input vertex data, different for all executions of this shader. layout(location = 0) in vec3 vertexPosition_modelspace; // Output data ; will be interpolated for each fragment. out vec2 UV; void main(){ gl_Position = vec4(vertexPosition_modelspace,1); UV = (vertexPosition_modelspace.xy+vec2(1,1))/2.0; }
出人意料的简单呢......仅仅传出一个UV坐标。不过,这里要注意,我们的顶点坐标xy的范围是[-1,1],而UV坐标uv的范围是[0,1],所以需要(xy+(1,1))/2来转换。
看片元着色器:
#version 330 core in vec2 UV; out vec3 color; uniform sampler2D renderedTexture; uniform float time; void main(){ color = texture( renderedTexture, UV + 0.005*vec2( sin(time+1024.0*UV.x),cos(time+768.0*UV.y)) ).xyz ; }
用时间来做一个偏移量,不多说了吧。
那么看看结果吧:
像素会随着时间的变化慢慢移动喔!