目录
- 混合
- 丢弃片段
- 渲染半透明纹理
- 面剔除
- 环绕顺序
- 帧缓冲
- 纹理附件
- 渲染缓冲对象附件
- 渲染到纹理
- 后期处理
- 核效果
GitHub主页:https://github.com/sdpyy1
OpenGL学习仓库:https://github.com/sdpyy1/CppLearn/tree/main/OpenGLtree/main/OpenGL):https://github.com/sdpyy1/CppLearn/tree/main/OpenGL
混合
OpenGL中,混合(Blending)通常是实现物体透明度(Transparency)的一种技术
通过控制颜色的第四个分量(透明度来实现)
丢弃片段
有些图片并不需要半透明,只需要根据纹理颜色值,显示一部分,或者不显示一部分,没有中间情况。
比如有些情况可以直接把草的纹理贴在四边形上,来冒充草。例如下图,只要不是草的部分,就应该是背景颜色(透明度为0),是草的部分就应该全是草(透明度为1)
所以当添加像草这样的植被到场景中时,我们不希望看到草的方形图像,而是只显示草的部分,并能看透图像其余的部分。我们想要**丢弃(Discard)**显示纹理中透明部分的片段,不将这些片段存储到颜色缓冲中。
首先将这个草当作纹理,导入到一个四边形中(注意这里的纹理坐标已经做了y轴反转)
// 临时设置一个草float transparentVertices[] = {// positions // texture Coords (swapped y coordinates because texture is flipped upside down)0.0f, 0.5f, 0.0f, 0.0f, 0.0f,0.0f, -0.5f, 0.0f, 0.0f, 1.0f,1.0f, -0.5f, 0.0f, 1.0f, 1.0f,0.0f, 0.5f, 0.0f, 0.0f, 0.0f,1.0f, -0.5f, 0.0f, 1.0f, 1.0f,1.0f, 0.5f, 0.0f, 1.0f, 0.0f};GLuint grassVBO, grassVAO;glGenBuffers(1, &grassVBO);glGenVertexArrays(1, &grassVAO);glBindVertexArray(grassVAO);glBindBuffer(GL_ARRAY_BUFFER, grassVBO);glBufferData(GL_ARRAY_BUFFER, sizeof(transparentVertices), &transparentVertices, GL_STATIC_DRAW);glEnableVertexAttribArray(0);glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);glEnableVertexAttribArray(2);glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));glBindVertexArray(0);GLuint grassTexture = loadTexture("./assets/grass.png");
// 绘制草glActiveTexture(GL_TEXTURE0);bagShader.setInt("texture_diffuse1",0);glBindTexture(GL_TEXTURE_2D, grassTexture);glBindVertexArray(grassVAO);glm::mat4 modelTrans = glm::mat4(1.0f);modelTrans = glm::translate(modelTrans, {0,0,0}); modelTrans = glm::scale(modelTrans, {1,1,1}); bagShader.setMat4("model", modelTrans);glDrawArrays(GL_TRIANGLES, 0, 6);
这里直接就是一个四边形,出现这种情况是因为OpenGL默认是不知道怎么处理alpha值的,更不知道什么时候应该丢弃片段。GLSL给了我们discard命令,一旦被调用,它就会保证片段不会被进一步处理,所以就不会进入颜色缓冲。
解决办法就是如果片段着色器设置颜色的透明度小于0.1,就进行丢弃
void main()
{ vec4 texColor = texture(texture1, TexCoords);if(texColor.a < 0.1)discard;FragColor = texColor;
}
由于黑色区域的透明度小于0.1,所以就没了
下来回到混合。虽然直接丢弃片段很好,但它不能让我们渲染半透明的图像。我们要么渲染一个片段,要么完全丢弃它。要想渲染有多个透明度级别的图像,我们需要启用混合(Blending)。
同样,混合功能也需要开启
glEnable(GL_BLEND);
启用了混合之后,我们需要告诉OpenGL它该如何混合。
OpenGL中的混合是通过下面这个方程来实现的:
也就是说原本该位置已经渲染了一种颜色,有它的透明度,现在新加入了一种颜色,就需要混合。
片段着色器运行完成,并且所有的测试都通过之后,这个混合方程(Blend Equation)才会应用到片段颜色输出与当前颜色缓冲中的值。颜色值已经有了,但是两个F我们可以手动设置。
OpenGL提供了设置F的函数glBlendFunc(GLenum sfactor, GLenum dfactor)
,其中的C¯constant
可以通过其他函数来设置
渲染半透明纹理
// 启动混合glEnable(GL_BLEND);// 设置FglBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
删除之前的discard
#version 330 core
out vec4 FragColor;in vec2 TexCoords;uniform sampler2D texture_diffuse1;void main()
{FragColor = texture(texture_diffuse1, TexCoords);
}
但是两块玻璃效果就会出问题
这是因为深度测试不会检查透明度,我是先渲染的前边的窗户,深度缓存已经存储了z最小的位置,之后渲染后边的窗户就不会再渲染了。所以对于透明物体渲染顺序,我们必须手动设置按照从远到近进行渲染。
调整渲染顺序后
但是只要把摄像机转到窗户背后就会出问题,因为在这个角度下渲染顺序又错了
所以要在渲染前真的进行排序才行,渲染顺序应该是
- 渲染不透明物体
- 排序透明物体
- 按顺序渲染透明物体
实时排序是个复杂的问题,它并没有考虑旋转、缩放或者其它的变换,奇怪形状的物体需要一个不同的计量,而不是仅仅一个位置向量。
那下图这种角度摄像机左右横移根本不需要排序,现在理解游戏优化差是什么意思了hhh
更高级的技术还有次序无关透明度(Order Independent Transparency, OIT)
面剔除
看不见的面没必要渲染
OpenGL能够检查所有面向(Front Facing)观察者的面,并渲染它们,而丢弃那些背向(Back Facing)的面,节省我们很多的片段着色器调用(它们的开销很大!)
但我们仍要告诉OpenGL哪些面是正向面(Front Face),哪些面是背向面(Back Face)。OpenGL使用了一个很聪明的技巧,分析顶点数据的环绕顺序(Winding Order)。
环绕顺序
这种方法在手写渲染器的时候用过,默认逆时针被认为正三角形
当你定义顶点顺序的时候,你应该想象对应的三角形是面向你的,所以你定义的三角形从正面看去应该是逆时针的。这样定义顶点很棒的一点是,实际的环绕顺序是在光栅化阶段进行的,也就是顶点着色器运行之后。这些顶点就是从观察者视角所见的了。
因为定义三角形都是按逆时针顺序的,背面渲染的三角形渲染顺序会变为逆时针,这样就能区分正面背面了。
在OpenGL中需要手动开启
glEnable(GL_CULL_FACE);
可以飞进模型内部查看后面是否被剔除,这里用上节课的玻璃,做一个正方体,然后关闭混合来进行测试
未剔除
开启面剔除后
OpenGL允许我们改变需要剔除的面的类型。如果我们只想剔除正向面而不是背向面会怎么样?我们可以调用
glCullFace来定义这一行为:
glCullFace(GL_FRONT);
下图就是展示只渲染看不见的部分
还有一个函数来定义什么方向才是正面
glFrontFace(GL_CW); // 顺时针是正面
glFrontFace(GL_CCW); // 逆时针是正面
帧缓冲
我们目前做的操作都是在默认帧缓冲上进行的,这个默认帧缓冲是由GLFW配置和管理的,它默认就有颜色、深度、模板缓冲。他的id是默认的0.
我们也可以创建一个自定义的帧缓冲对象
unsigned int fbo;
glGenFramebuffers(1, &fbo);
绑定它
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
我们现在还不能使用我们的帧缓冲,因为它还不完整(Complete),一个完整的帧缓冲需要满足以下的条件:
- 附加至少一个缓冲(颜色、深度或模板缓冲)。
- 至少有一个颜色附件(Attachment)。
- 所有的附件都必须是完整的(保留了内存)。
- 每个缓冲都应该有相同的样本数(sample)。
配置好帧缓冲后可以检查是否完整
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)
之后的渲染操作会写入到该自定义帧缓冲中,但他不会直接渲染到屏幕上,渲染到一个不同的帧缓冲被叫做离屏渲染(Off-screen Rendering)。要保证所有的渲染操作在主窗口中有视觉效果,我们需要再次激活默认帧缓冲,将它绑定到0。
glBindFramebuffer(GL_FRAMEBUFFER, 0);
完成操作后记得要删除这个自定义帧缓冲对象
glDeleteFramebuffers(1, &fbo);
纹理附件
说实话我不太理解为什么重新绑定到0号帧缓冲就可以写入刚才的内容,问了问GPT得到解答。离屏渲染的核心思想是:先把渲染结果存储到一个纹理中,然后通过纹理坐标映射到屏幕上。
- 渲染到离屏纹理:首先,场景渲染到一个纹理(通常是帧缓冲对象 FBO 附加的颜色纹理)。每个像素点的纹理坐标对应屏幕上的每个像素位置。
- 绑定纹理:接着,将渲染结果(纹理)绑定到着色器中,作为输入传递给片段着色器。
- 渲染全屏四边形:通过渲染一个屏幕大小的全屏四边形,在片段着色器中将纹理的颜色值根据纹理坐标映射到屏幕的每个像素。
- 0号帧缓冲(默认帧缓冲)就是OpenGL默认的渲染目标,它指的是显示设备(也就是你计算机的屏幕)。只有在渲染到这个帧缓冲时,最终的图像才会显示在屏幕上。
总体来说,使用纹理来传递颜色数据减少了数据的转移。
下面为帧缓冲创建一个纹理
// 创建一个纹理附件GLuint texture;glGenTextures(1, &texture);glBindTexture(GL_TEXTURE_2D, texture);// 创建一个空的纹理 传入的图片是NULLglTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, SCR_WIDTH, SCR_HEIGHT, 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);
除此之外也可以添加模板和深度传冲附件,纹理的每个位置可以存放32位信息,具体来说,这个格式包含 4 个通道(Red, Green, Blue, Alpha),每个通道 8 位(8 + 8 + 8 + 8 = 32),可以把模板和深度写在同一张纹理中,纹理的每32位数值将包含24位的深度信息和8位的模板信息。
渲染缓冲对象附件
渲染缓冲对象(Renderbuffer Object)是在纹理之后引入到OpenGL中,作为一个可用的帧缓冲附件类型的,所以在过去纹理是唯一可用的附件。和纹理图像一样,渲染缓冲对象是一个真正的缓冲,即一系列的字节、整数、像素等。渲染缓冲对象附加的好处是,它会将数据储存为OpenGL原生的渲染格式,它是为离屏渲染到帧缓冲优化过的。
他是不可读的(对于程序来说),相比通过纹理方式实现的缓冲,如果只是存储深度和模板信息而不需要读取,可以选择 渲染缓冲对象,如果需要在后续的渲染阶段读取数据(比如后处理),则使用 纹理附件 更为合适。
当我们不需要从这些缓冲中采样的时候,通常都会选择渲染缓冲对象,因为它会更优化一点。
创建一个帧缓冲对象
// 渲染缓冲对象RBOGLuint RBO;glGenRenderbuffers(1, &RBO);glBindRenderbuffer(GL_RENDERBUFFER, RBO);
在当前RBO中创建24位的深度和8位的模板缓冲。
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);
渲染到纹理
步骤:
- 绑定帧缓冲对象FBO
- 正常进行渲染
- 回到0号帧缓冲
- 绘制一个屏幕大小的正方形,并把UV坐标写好,之后绘制时选择FBO的附件纹理
创建正方形的VBO,VAO
// 用于离线渲染的正方形float quadVertices[] = { // vertex attributes for a quad that fills the entire screen in Normalized Device Coordinates.// positions // texCoords-1.0f, 1.0f, 0.0f,0.0f, 1.0f,-1.0f, -1.0f, 0.0f,0.0f, 0.0f,1.0f, -1.0f, 0.0f,1.0f, 0.0f,-1.0f, 1.0f, 0.0f,0.0f, 1.0f,1.0f, -1.0f, 0.0f,1.0f, 0.0f,1.0f, 1.0f, 0.0f,1.0f, 1.0f};unsigned int quadVAO, quadVBO;glGenVertexArrays(1, &quadVAO);glGenBuffers(1, &quadVBO);glBindVertexArray(quadVAO);glBindBuffer(GL_ARRAY_BUFFER, quadVBO);glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), &quadVertices, GL_STATIC_DRAW);glEnableVertexAttribArray(0);glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);glEnableVertexAttribArray(2);glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));glBindVertexArray(0);
渲染时用下面代码
while (!glfwWindowShouldClose(window)){// 清理窗口glClearColor(0.05f, 0.05f, 0.05f, 1.0f);// 离线渲染glBindFramebuffer(GL_FRAMEBUFFER, FBO);// 这里开始都是在FBO上渲染glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);// 绘制物体drawModel(bagShader, model,{1.f,0.f,-3.f},{1.0f,1.0f,1.0f});// 绘制草glActiveTexture(GL_TEXTURE0);bagShader.setInt("texture_diffuse1",0);glBindTexture(GL_TEXTURE_2D, grassTexture);glBindVertexArray(grassVAO);glm::mat4 modelTrans = glm::mat4(1.0f);modelTrans = glm::translate(modelTrans, {0,0.5,-1});modelTrans = glm::scale(modelTrans, {1,1,1});bagShader.setMat4("model", modelTrans);glDrawArrays(GL_TRIANGLES, 0, 36);// 回到0号帧缓冲glBindFramebuffer(GL_FRAMEBUFFER, 0);glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);glActiveTexture(GL_TEXTURE0);glBindVertexArray(quadVAO);bagShader.setInt("texture_diffuse1",0);glBindTexture(GL_TEXTURE_2D, texture);bagShader.use();bagShader.setMat4("model", glm::mat4(1.0f));bagShader.setMat4("view", glm::mat4(1.0f));bagShader.setMat4("projection", glm::mat4(1.0f));glDrawArrays(GL_TRIANGLES, 0, 6);// 事件处理glfwPollEvents();// 双缓冲glfwSwapBuffers(window);processFrameTimeForMove();processInput(window);}
可以看到渲染正常执行了,但从代码逻辑,0号帧缓冲只是渲染了一个正方形
后期处理
到目前位置,好像也没体现出自定义帧缓冲有什么用,反而让渲染变复杂了
既然整个场景都被渲染到了一个纹理上,我们可以简单地通过修改纹理数据创建出一些非常有意思的效果。在这一部分中,我们将会向你展示一些流行的后期处理效果,并告诉你改如何使用创造力创建你自己的效果。
反向
void main()
{FragColor = vec4(vec3(1.0 - texture(screenTexture, TexCoords)), 1.0);
}
灰度
void main()
{FragColor = texture(screenTexture, TexCoords);float average = (FragColor.r + FragColor.g + FragColor.b) / 3.0;FragColor = vec4(average, average, average, 1.0);
}
核效果
在一个纹理图像上做后期处理的另外一个好处是,我们可以从纹理的其它地方采样颜色值。,也就是说当渲染完一帧图片后,图片的一个像素可以拿到其他位置像素来进行特殊处理,直接渲染这种效果是不好实现的。
模糊
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.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);vec3 sampleTex[9];for(int i = 0; i < 9; i++){sampleTex[i] = vec3(texture(texture_diffuse1, 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);
}
其实这些处理可以理解为,已经获得一张图片了,做的处理就属于图片处理了。