首页 > 技术文章 > OpenGL笔记(六)

hwx0000 2019-10-24 19:11 原文

引用链接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);
}  

因为进行了光线衰减 ,所有可以实现折射的透明效果:
在这里插入图片描述

推荐阅读