LearnOpenGL学习(高级OpenGL - - 实例化,抗锯齿)
实例化
对于在同一场景中使用相同顶点数据的对象(如草地中的草),可以使用实例化(Instancing)技术,用一个绘制函数让OpenGL绘制多个物体,而非循环(Drawcall: N->1)。
实例化技术本质上是减少了数据从CPU到GPU的传输次数。
实例化这项技术能够让我们使用一个渲染调用来绘制多个物体,来节省每次绘制物体时CPU -> GPU的通信,它只需要一次即可。
使用 glDrawArraysInstanced 和 glDrawElementsInstanced 就可以。这些渲染函数的实例化版本需要一个额外的参数,叫做实例数量(Instance Count),它能够设置我们需要渲染的实例个数。
顶点着色器内建变量 gl_InstanceID 保存了当前渲染图元所在是实例索引。借助该变量,我们可以改变其位置,渲染方式等。从0开始,当渲染第43个实例时,该变量为42
索引一个包含100个偏移向量的uniform数组,将偏移值加到每个实例化的四边形上。最终的结果是一个排列整齐的四边形网格:
//vs
#version 330 core
out vec4 FragColor;in vec3 fColor;void main()
{FragColor = vec4(fColor, 1.0);
}//fs
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;out vec3 fColor;uniform vec2 offsets[100];void main()
{vec2 offset = offsets[gl_InstanceID];gl_Position = vec4(aPos + offset, 0.0, 1.0);fColor = aColor;
}//.cpp//定义数组
glm::vec2 translations[100];
int index = 0;
float offset = 0.1f;
for(int y = -10; y < 10; y += 2)
{for(int x = -10; x < 10; x += 2){glm::vec2 translation;translation.x = (float)x / 10.0f + offset;translation.y = (float)y / 10.0f + offset;translations[index++] = translation;}
}//将数组转移到顶点着色器的uniform中
shader.use();
for(unsigned int i = 0; i < 100; i++)
{stringstream ss;string index;ss << i; index = ss.str(); shader.setVec2(("offsets[" + index + "]").c_str(), translations[i]);
}//绘制
glBindVertexArray(quadVAO);
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100);
实例化数组
实例化数组(Instanced Array),它被定义为一个顶点属性(能够让我们储存更多的数据),仅在顶点着色器渲染一个新的实例时才会更新。
使用顶点属性时,顶点着色器的每次运行都会让GLSL获取新一组适用于当前顶点的属性。而当我们将顶点属性定义为一个实例化数组时,顶点着色器就只需要对每个实例,而不是每个顶点,更新顶点属性的内容了。这允许我们对逐顶点的数据使用普通的顶点属性,而对逐实例的数据使用实例化数组。
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aOffset;//实例化数组out vec3 fColor;void main()
{gl_Position = vec4(aPos + aOffset, 0.0, 1.0);fColor = aColor;
}
unsigned int instanceVBO;
glGenBuffers(1, &instanceVBO);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(glm::vec2) * 100, &translations[0], GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glEnableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glVertexAttribDivisor(2, 1);
可以看到,唯一的区别在于 glVertexAttribDivisor(AttribIdx,Count) 函数。这个函数定义了什么时候更新顶点属性的内容到新一组数据。Count
参数为0时,每次顶点着色器运行都更新,即默认的方式;参数为1时,运行到每个实例时更新;参数为2时,每两个实例更新,以此类推。
小行星带
随机代码:
unsigned int amount = 1000;
glm::mat4 *modelMatrices;
modelMatrices = new glm::mat4[amount];
srand(glfwGetTime()); // 初始化随机种子
float radius = 50.0;
float offset = 2.5f;
for(unsigned int i = 0; i < amount; i++)
{glm::mat4 model;// 1. 位移:分布在半径为 'radius' 的圆形上,偏移的范围是 [-offset, offset]float angle = (float)i / (float)amount * 360.0f;float displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;float x = sin(angle) * radius + displacement;displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;float y = displacement * 0.4f; // 让行星带的高度比x和z的宽度要小displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;float z = cos(angle) * radius + displacement;model = glm::translate(model, glm::vec3(x, y, z));// 2. 缩放:在 0.05 和 0.25f 之间缩放float scale = (rand() % 20) / 100.0f + 0.05;model = glm::scale(model, glm::vec3(scale));// 3. 旋转:绕着一个(半)随机选择的旋转轴向量进行随机的旋转float rotAngle = (rand() % 360);model = glm::rotate(model, rotAngle, glm::vec3(0.4f, 0.6f, 0.8f));// 4. 添加到矩阵的数组中modelMatrices[i] = model;
}
绘制代码:
// 绘制行星
shader.use();
glm::mat4 model;
model = glm::translate(model, glm::vec3(0.0f, -3.0f, 0.0f));
model = glm::scale(model, glm::vec3(4.0f, 4.0f, 4.0f));
shader.setMat4("model", model);
planet.Draw(shader);// 绘制小行星
for(unsigned int i = 0; i < amount; i++)
{shader.setMat4("model", modelMatrices[i]);rock.Draw(shader);
}
不使用实例化,绘制了1000个小行星,帧率为 60
使用实例化绘制了10000个小行星,50帧左右,有点卡
抗锯齿
锯齿现象又称走样(Aliasing),抗锯齿技术又称反走样(Anti-aliasing)。
超采样抗锯齿(Super Sample Aniti-aliasing, SSAA)使用比正常分辨率更高的分辨率渲染常见,当图像输出在帧缓冲更新时,下采样(Downsample)到正常分辨率。额外的分辨率用来防止走样的产生。但由于渲染分辨率的提高,性能开销将变大。NxSSAA指的就是把原分辨率放大N倍渲染后降采样的SSAA。
多重采样
多重采样抗锯齿(Multisample Aniti-aliasing, MSAA)是较为常见的抗锯齿方法。
光栅器是位于最终处理过的顶点之后到片段着色器之前所经过的所有的算法与过程的总和。光栅器会将一个图元的所有顶点作为输入,并将它转换为一系列的片段。顶点坐标与片段之间几乎永远也不会有一对一的映射,所以光栅器必须以某种方式来决定每个顶点最终所在的片段/屏幕坐标。
每个像素中心包含有一个采样点(Sample Point),当采样点位于三角形内部时,这个采样点对应的像素就会生成一个片段。
MSAA把像素的单一采样点变为多个按特定图案排列的四个子采样点(Subsample)。颜色缓冲的大小会随着子采样点的增加而增加。
无论三角形覆盖了多少子采样点,每个像素点都只会运行一次片段着色器。最终输出的片段依然位于像素中央,其y暗色由覆盖的子采样点数量决定。以4xMSAA为例,当三角形覆盖了一个像素的2个采样点时,其颜色就是0.5*三角形颜色+0.5*背景色。
本质上其实是每个子采样点都存储了颜色数据,在为像素计算片段颜色时将四个子采样点中的颜色做平均。
深度和模板测试也能够使用多个采样点。对深度测试来说,每个顶点的深度值会在运行深度测试之前被插值到各个子样本中。对模板测试来说,我们对每个子样本,而不是每个像素,存储一个模板值。当然,这也意味着深度和模板缓冲的大小会乘以子采样点的个数。
OpenGL中的MSAA
使用MSAA后,每个像素中都需要存储特定数量的颜色值。OpenGL中,多重采样缓冲(Multisample Buffer)用于存储特定数量的多重采样样本,替代原来的颜色缓冲。
glfwWindowHint(GLFW_SAMPLES, 4);
//提示GLFW使用一个包含 N 个样本的多重采样缓冲,这里是4
//在创建窗口之前调用
glEnable(GL_MULTISAMPLE); //开启多重采样
离屏MSAA
当我们使用自己的帧缓冲时,需要手动生成多重采样缓冲。与帧缓冲类似,有纹理附件和渲染缓冲对象两种方式。
纹理附件
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, tex);
glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, GL_RGB, width, height, GL_TRUE);
//它的第二个参数设置的是纹理所拥有的样本个数。如果最后一个参数为GL_TRUE,
//图像将会对每个纹素使用相同的样本位置以及相同数量的子采样点个数。
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, tex, 0);
当前绑定的帧缓冲现在就有了一个纹理图像形式的多重采样颜色缓冲。
渲染缓冲对象
glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH24_STENCIL8, width, height);
函数中,渲染缓冲对象后的参数我们将设定为样本的数量,在当前的例子中是4。
渲染到多重采样帧缓冲
绑定到多重采样帧缓冲后,任何绘制调用都会由光栅器负责多重采样运算。我们得到的多重采样缓冲包含了颜色、深度与模板缓冲。多重采样缓冲
多重采样缓冲不能直接用于着色器采样或深度、模板测试。因此,我们在绑定多重采样缓冲并完成绘制后,需要通过 glBuiltFramebuffer 函数将颜色等缓冲传递到其他帧缓冲上,并且将多重采样缓冲还原。
例如,我们想把完成多重采样后的画面传输到默认帧缓冲上,进而显示在窗口上:
glBindFramebuffer(GL_READ_FRAMEBUFFER, multisampledFBO);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
再比如说,我们向把完成多重采样的画面作为一个2D纹理,用于后处理等操作:
unsigned int msFBO = CreateFBOWithMultiSampledAttachments();
// 使用普通的纹理颜色附件创建一个新的FBO
...
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, screenTexture, 0);
...
while(!glfwWindowShouldClose(window))
{...glBindFramebuffer(msFBO);ClearFrameBuffer();DrawScene();// 将多重采样缓冲还原到中介FBO上glBindFramebuffer(GL_READ_FRAMEBUFFER, msFBO);glBindFramebuffer(GL_DRAW_FRAMEBUFFER, intermediateFBO);glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);// 现在场景是一个2D纹理缓冲,可以将这个图像用来后期处理glBindFramebuffer(GL_FRAMEBUFFER, 0);ClearFramebuffer();glBindTexture(GL_TEXTURE_2D, screenTexture);DrawPostProcessingQuad(); ...
}
参考:实例化 - LearnOpenGL CN
LearnOpenGL学习笔记(十) - 高级GLSL、几何着色器、实例化与抗锯齿 - Yoi's Home