练习一、实例化渲染
如果要绘制多个类似物体时,可能最先想到是循环绘制,但是用实例化glDraw*Instanced,性能要好特别多!我们先直接看效果图,如下所示
这里加载了3W个陨石和1个行星,都是用的外部模型obj文件加载渲染的,实现原理可参考【实践LearnOpenGL项目】提升篇。加载好这俩模型数据后,使用随机数定义3W个移动缩放旋转的四维矩阵,让3W个共用一套模型数据的陨石看起来自然一些。
我们重点看下实例化流程,部分代码如下
// 遍历陨石面去设置变换矩阵
for (unsigned int i = 0; i < rock.m_meshes.size(); i++)
{
unsigned int VAO = rock.m_meshes[i].VAO;
glBindVertexArray(VAO);
glEnableVertexAttribArray(3);
glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (void*)0);
glEnableVertexAttribArray(4);
glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (void*)(sizeof(glm::vec4)));
glEnableVertexAttribArray(5);
glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (void*)(2 * sizeof(glm::vec4)));
glEnableVertexAttribArray(6);
glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (void*)(3 * sizeof(glm::vec4)));
glVertexAttribDivisor(3, 1);
glVertexAttribDivisor(4, 1);
glVertexAttribDivisor(5, 1);
glVertexAttribDivisor(6, 1);
glBindVertexArray(0);// 解绑
}
由于OpenGL顶点属性通道最大支持的单次传递是vec4类型数据,这里将四维矩阵拆分成了四个vec4。别忘了在顶点着色器加上这里传入的矩阵,这里对应的“layout (location = 3)” 。由于要使用glDrawElementsInstanced,而不是封装接口内的glDrawElements,因此需要自己实现以下部分。
asteroidShader.use();
asteroidShader.setInt("texture_diffuse1", 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, rock.m_textures_loaded[0].id);
for (unsigned int i = 0; i < rock.m_meshes.size(); i++)
{
glBindVertexArray(rock.m_meshes[i].VAO);
glDrawElementsInstanced(GL_TRIANGLES, static_cast<unsigned int>(rock.m_meshes[i].indices.size()), GL_UNSIGNED_INT, 0, amount);
glBindVertexArray(0);
}
练习二、离屏抗锯齿
其实之前在文章【实践LearnOpenGL项目】光照基础篇中,就已经出现了视觉锯齿,也就是边缘不光滑的情况,如下所示。
为了解决这个问题,我们需要用到离屏抗锯齿技术。原理大概可以理解为,先创建一个多重采样帧缓冲区,用来多次采样,相当于打草稿。然后创建后处理帧缓冲区,用来存储解析后的抗锯齿图像,相当于将草稿整合并拓印出来,最后是以纹理的形式进行屏幕输出,部分代码如下
// 多重采样帧缓冲区配置
unsigned int framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
// 创建支持4倍采样的颜色附件纹理
unsigned int textureColorBufferMultiSampled;
glGenTextures(1, &textureColorBufferMultiSampled);
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, textureColorBufferMultiSampled);
glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, 4, GL_RGB, SCR_WIDTH, SCR_HEIGHT, GL_TRUE);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, textureColorBufferMultiSampled, 0);
// 创建多重采样的深度/模板渲染缓冲对象
unsigned int rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH24_STENCIL8, SCR_WIDTH, SCR_HEIGHT);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
以上的4倍采样相当于画4次,然后取平均。然后我们再看后处理帧缓冲
// 后处理帧缓冲区配置
unsigned int intermediateFBO;
glGenFramebuffers(1, &intermediateFBO);
glBindFramebuffer(GL_FRAMEBUFFER, intermediateFBO);
// 创建普通2D纹理作为颜色附件,用来存储解析后的抗锯齿图像
unsigned int screenTexture;
glGenTextures(1, &screenTexture);
glBindTexture(GL_TEXTURE_2D, screenTexture);
glTexImage2D(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, screenTexture, 0);
这里最终结果存储在了2D纹理screenTexture中,配置好这俩帧缓冲后,接下来我们看渲染的流程
// 1. 初始化多重采样
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);
// 2. 多重采样缓冲区解析到普通帧缓冲
glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, intermediateFBO);
glBlitFramebuffer(0, 0, SCR_WIDTH, SCR_HEIGHT, 0, 0, SCR_WIDTH, SCR_HEIGHT, GL_COLOR_BUFFER_BIT, GL_NEAREST);
// 3. 将其作为纹理进行屏幕输出
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glDisable(GL_DEPTH_TEST);
加载纹理screenTexture后可得到如下效果
练习三、统一缓冲对象(UBO)
当视图、投影矩阵、光源参数等多个数据需要多个着色器中共享时,我们可以使用UBO,可以提升渲染性能。原项目只共享了投影和视图矩阵,部分代码如下
// 获取着色器中“Matrices”的索引
unsigned int uniformBlockIndexRed = glGetUniformBlockIndex(shaderRed.ID, "Matrices");
unsigned int uniformBlockIndexGreen = glGetUniformBlockIndex(shaderGreen.ID, "Matrices");
unsigned int uniformBlockIndexBlue = glGetUniformBlockIndex(shaderBlue.ID, "Matrices");
unsigned int uniformBlockIndexYellow = glGetUniformBlockIndex(shaderYellow.ID, "Matrices");
// 将所有着色器的uniformBlock绑定到同一点(0)
glUniformBlockBinding(shaderRed.ID, uniformBlockIndexRed, 0);
glUniformBlockBinding(shaderGreen.ID, uniformBlockIndexGreen, 0);
glUniformBlockBinding(shaderBlue.ID, uniformBlockIndexBlue, 0);
glUniformBlockBinding(shaderBlue.ID, uniformBlockIndexYellow, 0);
// 创建UBO对象,分配2个mat4大小的内存(对应后面的投影和视图矩阵)
unsigned int uboMatrices;
glGenBuffers(1, &uboMatrices);
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferData(GL_UNIFORM_BUFFER, 2 * sizeof(glm::mat4), NULL, GL_STATIC_DRAW);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
// 第二个参数index,对应绑定点0,第四和第五个参数对应起始偏移量和数据大小
glBindBufferRange(GL_UNIFORM_BUFFER, 0, uboMatrices, 0, 2 * sizeof(glm::mat4));
// 填充投影矩阵
glm::mat4 projection = glm::perspective(45.0f, (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
//第二个参数0代表着填充到第一个mat4位置
glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(glm::mat4), glm::value_ptr(projection));
glBindBuffer(GL_UNIFORM_BUFFER, 0);
如果共享数据是光源类参数,可定义结构体LightData,将sizeof(glm::mat4)换成sizeof(LightData)就可。我们接着看其他核心代码
{
// 由于视图矩阵会随鼠标变化,因此放在渲染循环内
glm::mat4 view = camera.GetViewMatrix();
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
//填充到第二个mat4位置
glBufferSubData(GL_UNIFORM_BUFFER, sizeof(glm::mat4), sizeof(glm::mat4), glm::value_ptr(view));
glBindBuffer(GL_UNIFORM_BUFFER, 0);
}
//顶点着色器代码
layout (std140) uniform Matrices
{
mat4 projection;
mat4 view;
};
其中的“std140”表示内存对齐规则,最后修改了下原项目的纹理,效果如下
练习四、几何着色器
我们之前常用的有顶点和片段着色器,为了引出几何着色器,下面来对比一下。
下面我们来看一个具体修改三角形图元的例子,几何着色器代码如下
layout (triangles) in;
layout (triangle_strip, max_vertices = 3) out;
in VS_OUT {
vec2 texCoords;
vec3 vNormal;
vec3 vModelPos;
} gs_in[];
in vec3 vNormal[];
in vec3 vModelPos[];
第一行的triangles表示输入图元是三角形,第二行表示triangle_strip表示图元类型是三角形带,max_vertices表示单次最多生成的顶点数量。因此这里输入的数据都是容量为3的数组。
vec4 explode(vec4 position, vec3 normal)
{
float magnitude = 2;
vec3 direction = normal * ((sin(time) + 1.0) / 2.0) * magnitude;
return position + vec4(direction, 0.0);
}
vec3 GetNormal()
{
vec3 a = vec3(gl_in[0].gl_Position) - vec3(gl_in[1].gl_Position);
vec3 b = vec3(gl_in[2].gl_Position) - vec3(gl_in[1].gl_Position);
return normalize(cross(a, b));
}
void main() {
vec3 normal = GetNormal();
Normal = gs_in[0].vNormal;
modelPos = gs_in[0].vModelPos;
gl_Position = explode(gl_in[0].gl_Position, normal);
TexCoords = gs_in[0].texCoords;
EmitVertex();
...
EndPrimitive();
...
上面通过计算三角形两向量的叉积,然后再将每个顶点朝叉积方向偏移,从而使三角形图元朝外法线方向移动,模拟类似爆炸的效果。后面在main函数中发射顶点、法向量等数据,这里需要发射三次,最后EndPrimitive函数表示图元发射的结束。我在原项目的基础上加上了镜面反射,最终效果如下~
最后,水平有限,欢迎指正和交流~觉得有用的话点点赞或收藏吧~