完整代码见:zaizai77/Cherno-OpenGL: OpenGL 小白学习之路
帧缓冲
帧缓冲(FrameBuffer)是所有屏幕缓冲(包括颜色缓冲,深度缓冲,模板缓冲)的集合。它被存储在GPU内存中,我们可以定义自己的帧缓冲
我们目前所做的所有操作都是在默认帧缓冲 的渲染缓冲上进行的。默认的帧缓冲是在你创建窗口的时候生成和配置的(GLFW帮我们做了这些)。通过创建我们自己的帧缓冲,我们可以获得额外的渲染目标(target)。
创建一个帧缓冲
cpp
unsigned int fbo;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
创建一个帧缓冲对象并绑定
在绑定到GL_FRAMEBUFFER目标之后,所有的读取 和写入 帧缓冲的操作将会影响当前绑定的帧缓冲。我们也可以使用GL_READ_FRAMEBUFFER 或GL_DRAW_FRAMEBUFFER,将一个帧缓冲分别绑定到读取目标或写入目标。
一个完整的帧缓冲需要满足以下的条件:
- 附加至少一个缓冲(颜色、深度或模板缓冲)。
- 至少有一个颜色附件(Attachment)。
- 所有的附件都必须是完整的(保留了内存)。
- 每个缓冲都应该有相同的样本数(sample)。
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) // 执行胜利的舞蹈
//检查缓冲是否完整
之后所有的渲染操作将会渲染到当前绑定帧缓冲的附件中。
由于我们的帧缓冲不是默认帧缓冲,渲染指令将不会对窗口的视觉输出有任何影响。出于这个原因,渲染到一个不同的帧缓冲被叫做**离屏渲染(Off-screen Rendering)。**要保证所有的渲染操作在主窗口中有视觉效果,我们需要再次激活默认帧缓冲,将它绑定到 0
glBindFramebuffer(GL_FRAMEBUFFER, 0);
//完成渲染之后删除帧缓冲对象
glDeleteFramebuffers(1, &fbo);
附件是一个内存位置, 它能够作为帧缓冲的一个缓冲,可以将它想象为一个图像。当创建一个附件的时候我们有两个选项:纹理 或渲染缓冲对象(Renderbuffer Object)。
纹理附件
当把一个纹理附加到帧缓冲的时候,所有的渲染指令将会写入到这个纹理中,就像它是一个普通的颜色/深度或模板缓冲一样。使用纹理的优点是,所有渲染操作的结果将会被储存在一个纹理图像中,我们之后可以在着色器中很方便地使用它。
为帧缓冲创建一个纹理和创建一个普通的纹理差不多:
cpp
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
//无图片数据,data参数为NULL
//若附加深度缓冲纹理,Format和Internalformat参数应当为GL_DEPTH_COMPONENT
//若附加模板缓冲纹理,则为GL_STENCIL_INDEX
//若同时附加深度和模板缓冲纹理,Format为GL_DEPTH24_STENCIL8,Internalformat为GL_DEPTH_STENCIL,Type为GL_UNSIGNED_INT_24_8
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);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
glFrameBufferTexture2D有以下的参数:
- target :帧缓冲的目标(绘制、读取或者两者皆有)
- attachment :我们想要附加的附件类型。除了颜色缓冲外,还有GL_DEPTH_ATTACHMENT,GL_STENCIL_ATTACHMENT,GL_DEPTH_STENCIL_ATTACHMENT
- textarget: 你希望附加的纹理类型
- texture: 要附加的纹理本身
- level: 多级渐远纹理的级别。我们将它保留为0。
渲染缓冲对象附件
渲染缓冲对象(Renderbuffer Object,RBO)是真正的缓冲(相对于纹理等通用数据缓冲, General Purpose Data Buffer),它相比于纹理缓冲具有更快的读取速度。
渲染缓冲对象是只写的,可以通过 glReadPixels 函数来读取当前绑定的帧缓冲中的特定像素
cpp
unsigned int rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
当我们不需要从这些缓冲中采样的时候,通常选择渲染缓冲对象,因为它更优化一些创建一个深度和模板渲染缓冲对象可以通过调用glRenderbufferStorage函数来完成:
//创建一个深度和模板渲染缓冲对象可以通过调用glRenderbufferStorage函数来完成:
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);
//附加这个渲染缓冲对象:
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
创建一个渲染缓冲对象和纹理对象类似,不同的是这个对象是专门被设计作为帧缓冲附件使用的,而不是纹理那样的通用数据缓冲(General Purpose Data Buffer)。这里我们选择GL_DEPTH24_STENCIL8 作为内部格式,它封装了24位的深度和8位的模板缓冲。
渲染到纹理
我们将会将场景渲染到一个附加到帧缓冲对象上的颜色纹理中,之后将在一个横跨整个屏幕的四边形上绘制这个纹理。这样视觉输出和没使用帧缓冲时是完全一样的,但这次是打印到了一个四边形上。
cpp
//创建一个帧缓冲对象并绑定
unsigned int framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
//创建一个纹理图像,我们将它作为一个颜色附件附加到帧缓冲上
//我们将纹理的维度设置为窗口的宽度和高度,并且不初始化它的数据:
// 生成纹理
unsigned int 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);
// 将它附加到当前绑定的帧缓冲对象
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texColorBuffer, 0);
//添加一个深度(和模板)附件到帧缓冲中
unsigned int rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
//当我们为渲染缓冲对象分配了足够的内存之后,我们可以解绑这个渲染缓冲。
//接下来,作为完成帧缓冲之前的最后一步,我们将渲染缓冲对象附加到帧缓冲的深度和模板附件上:
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
//我们希望检查帧缓冲是否是完整的,如果不是,我们将打印错误信息。
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
std::cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << std::endl;
glBindFramebuffer(GL_FRAMEBUFFER, 0);
要想绘制场景到一个纹理上,我们需要采取以下的步骤:
- 将新的帧缓冲绑定为激活的帧缓冲,和往常一样渲染场景
- 绑定默认的帧缓冲
- 绘制一个横跨整个屏幕的四边形,将帧缓冲的颜色缓冲作为它的纹理。
帧缓冲的一个渲染迭代将会有以下的结构:
cpp
// 第一处理阶段(Pass)
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 我们现在不使用模板缓冲
glEnable(GL_DEPTH_TEST);
DrawScene();
// 第二处理阶段
glBindFramebuffer(GL_FRAMEBUFFER, 0); // 返回默认
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);
后期处理
反相:
cpp
void main()
{
FragColor = vec4(vec3(1.0 - texture(screenTexture, TexCoords)), 1.0);
}
灰度:
移除场景中除了黑白灰以外所有的颜色,让整个图像灰度化(Grayscale)。很简单的实现方式是,取所有的颜色分量,将它们平均化:
cpp
void main()
{
FragColor = texture(screenTexture, TexCoords);
float average = (FragColor.r + FragColor.g + FragColor.b) / 3.0;
FragColor = vec4(average, average, average, 1.0);
}
核处理:
核(Kernel)( 或卷积矩阵(Convolution Matrix))是一个类矩阵的数值数组,它的中心为当前的像素,它会用它的核值乘以周围的像素值,并将结果相加变成一个值。所以,基本上我们是在对当前像素周围的纹理坐标添加一个小的偏移量,并根据核将结果合并。下面是核的一个例子:
这个核取了8个周围像素值,将它们乘以2,而把当前的像素乘以-15。这个核的例子将周围的像素乘上了一个权重,并将当前像素乘以一个比较大的负权重来平衡结果。
在网上找到的大部分核将所有的权重加起来之后都应该会等于1,如果它们加起来不等于1,这就意味着最终的纹理颜色将会比原纹理值更亮或者更暗了。
使用卷积核对图像进行卷积:
cpp
const float offset = 1.0 / 300.0; //常量,可自行配置
void main()
{
vec2 offsets[9] = vec2[](
vec2(-offset, offset), // 左上
vec2( 0.0f, offset), // 正上
vec2( offset, offset), // 右上
vec2(-offset, 0.0f), // 左
vec2( 0.0f, 0.0f), // 中
vec2( offset, 0.0f), // 右
vec2(-offset, -offset), // 左下
vec2( 0.0f, -offset), // 正下
vec2( offset, -offset) // 右下
);
float kernel[9] = float[](
-1, -1, -1,
-1, 15, -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 = vec3(0.0);
for(int i = 0; i < 9; i++)
col += sampleTex[i] * kernel[i];
FragColor = vec4(col, 1.0);
}
卷积核不同,处理的效果也不同。(行主序)
- 锐化:2 2 2 2 -15 2 2 2 2
- 模糊:(1 2 1 2 4 2 1 2 1)/16
- 边缘检测:1 1 1 1 -8 1 1 1 1
立方体贴图
在本节中,我们将讨论的是将多个纹理组合起来映射到一张纹理上的一种纹理类型:立方体贴图(Cube Map)。
立方体贴图可以**通过一个方向向量来进行索引/采样。**假设我们有一个1x1x1的单位立方体,方向向量的原点位于它的中心。使用一个橘黄色的方向向量来从立方体贴图上采样一个纹理值会像是这样:
创建立方体贴图
cpp
unsigned int textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID); // CUBE_MAP
立方体贴图有六个纹理,每一个面都需要调用一次glTexImage2D ,这里将纹理目标参数设置为立方体贴图的一个特定的面:
和OpenGL的很多枚举(Enum)一样,它们背后的int值是线性递增 的,所以如果我们有一个纹理位置的数组或者vector,我们就可以从GL_TEXTURE_CUBE_MAP_POSITIVE_X开始遍历它们,在每个迭代中对枚举值加1,遍历了整个纹理目标:
cpp
int width, height, nrChannels;
unsigned char *data;
for(unsigned int i = 0; i < textures_faces.size(); i++)
{
data = stbi_load(textures_faces[i].c_str(), &width, &height, &nrChannels, 0);
glTexImage2D(
GL_TEXTURE_CUBE_MAP_POSITIVE_X + i,
0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data
);
}
这里我们有一个叫做textures_faces的vector,它包含了立方体贴图所需的所有纹理路径,并以表中的顺序排列。这将为当前绑定的立方体贴图中的每个面生成一个纹理。
因为立方体贴图和其它纹理没什么不同,我们也需要设定它的环绕和过滤方式:
cpp
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);
与2D纹理不同的是,Cube Map在环绕方式上除了S、T还有R维度。它类似于三维空间中的Z轴,当方向矢量未击中任何面(如接缝处)时,返回边界值。
使用不同类型的采样器,samplerCube,使用一个 vec3 方向向量而不是 vec2
cpp
in vec3 textureDir; // 代表3D纹理坐标的方向向量
uniform samplerCube cubemap; // 立方体贴图的纹理采样器
void main()
{
FragColor = texture(cubemap, textureDir);
}
天空盒
加载天空盒
为了加载天空盒,我们将使用下面的函数,它接受一个包含6个纹理路径的vector:
cpp
unsigned int loadCubemap(vector<std::string> faces)
{
unsigned int textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
int width, height, nrChannels;
for (unsigned int i = 0; i < faces.size(); i++)
{
unsigned char *data = stbi_load(faces[i].c_str(), &width, &height, &nrChannels, 0);
if (data)
{
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i,
0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data
);
stbi_image_free(data);
}
else
{
std::cout << "Cubemap texture failed to load at path: " << faces[i] << std::endl;
stbi_image_free(data);
}
}
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_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);
return textureID;
}
cpp
vector<std::string> faces
{
"right.jpg",
"left.jpg",
"top.jpg",
"bottom.jpg",
"front.jpg",
"back.jpg"
};
unsigned int cubemapTexture = loadCubemap(faces);
显示天空盒
用于贴图3D立方体的立方体贴图可以使用立方体的位置作为纹理坐标来采样。当立方体处于原点(0, 0, 0)时,它的每一个位置向量都是从原点出发的方向向量。这个方向向量正是获取立方体上特定位置的纹理值所需要的。正是因为这个,我们只需要提供位置向量而不用纹理坐标了。
cpp
//顶点着色器
#version 330 core
layout (location = 0) in vec3 aPos;
out vec3 TexCoords;
uniform mat4 projection;
uniform mat4 view;
void main()
{
TexCoords = aPos;
gl_Position = projection * view * vec4(aPos, 1.0);
}
//片段着色器
#version 330 core
out vec4 FragColor;
in vec3 TexCoords;
uniform samplerCube skybox;
void main()
{
//片段着色器非常直观。我们将顶点属性的位置向量作为纹理的方向向量,
//并使用它从立方体贴图中采样纹理值。
FragColor = texture(skybox, TexCoords);
}
注意采样 samplerCube
渲染天空盒:绑定立方体贴图纹理,skybox采样器就会自动填充上天空盒立方体贴图了。绘制天空盒时,我们需要将它变为场景中的第一个 渲染的物体,并且**禁用深度写入。**这样子天空盒就会永远被绘制在其它物体的背后了。
cpp
glDepthMask(GL_FALSE);
skyboxShader.use();
// ... 设置观察和投影矩阵
glBindVertexArray(skyboxVAO);
glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture);
glDrawArrays(GL_TRIANGLES, 0, 36);
glDepthMask(GL_TRUE);
// ... 绘制剩下的场景
cpp
//消除天空盒的位移
glm::mat4 view = glm::mat4(glm::mat3(camera.GetViewMatrix()));
优化
首先渲染天空盒然后渲染其他物体会造成性能的浪费,先渲染天空盒会对屏幕上每一个像素运行一遍片段着色器,即使很多像素最后是看不到的。
可以通过提前深度测试(Early Depth Testing)丢弃不需要的片段
我们将天空盒顶点着色器的 gl_Position 设置为 xyww ,使得它的深度值始终为一(透视除法在顶点着色器之后进行,用 xyz 分量除以 w 分量,w/w 即为1,最大深度)可以让所有物体在天空盒的前面
cpp
void main()
{
TexCoords = aPos;
vec4 pos = projection * view * vec4(aPos, 1.0);
gl_Position = pos.xyww;
}
颠倒了
-----shader 代码是真难写,一不小心就写错了,还没有准错的报错信息。。。。
环境映射
我们可以给物体反射和折射的属性。这样使用环境立方体贴图的技术叫做环境映射(Environment Mapping) ,其中最流行的两个是反射(Reflection) 和折射(Refraction)。
反射
cpp
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
out vec3 Normal;
out vec3 Position;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
Normal = mat3(transpose(inverse(model))) * aNormal;
Position = vec3(model * vec4(aPos, 1.0));
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
#version 330 core
out vec4 FragColor;
in vec3 Normal;
in vec3 Position;
uniform vec3 cameraPos;
uniform samplerCube skybox;
void main()
{
vec3 I = normalize(Position - cameraPos);
vec3 R = reflect(I, normalize(Normal));
FragColor = vec4(texture(skybox, R).rgb, 1.0);
}