引用链接Learn OpenGL 作者 Joey de Vries
一. 帧缓冲 Framebuffers
之前学到的ColorBuffer、DepthBuffer和StencilBuffer都属于帧缓冲的一部分
OpenGL允许用户自己定义FrameBuffer,即自行定义ColorBuffer、DepthBuffer和StencilBuffer
前面用的BindBuffer都是绑定的OpenGL的default FrameBuffer
相关操作与普通操作类似:
//VBO的写法
unsigned int cubeVBO;
glGenBuffers(1, &cubeVBO);
glBindBuffer(GL_ARRAY_BUFFER, cubeVBO);
//FBO的写法与VBO类似
unsigned int fbo;
glGenFramebuffers(1, &fbo);
//可对绑定的Framebuffer(即fbo)进行读和写操作,最常用的一种
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
//OpenGL的默认帧缓存的Id为0
//glBindFramebuffer(GL_FRAMEBUFFER. 0);
//可读不可写,比如只可用glReadPixels这种写操作
//glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
//可写不可读
//glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
一个最基本的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
所以怎么理解Framebuffer呢,首先Framebuffer分为两种:
- default framebuffer,用来储存depth buffer、stencil buffer和color buffer,然后展示到屏幕上。
- 自定义的 framebuffer,可以用来将整个场景或整个物体渲染成一张2D贴图,再把它直接传给default framebuffer
如果完成一个framebuffer的创建和相关attachment的attach操作后,可以用API检测FrameBuffer是否是完整的:
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)
看完了上面,我仍然不理解什么样的FrameBuffer才算是complete的framebuffer?
什么是FrameBuffer
其实我们所说的FrameBuffer并不是一个真正意义上的buffer,它类似于C语言中结构体,里面存了一些指针,这些指针分别指向输入的depth buffer、color buffer、stencil buffer和输出的贴图对象texture或rbo。
FrameBuffer的组成
前面讲到,自定义的FrameBuffer可以实现离屏渲染,也就是说用FrameBuffer可以将场景渲染成一张贴图,那么这个过程,需要什么数据?
将场景渲染成一张贴图,至少需要两项内容:
- 输入:输入的数据,一个完整的FBO需要有输入的数据(
at least one buffer
) - 输出:输出的贴图,一个完整的FBO需要有输出的数据(
at least one color attachment
),这里的attachment就相当于贴图。
OpenGL定义了两种attachment,texture attachment和render buffer object,二者对应不同的贴图格式。
至于后面两点,就比较好理解了,输出的贴图必须是完整的,而且每一个输入的buffer因为都是渲染一个场景,所以其大小、像素数都必须是一样的。
Attachment
An attachment is a memory location that can act as a buffer for the framebuffer, think of it as an image.
attachment是一块内存,可以充当framebuffer的缓存buffer,可以把attachment看作一张图像。
attachments有两种
- Texture attachment(老版本的OpenGL只有texture attachment)
- Renderbuffer object attachments
把Texture attachment细分后,具体可以分为四种:
texture attachment 与 renderbuffer object attachment的区别
texture attachment就是一张texture图,可以被采样,可以通过shader进行读取,但rbo attachment,不能被采样,没有mip-maps,不可被shader读取。
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
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
glFrambebufferTexture2D
API:
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
- 画正常场景,把刚刚生成的贴图贴上去
二. 通过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大,所以显得更亮
效果图如下:
3. 正方体贴图
正方体贴图由六个面组成,正方体贴图的贴图坐标由三个坐标组成,对于下图所示的正方体,可以发现,正好对于一个点,其坐标点的坐标和其贴图坐标是相同的,这样就不用单独再去给贴图传入贴图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);
}
因为进行了光线衰减 ,所有可以实现折射的透明效果: