引用链接Learn OpenGL 作者 Joey de Vries
一. Framebuffer(帧缓冲)
什么是Framebuffer
Framebuffer其实很简单,第一,它是一块buffer,也就是一块内存;第二,它是存储的是一帧的buffer数据。之前学到的ColorBuffer、DepthBuffer和StencilBuffer都是Framebuffer,除此之外,OpenGL还允许用户自定义FrameBuffer,可以类似ColorBuffer、DepthBuffer和StencilBuffer这些的操作,自定义的FrameBuffer可以实现离屏渲染,用FrameBuffer可以将场景渲染成一张贴图。
也就是说,Framebuffer分为两种:
- default framebuffer,用来储存depth buffer、stencil buffer和color buffer,然后展示到屏幕上。
- 自定义的 framebuffer,可以用来将整个场景或整个物体渲染成一张2D贴图,再把它直接传给default framebuffer
PS:从数据结构的角度上看,其实我们所说的FrameBuffer并不是一个真正意义上的buffer,它类似于C语言中结构体,里面存了一些指针,这些指针分别指向输入的depth buffer、color buffer、stencil buffer和输出的贴图对象texture或rbo。
创建Framebuffer
示例代码如下:
//VBO的写法
unsigned int cubeVBO;
glGenBuffers(1, &cubeVBO);
glBindBuffer(GL_ARRAY_BUFFER, cubeVBO);
//FBO的写法与VBO类似
unsigned int fbo;
glGenFramebuffers(1, &fbo);
//Bind到Framebuffer(即fbo)
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
Read Framebuffer和Write Framebuffer
参考:https://www.khronos.org/opengl/wiki/Framebuffer_Object
默认的framebuffer(The default framebuffer)的各种特殊buffer都有各自的名字,比如GL_FRONT
, GL_BACK
, GL_AUXi
, GL_ACCUM
等,之前的glfwSwapBuffers就会把默认framebuffer的GL_FRONT
和GL_BACK
俩buffer交换,而这里自定义的framebuffer则没有这种东西,它应该只有俩buffer,即GL_READ_FRAMEBUFFER
和GL_DRAW_FRAMEBUFFER
,绑定时可以这么写:
// 同时绑定到fbo的Read和Draw Buffer
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
// 只绑定Read
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
// 只绑定Write
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
在实际调用里,reading commands(比如glReadPixels)会读当前绑定的Read fbo,而writing commands (all rendering commands)都会用到当前绑定的Draw buffer
填充Framebuffer数据
在创建完基本的FrameBuffer之后,还要为其填充attachment,可以理解为为其填充数据
一个最基本的Framebuffer需要包含四个内容:
- We have to attach at least one buffer (color, depth or stencil buffer)
- There should be at least one color attachment
- All attachments should be complete as well
- Each buffer should have the same number of samples
关于attachment
为了理解这四个内容,需要先介绍attachment
An attachment is a memory location that can act as a buffer for the framebuffer, think of it as an image. When creating an attachment we have two options to take: textures or renderbuffer objects.
OpenGL定义了两种attachment,texture attachment和render buffer object,二者对应不同的贴图格式。不过老版本的OpenGL只有texture attachment这一种attachment。
把Texture attachment细分后,具体可以分为四种:
texture attachment 与 renderbuffer object attachment的区别
texture attachment就是一张texture图,可以被采样,可以通过shader进行读取,但rbo attachment,不能被采样,没有mip-maps,不可被shader读取。
理解Framebuffer的四个部分
首先,一个Framebuffer,为了进行渲染,需要两项内容:输入数据和输出数据:
- 输入:输入的数据,一个完整的FBO需要有输入的数据(
at least one buffer
) - 输出:输出的贴图,一个完整的FBO需要有输出的数据(
at least one color attachment
),这里的attachment就相当于image,作为输出。
完成一个framebuffer的创建和相关attachment的attach操作后,可以用API检测FrameBuffer是否是完整的:
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)
至于后面两点,就比较好理解了,输出的贴图必须是完整的,而且每一个输入的buffer因为都是渲染一个场景,所以其大小、像素数都必须是一样的。
生成并绑定Texture attachment
生成Texture attachment的方法跟生成texture的方法类似,只是多了个把它attach到Framebuffer的操作。
unsigned int 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);
//attach it to the frame buffer, 作为输出的texture
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
glFrambebufferTexture2D
API:
生成并绑定Renderbuffer object attachment
Renderbuffer object attachment 是一块真正的buffer,里面直接存储了渲染的data,这些data直接采用了OpenGL内置的渲染格式的,对其进行write操作和copy操作都很快。data是可写不可读的,这里的不可读指的是不可直接从attachment中读取数据,但是可以借助glReadPixels从当前绑定的frame buffer中读取特定区域的片元。
rbo相关的API:
unsigned int rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
由于renderbuffer的write-only的特性,rbo常常用作于depth和stencil的Attachment
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600); //24位的depth,8位的stencil
通过设置texture类型的framebuffer,可以把整个场景渲染成一张贴图。
OpenGL默认的四个framebuffer
- FRONT_LEFT 展示给屏幕的fbo
- FRONT_RIGHT
- BACK_LEFT 在后面偷偷绘制的fbo,用于swapbuffer用,是双缓冲机制
- BACK_RIGHT
需要四个fbo,以适用特殊用途,比如VR,Left是左眼,RIght是右眼
具体使用FBO的三个步骤
- 绑定到自己创建的FBO,按照正常操作进行绘制
- 切换到默认的framebuffer
- 画正常场景,把刚刚生成的贴图贴上去
Framebuffer里使用MSAA的贴图
我在用Framebuffer把场景渲染成一张贴图绘制出来的过程中,发现绘制出的图案有锯齿,这里打算提高Framebuffer输出贴图的采样率,即MSAA,参考Anti Aliasing,里面介绍的有两种在OpenGL里使用MSAA的方法:
- 一种是开启窗口的MSAA,即整个窗口在绘制时启用MSAA技术
- 一种是使用MSAA的贴图
A Multisample Texture is a Texture that can have multiple samples per pixel, thereby allowing it to be used in multisampled rendering.
第一种很简单,也是我之前学习OpenGL时使用的方法:
// 在glfwCreateWindow之前调用此函数
glfwWindowHint(GLFW_SAMPLES, 4);
...
// 设置OpenGL的contex
glEnable(GL_MULTISAMPLE);
看上去这么简单,其实是因为GLFW把默认窗口的Framebuffer里相关MSAA的内容都设置好了,而这里的游戏引擎是把场景渲染为贴图输出到viewport子窗口给imgui绘制的,所以不适合第一种方法,对于自己创建的framebuffer而言,MSAA本质上是提高了buffer的大小,4x的MSAA对应了原本4倍的buffer大小,具体分为两类:
- Multisampled texture attachments
- Multisampled renderbuffer objects
MSAA贴图的写法如下,跟创建Texture2D很像:
// GL_TEXTURE_2D和GL_TEXTURE_2D_MULTISAMPLE是两种GLenum
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, tex);
// 用glTexImage2DMultisample取代glTexImage2D函数
glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, GL_RGB, width, height, GL_TRUE);
// Bind完后绑定还原到空槽位
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);
// 把MultiSample的Texture绑定到Framebuffer上
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, tex, 0);
OpenGL定义了两种attachment,texture attachment和render buffer object,二者对应不同的贴图格式。texture attachment就是一张texture图,可以被采样,可以通过shader进行读取,但rbo attachment,不能被采样,没有mip-maps,不可被shader读取,但是可以借助glReadPixels从当前绑定的frame buffer中读取特定区域的片元,由于renderbuffer的write-only的特性,rbo常常用作于depth和stencil的Attachment,其进行write操作和copy操作都很快
MSAA的贴图设置前面介绍过了,MSAA的rbo设置如下所示:
// ===================== 原本的RBO设置 ============================
// 创建用于depth和stencil attachment的rbo(we won't be sampling these)
unsigned int rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
// 设置数据格式
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, SCR_WIDTH, SCR_HEIGHT);
// attach到当前绑定的fbo上
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
// ===================== 现在的RBO设置 ============================
unsigned int rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
// 改下API而已, 其实只改了这一行
glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH24_STENCIL8, SCR_WIDTH, SCR_HEIGHT);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
最后还需要相关framebuffer的设置,即Render to multisampled framebuffer,因为multisampled buffer有点特殊,不能直接用shader直接使用此buffer,此时需要对其输出的贴图进行downsclae操作(也叫resolve),此时需要借助glBlitFramebuffer
,从此MSAA buffer里复制得到一份新的buffer,相当于针对MSAA的贴图,每帧Copy出一份新的普通Image,代码如下:
// 当使用glBindFramebuffer(GL_FRAMEBUFFER, fbo)时, 其实是同时绑定到了GL_READ_FRAMEBUFFER和GL_DRAW_FRAMEBUFFER上
// 此部分代码需要每帧调用
// 把绑定了MSAA的texture attachment和rbo attachment的frame buffer绑定到ReadBuffer上
glBindFramebuffer(GL_READ_FRAMEBUFFER, multisampledFBO);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
// 使用BlitFramebuffer把read buffer里的东西Copy到draw buffer上
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
glBlitFramebuffer, glBlitNamedFramebuffer — copy a block of pixels from one framebuffer object to another
再强调一下这里的glBlitFramebuffer函数,blit可以理解为移动、传送,这里左边是MSAA输出的贴图的四个顶点的像素坐标,右边也是输出贴图的坐标,其他细节如下图所示:
二. 通过post-processing 的操作,实现某些特殊effects
当把整个场景渲染成一张贴图后,可以对贴图进行以下后处理操作
(1)曝光效果: 把贴图颜色进行inverse操作即可
void main()
{
FragColor = vec4(vec3(1.0 - texture(screenTexture, TexCoords)), 1.0);
}
(2)Grayscale: 灰白效果
可达到这种效果
(3)Kenel Effect
Kener Effect是利用卷积矩阵,把点在texture上的颜色与周围点的颜色做了一个采样,也就是说,texture上的每个点,都会收到周围点的颜色影响。
比如这样的矩阵:
float kernel[9] = float[](
-1, -1, -1,
-1, 9, -1,
-1, -1, -1
);
float kernel[9] = float[](
2, 2, 2,
2, -15, 2,
2, 2, 2
);
这种矩阵和都为1,若大于1,则整体偏亮,小于1 整体偏暗
可以模拟出这种喝醉酒的感觉:
(4)模糊效果: 同样是采用与周围片元混合的方式,但混合的矩阵有点不同:
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
);
通过这样的矩阵,可以实现模糊效果
模糊效果大致如下,可以用于摘掉眼镜、喝醉酒等情况
(5)Edge detection(亮化边缘): 同样是用kernel矩阵
我觉得是因为边缘的矩阵周围有0,所以加起来其color的大小会比1大,所以显得更亮
效果图如下:
三. 正方体贴图
正方体贴图由六个面组成,正方体贴图的贴图坐标由三个坐标组成,对于下图所示的正方体,可以发现,正好对于一个点,其坐标点的坐标和其贴图坐标是相同的,这样就不用单独再去给贴图传入贴图UV坐标了。
创建cubemap的相关API如下:
unsigned int textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
//调用6次glTexImage2D,对正方体六个面进行贴图
glTexImage2D(
GL_TEXTURE_CUBE_MAP_POSITIVE_X , //right side of the cube
0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data
);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); //三维的需要多加一个R方向
//shader中的写法
in vec3 textureDir; // direction vector representing a 3D texture coordinate
uniform samplerCube cubemap; // cubemap texture sampler
void main()
{
FragColor = texture(cubemap, textureDir);
}
在绘制Skybox的时候,如果按照绘制普通物体的画法,Skybox其实就是一个正方体而已,如图所示:
为了实现在天空盒里面的视角,在绘制天空盒的时候,需要做以下特殊处理:
- 将绘制天空盒的view矩阵的第四行和第四列元素置为0。
- 在绘制时,为了保证Skybox的深度最大,在片元着色器里,将其pos的z分量置为1
具体操作如下:
glm::mat4 view = glm::mat4(glm::mat3(myCamera.GetViewMatrix()));
void main()
{
TexCoords = aPos;
vec4 pos = projection * view * vec4(aPos, 1.0);
gl_Position = pos.xyww; //MVP转换之后,再把深度值转换为1
}
还有一个具体的小细节,在绘制场景时,需要把默认的深度测试的模式从GL_LESS换为GL_LEQUAL,后者是包含深度相等的清空,也就是如果深度都为1,仍然绘制场景,画完skybox之后,可再把深度测试模式还原。
四. 环境映射(Environment mapping)
立方体贴图不仅仅只用于skybox,还可以用来给物体带来反射和折射特性。
反射特性
反射特性可以如下图所示,反射出环境光
为了突出反射光的效果,直接将反射的光,不进行衰减的,原路返回进行输出(我理解的是把所有反射的光,都照射在垂直角度的skybox上),片元着色器这么写:
#version 330 core
in vec3 Normal;
in vec3 Pos;
uniform samplerCube skybox;
uniform vec3 cameraPos;
out vec4 fragColor;
void main()
{
vec3 dir = normalize(Pos - cameraPos);
vec3 target = reflect(dir,normalize(Normal));
//fragColor = texture(skybox,target);
fragColor = vec4(texture(skybox,target).rgb,1.0);
}
最后得到的效果,用的贴图完全是从skybox上反射回来的:
折射
折射也是类似的,只不过多了个折射系数而已,折射的API是refract()
下面是常用介质的折射系数
片元着色器可以这么写:
void main()
{
float ratio = 1.00 / 1.52;
vec3 I = normalize(Position - cameraPos);
vec3 R = refract(I, normalize(Normal), ratio);
FragColor = vec4(texture(skybox, R).rgb, 1.0);
}
因为进行了光线衰减 ,所有可以实现折射的透明效果: