当前位置: 首页 > news >正文

OpenGL学习笔记(几何着色器、实例化、抗锯齿)

目录

  • 几何着色器
    • 爆破物体
    • 法向量可视化
  • 实例化(偏移量存在uniform中)
    • 实例化数组(偏移量存在顶点属性中)
    • 小行星带
  • 抗锯齿
    • SSAA(Super Sample Anti-aliasing)
    • MSAA(Multi-Sampling Anti-aliasing)
    • OpenGL中的MSAA
    • 离屏MSAA
      • 多重采样纹理附件
      • 多重采样渲染缓冲对象
      • 渲染到多重采样帧缓冲
    • 自定义抗锯齿算法

GitHub主页:https://github.com/sdpyy1
OpenGL学习仓库:https://github.com/sdpyy1/CppLearn/tree/main/OpenGLtree/main/OpenGL):https://github.com/sdpyy1/CppLearn/tree/main/OpenGL

几何着色器

在顶点着色器和片段着色器中间还可以添加一个几何着色器(Geometry Shader),输入为图元,可以将顶点进行随意的变换。它能够将(这一组)顶点变换为完全不同的图元,并且还能生成比原来更多的顶点。
下面是顶点着色器的一个例子

#version 330 core
layout (points) in;
layout (line_strip, max_vertices = 2) out;void main() {    gl_Position = gl_in[0].gl_Position + vec4(-0.1, 0.0, 0.0, 0.0); EmitVertex();gl_Position = gl_in[0].gl_Position + vec4( 0.1, 0.0, 0.0, 0.0);EmitVertex();EndPrimitive();
}

其中layout (points) in;用于声明图元的类型(可以是点、线、三角形等待),接下来还需要指定输出的图元类型,这里输出的是line_strip,将最大顶点数设置为2个(EmitVertex()最多执行两次)。
在这里插入图片描述

为了生成更有意义的结果,我们需要某种方式来获取前一着色器阶段的输出。GLSL提供给我们一个名为gl_in的内建(Built-in)变量,在内部看起来(可能)是这样的:

in gl_Vertex
{vec4  gl_Position;float gl_PointSize;float gl_ClipDistance[];
} gl_in[];

要注意的是,它被声明为一个数组,因为大多数的渲染图元包含多于1个的顶点,而几何着色器的输入是一个图元的所有顶点。
下面意思就是在输入图元是点时,输出点是把这个点左移一个单位和右移一个单位的两个点连成的线

void main() {gl_Position = gl_in[0].gl_Position + vec4(-0.1, 0.0, 0.0, 0.0); EmitVertex();gl_Position = gl_in[0].gl_Position + vec4( 0.1, 0.0, 0.0, 0.0);EmitVertex();EndPrimitive();
}

通过这个几何着色器,输入4个顶点渲染时,输出会变成四条线
在这里插入图片描述

下面来进行实际演示

float points[] = {-0.5f,  0.5f, // 左上0.5f,  0.5f, // 右上0.5f, -0.5f, // 右下-0.5f, -0.5f  // 左下
};
shader.use();
glBindVertexArray(VAO);
glDrawArrays(GL_POINTS, 0, 4);

先绘制出4个点
在这里插入图片描述
下来创建一个几何着色器,不做任何处理,直接发射

#version 330 core
layout(points) in;
layout(points, max_vertices = 1) out;
void main(){gl_Position = gl_in[0].gl_Position;EmitVertex();EndPrimitive();
}

shader编译时添加几何着色器的编译

Shader(const char* vertexPath, const char* geometryPath ,const char* fragmentPath){// 1. retrieve the vertex/fragment source code from filePathstd::string vertexCode;std::string fragmentCode;std::string geometryCode;std::ifstream vShaderFile;std::ifstream fShaderFile;std::ifstream gShaderFile;// ensure ifstream objects can throw exceptions:vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);gShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);try{// open filesvShaderFile.open(vertexPath);fShaderFile.open(fragmentPath);gShaderFile.open(geometryPath);std::stringstream vShaderStream, fShaderStream, gShaderStream;// read file's buffer contents into streamsvShaderStream << vShaderFile.rdbuf();fShaderStream << fShaderFile.rdbuf();gShaderStream << gShaderFile.rdbuf();// close file handlersvShaderFile.close();fShaderFile.close();gShaderFile.close();// convert stream into stringvertexCode = vShaderStream.str();fragmentCode = fShaderStream.str();geometryCode = gShaderStream.str();}catch (std::ifstream::failure& e){std::cout << "ERROR::SHADER::FILE_NOT_SUCCESSFULLY_READ: " << e.what() << std::endl;}const char* vShaderCode = vertexCode.c_str();const char * fShaderCode = fragmentCode.c_str();const char * gShaderCode = geometryCode.c_str();// 2. compile shadersunsigned int vertex, fragment, geometry;// vertex shaderGL_CALL(vertex = glCreateShader(GL_VERTEX_SHADER));GL_CALL(glShaderSource(vertex, 1, &vShaderCode, NULL));GL_CALL(glCompileShader(vertex));GL_CALL(checkCompileErrors(vertex, "VERTEX"));// 几何着色器GL_CALL(geometry = glCreateShader(GL_GEOMETRY_SHADER));glShaderSource(geometry, 1, &gShaderCode, NULL);glCompileShader(geometry);GL_CALL(checkCompileErrors(geometry, "GEOMETRY"));// fragment ShaderGL_CALL(fragment = glCreateShader(GL_FRAGMENT_SHADER));GL_CALL(glShaderSource(fragment, 1, &fShaderCode, NULL));GL_CALL(glCompileShader(fragment));GL_CALL(checkCompileErrors(fragment, "FRAGMENT"));// shader ProgramGL_CALL(ID = glCreateProgram());GL_CALL(glAttachShader(ID, vertex));GL_CALL(glAttachShader(ID, fragment));GL_CALL(glAttachShader(ID, geometry));GL_CALL(glLinkProgram(ID));GL_CALL(checkCompileErrors(ID, "PROGRAM"));// delete the shaders as they're linked into our program now and no longer necessaryGL_CALL(glDeleteShader(vertex));GL_CALL(glDeleteShader(fragment));GL_CALL(glDeleteShader(geometry));}

在这里插入图片描述
这里要理解最终调用的是画点的指令glDrawArrays(GL_POINTS, 0, 4);所以几何着色器运行一次只能得到一个点。通过一个点的位移来得到不同的输出点,而不是可以一口气输入3个点。
修改几何着色器

#version 330 core
layout (points) in;
layout (triangle_strip, max_vertices = 5) out;void build_house(vec4 position)
{gl_Position = position + vec4(-0.2, -0.2, 0.0, 0.0);    // 1:左下EmitVertex();gl_Position = position + vec4( 0.2, -0.2, 0.0, 0.0);    // 2:右下EmitVertex();gl_Position = position + vec4(-0.2,  0.2, 0.0, 0.0);    // 3:左上EmitVertex();gl_Position = position + vec4( 0.2,  0.2, 0.0, 0.0);    // 4:右上EmitVertex();gl_Position = position + vec4( 0.0,  0.4, 0.0, 0.0);    // 5:顶部EmitVertex();EndPrimitive();
}
void main() {build_house(gl_in[0].gl_Position);
}

在这里插入图片描述
进一步我们可以在几何着色器中处理颜色
首先在顶点着色器传递颜色,用接口快传递

#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;out VS_OUT {vec3 color;
} vs_out;void main()
{gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0);vs_out.color = aColor;
}

注意接收接口块的时候使用数组接收(应该是为了兼容输入图元是三角形的情况),之后还要把颜色输出

#version 330 core
layout (points) in;
layout (triangle_strip, max_vertices = 5) out;
in VS_OUT {vec3 color;
} gs_in[];
out vec3 fColor;void build_house(vec4 position)
{fColor = gs_in[0].color; // gs_in[0] 因为只有一个输入顶点gl_Position = position + vec4(-0.2, -0.2, 0.0, 0.0);    // 1:左下EmitVertex();gl_Position = position + vec4( 0.2, -0.2, 0.0, 0.0);    // 2:右下EmitVertex();gl_Position = position + vec4(-0.2,  0.2, 0.0, 0.0);    // 3:左上EmitVertex();gl_Position = position + vec4( 0.2,  0.2, 0.0, 0.0);    // 4:右上EmitVertex();gl_Position = position + vec4( 0.0,  0.4, 0.0, 0.0);    // 5:顶部EmitVertex();EndPrimitive();
}void main() {build_house(gl_in[0].gl_Position);
}

在这里插入图片描述
如果在发射某个顶点时修改了fcolor的值,那这个顶点的数据就会被修改,

    gl_Position = position + vec4( 0.0,  0.4, 0.0, 0.0);    // 5:顶部fColor = vec3(1.0, 1.0, 1.0);EmitVertex();

所以顶部的颜色设置为了白色,通过插值就有了下图的效果
在这里插入图片描述

爆破物体

我们将每个三角形在几何着色器中沿着法向量移动一小段时间。
首先把代码恢复到展示一个背包的状态。
几何着色器输入是三角形,输出也是三角形,在发射三个顶点的时候,修改顶点的位置

#version 330 core
layout (triangles) in;
layout (triangle_strip, max_vertices = 3) out;in VS_OUT {vec2 texCoords;
} gs_in[];out vec2 TexCoords; uniform float time;vec4 explode(vec4 position, vec3 normal) { ... }vec3 GetNormal() { ... }void main() {    vec3 normal = GetNormal();gl_Position = explode(gl_in[0].gl_Position, normal);TexCoords = gs_in[0].texCoords;EmitVertex();gl_Position = explode(gl_in[1].gl_Position, normal);TexCoords = gs_in[1].texCoords;EmitVertex();gl_Position = explode(gl_in[2].gl_Position, normal);TexCoords = gs_in[2].texCoords;EmitVertex();EndPrimitive();
}

在这里插入图片描述

法向量可视化

三角形图元输入后额外添加3个法向量方向的点,输出图元为线图,所以会绘制出三角形+三条法线

#version 330 core
layout (triangles) in;
layout (line_strip, max_vertices = 6) out;in VS_OUT {vec3 normal;
} gs_in[];const float MAGNITUDE = 0.4;uniform mat4 projection;void GenerateLine(int index)
{gl_Position = projection * gl_in[index].gl_Position;EmitVertex();gl_Position = projection * (gl_in[index].gl_Position +vec4(gs_in[index].normal, 0.0) * MAGNITUDE);EmitVertex();EndPrimitive();
}void main()
{GenerateLine(0); // 第一个顶点法线GenerateLine(1); // 第二个顶点法线GenerateLine(2); // 第三个顶点法线
}

第一遍用正常的shader进行渲染,第二次渲染用这一套shader,就可以实现如下效果
在这里插入图片描述
换个模型
在这里插入图片描述
除了让我们的背包变得毛茸茸之外,它还能让我们很好地判断模型的法向量是否准确。你可以想象到,这样的几何着色器也经常用于给物体添加毛发(Fur)。

实例化(偏移量存在uniform中)

当一个模型通过修改Model变换矩阵后渲染多份(草地),如果我们需要渲染大量物体时,代码看起来会像这样:

for(unsigned int i = 0; i < amount_of_models_to_draw; i++)
{DoSomePreparations(); // 绑定VAO,绑定纹理,设置uniform等glDrawArrays(GL_TRIANGLES, 0, amount_of_vertices);
}

如果像这样绘制模型的大量实例(Instance),你很快就会因为绘制调用过多而达到性能瓶颈。【因为OpenGL在绘制顶点数据之前需要做很多准备工作(比如告诉GPU该从哪个缓冲读取数据,从哪寻找顶点属性,而且这些都是在相对缓慢的CPU到GPU总线(CPU to GPU Bus)上进行的)。所以,即便渲染顶点非常快,命令GPU去渲染却未必】。

如果我们能够将数据一次性发送给GPU,然后使用一个绘制函数让OpenGL利用这些数据绘制多个物体,就会更方便了。这就是实例化(Instancing)
glDrawArraysInstanced和glDrawElementsInstanced就是用来实例化的,这些渲染函数需要需要一个额外的参数,叫做实例数量(Instance Count)。这样我们只需要将必须的数据发送到GPU一次,然后使用一次函数调用告诉GPU它应该如何绘制这些实例。

但我们还需要考虑是在不同的位置渲染,出于这个原因,GLSL在顶点着色器中嵌入了另一个内建变量,gl_InstanceID

在使用实例化渲染调用时,gl_InstanceID会从0开始,在每个实例被渲染时递增1。比如说,我们正在渲染第43个实例,那么顶点着色器中它的gl_InstanceID将会是42。

具体做法就是在片段着色器中添加一个uniform,表示偏移量数组,刚好用gl_InstanceID可以来表示渲染的id

uniform vec2 offsets[100];
main中:vec2 offset = offsets[gl_InstanceID];gl_Position = vec4(aPos + offset, 0.0, 1.0);

下来就需要填充这些参数了,之后直接调glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100);来渲染一百次。这样就是一次性交给GPU100条渲染,每条渲染都有一个gl_InstanceID
调用过程就是每次渲染传入的gl_InstanceID是不一样的,所以偏移量也不同。

实例化数组(偏移量存在顶点属性中)

如果我们渲染个数特别多,偏移量将达到uniform数据上限,替代方案就是实例化数组,他被定义为一个顶点属性,只有渲染一个新的实例时才会刷新。
可以把在程序中定义的偏移量数组装在一个VBO中

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);

并设置VAO

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告诉了OpenGl该什么时候更新顶点属性内容到新一组数据,第一个参数是需要的顶点属性,第二个参数是属性除数,属性除数是0,告诉OpenGL我们需要在顶点着色器的每次迭代时更新顶点属性。将它设置为1时,我们告诉OpenGL我们希望在渲染一个新实例的时候更新顶点属性。而设置为2时,我们希望每2个实例更新一次属性,以此类推。我们将属性除数设置为1,是在告诉OpenGL,处于位置值2的顶点属性是一个实例化数组。

看下边这个就懂了,设置属性位置1,2时还是正常进行,但属性3不是来自的不是quadVBO(也就是顶点数据),而是来自instanceVBO(就是偏移量数据),它存储在layout(location = 2),并通过glVertexAttribDivisor来告诉2号参数每个实例化取下一个。

 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, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);glEnableVertexAttribArray(1);glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(2 * sizeof(float)));// also set instance dataglEnableVertexAttribArray(2);glBindBuffer(GL_ARRAY_BUFFER, instanceVBO); // this attribute comes from a different vertex bufferglVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);glBindBuffer(GL_ARRAY_BUFFER, 0);glVertexAttribDivisor(2, 1); // tell OpenGL this is an instanced vertex attribute.

按照这样理解,那属性0和属性1其实事实上就是每个顶点刷新一次,也就是调用了
glVertexAttribDivisor(0,0)和glVertexAttribDivisor(1, 0)

从另外一个角度理解就是这些layout的参数是可以设置刷新时机(每个顶点或每次实例化)的,不过只有在调用实例化绘制方法glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100);时才会生效

layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aOffset;

小行星带

围绕星体旋转的岩石就可以用同一个模型进行渲染。实例化很适合的场景。
如果直接渲染1000次代码如下

const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
// camera
Camera camera(glm::vec3(0.0f, 10.0f, 70.0f));
float lastX = SCR_WIDTH / 2.0f;
float lastY = SCR_HEIGHT / 2.0f;
bool firstMouse = true;
// timing
float deltaTime = 0.0f;	// time between current frame and last frame
float lastFrame = 0.0f;int main(){// 初始化窗口GLFWwindow * window = InitWindowAndFunc();stbi_set_flip_vertically_on_load(true);// 启用深度测试glEnable(GL_DEPTH_TEST);// 加载模型Model rock("./assets/rock/rock.obj");Model planet("./assets/planet/planet.obj");// 加载shaderShader shader("./shader/rockAndPlanet.vert", "./shader/rockAndPlanet.frag");unsigned int amount = 1000;glm::mat4* modelMatrices;modelMatrices = new glm::mat4[amount];srand(static_cast<unsigned int>(glfwGetTime())); // initialize random seedfloat radius = 50.0;float offset = 2.5f;for (unsigned int i = 0; i < amount; i++){glm::mat4 model = glm::mat4(1.0f);// 1. translation: displace along circle with 'radius' in range [-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; // keep height of asteroid field smaller compared to width of x and zdisplacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;float z = cos(angle) * radius + displacement;model = glm::translate(model, glm::vec3(x, y, z));// 2. scale: Scale between 0.05 and 0.25ffloat scale = static_cast<float>((rand() % 20) / 100.0 + 0.05);model = glm::scale(model, glm::vec3(scale));// 3. rotation: add random rotation around a (semi)randomly picked rotation axis vectorfloat rotAngle = static_cast<float>((rand() % 360));model = glm::rotate(model, rotAngle, glm::vec3(0.4f, 0.6f, 0.8f));// 4. now add to list of matricesmodelMatrices[i] = model;}while (!glfwWindowShouldClose(window)){// 清理窗口glClearColor(0.05f, 0.05f, 0.05f, 1.0f);glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);glm::mat4 projection = glm::perspective(glm::radians(45.0f), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 1000.0f);glm::mat4 view = camera.GetViewMatrix();;shader.use();shader.setMat4("projection", projection);shader.setMat4("view", view);// draw planetglm::mat4 model = glm::mat4(1.0f);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);// draw meteoritesfor (unsigned int i = 0; i < amount; i++){shader.setMat4("model", modelMatrices[i]);rock.Draw(shader);}// 事件处理glfwPollEvents();// 双缓冲glfwSwapBuffers(window);processFrameTimeForMove();processInput(window);}glfwTerminate();return 0;
}

1000次独立渲染我的4070ts还完全hold住,
在这里插入图片描述
但是到了10000次就开始卡顿了,最后调整到100000次就很卡了

在这里插入图片描述
接着调整到100w次,就已经是5s一帧了
在这里插入图片描述
下面用实例化数组来进行优化,首先调整顶点着色器来接收数组(直接把Model变换矩阵存在数组里)

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 2) in vec2 aTexCoords;
layout (location = 3) in mat4 instanceMatrix;out vec2 TexCoords;uniform mat4 projection;
uniform mat4 view;void main()
{gl_Position = projection * view * instanceMatrix * vec4(aPos, 1.0); TexCoords = aTexCoords;
}

把刚才存放model变换矩阵的数组存入显存中

// 数组存入VBO中待用unsigned int buffer;glGenBuffers(1, &buffer);glBindBuffer(GL_ARRAY_BUFFER, buffer);glBufferData(GL_ARRAY_BUFFER, amount * sizeof(glm::mat4), &modelMatrices[0], GL_STATIC_DRAW);

修改rock的VAO,让他接收该VBO,这里注意每个Vertex Attribute槽位最多接收4个分量,如果向存储4维矩阵,就得绑定4个属性的位置,但是接收只需要用最前边的location来接收

    for (unsigned int i = 0; i < rock.meshes.size(); i++){unsigned int VAO = rock.meshes[i].VAO;glBindVertexArray(VAO);// set attribute pointers for matrix (4 times vec4)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);}

最后渲染岩石的方法改为,因为rock不止一个mesh

        for (unsigned int i = 0; i < rock.meshes.size(); i++){glBindVertexArray(rock.meshes[i].VAO);glDrawElementsInstanced(GL_TRIANGLES, static_cast<unsigned int>(rock.meshes[i].indices.size()), GL_UNSIGNED_INT, 0, amount);glBindVertexArray(0);}

直接上100w测试,从5s一帧变为不到1s一帧,提升还是很大的
在这里插入图片描述
可以看到,在合适的环境下,实例化渲染能够大大增加显卡的渲染能力。正是出于这个原因,实例化渲染通常会用于渲染草、植被、粒子,以及上面这样的场景,基本上只要场景中有很多重复的形状,都能够使用实例化渲染来提高性能。

抗锯齿

在这里插入图片描述
这里不过多介绍抗锯齿的原因,采用一位大佬的话

锯齿的来源是因为场景的定义在三维空间中是连续的,而最终显示的像素则是一个离散的二维数组。所以判断一个点到底没有被某个像素覆盖的时候单纯是一个“有”或者“没有"问题,丢失了连续性的信息,导致锯齿。也叫做走样Aliasing,所以抗锯齿就是反走样(Anti-aliasing)
在这里插入图片描述

SSAA(Super Sample Anti-aliasing)

最直接的抗锯齿方法就是SSAA(Super Sampling AA)。拿4xSSAA举例子,假设最终屏幕输出的分辨率是800x600, 4xSSAA就会先渲染到一个分辨率1600x1200的buffer上,然后再直接把这个放大4倍的buffer下采样致800x600。这种做法在数学上是最完美的抗锯齿。但是劣势也很明显,光栅化和着色的计算负荷都比原来多了4倍,render target的大小也涨了4倍。

之前不是学过OpenGL的离线渲染吗,就可以先在自定义帧缓冲中渲染一个高分辨的图片加入到纹理中,在0号帧缓冲中再采样纹理,即可达到SSAA的目的

MSAA(Multi-Sampling Anti-aliasing)

光栅器是位于最终处理过的顶点之后到片段着色器之前所经过的所有的算法与过程的总和。
在这里插入图片描述
从上图到下图就有锯齿了
在这里插入图片描述
一张图就解释MSAA在干什么了。
在这里插入图片描述
在这里插入图片描述
三角形的不平滑边缘被稍浅的颜色所包围后,从远处观察时就会显得更加平滑了。
在这里插入图片描述

OpenGL中的MSAA

走样的效果
在这里插入图片描述

开启MSAA需要在创建窗口之前告诉OpenGL需要多重采样,每个像素有了4个颜色缓冲,4代表每个像素将会被采样4次(都cover的情况下),每次采样都会获得一个子像素值,这些值最终被平均以生成最终的像素颜色

    glfwWindowHint(GLFW_SAMPLES, 4);

4次采样发生在光栅化之后,片段着色器执行之前。
光栅化首先将三角形顶点通过视口变换转变到屏幕坐标上,之后对三角形求包围盒,变量包围盒中的像素,判断那些像素点在三角形内部,通过插值算出该像素对应的UV坐标,利用该坐标去纹理图片中取颜色。
4个采样点说明一个像素4个点都需要做一次判断是否在三角形内部的操作,在三角形内部的点取纹理上采样,并写入对应的子颜色缓冲,不在三角形内部的点就不改变目前子颜色缓冲中的值

开启MSAA(其实默认就是开启的)

glEnable(GL_MULTISAMPLE);

因为多重采样的算法都在OpenGL驱动的光栅器中实现了,我们不需要再多做什么。
开启后
在这里插入图片描述

离屏MSAA

由于GLFW负责了创建多重采样缓冲,启用MSAA非常简单。然而,如果我们想要使用我们自己的帧缓冲来进行离屏渲染,那么我们就必须要自己动手生成多重采样缓冲了。现在,我们确实需要自己创建多重采样缓冲区。
有两种方式可以创建多重采样缓冲,将其作为帧缓冲的附件:纹理附件和渲染缓冲附件

多重采样纹理附件

为了创建一个支持储存多个采样点的纹理,我们使用glTexImage2DMultisample来替代glTexImage2D,它的纹理目标是GL_TEXTURE_2D_MULTISAPLE。

glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, tex);
glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, GL_RGB, width, height, GL_TRUE);
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);

我们使用glFramebufferTexture2D将多重采样纹理附加到帧缓冲上,但这里纹理类型使用的是GL_TEXTURE_2D_MULTISAMPLE。

glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, tex, 0);

多重采样渲染缓冲对象

同样也是创建RBO并绑定。在设置深度和模板缓冲时,要切换为多重采样的缓冲

glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH24_STENCIL8, width, height);

渲染到多重采样帧缓冲

因为多重采样缓冲有一点特别,我们不能直接将它们的缓冲图像用于其他运算,比如在着色器中对它们进行采样。
一个多重采样的图像包含比普通图像更多的信息,我们所要做的是缩小或者还原(Resolve)图像。多重采样帧缓冲的还原通常是通过glBlitFramebuffer来完成,它能够将一个帧缓冲中的某个区域复制到另一个帧缓冲中,并且将多重采样缓冲还原。

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);

自定义抗锯齿算法

将一个多重采样的纹理图像不进行还原直接传入着色器也是可行的。GLSL提供了这样的选项,让我们能够对纹理图像的每个子样本进行采样,所以我们可以创建我们自己的抗锯齿算法。在大型的图形应用中通常都会这么做。
要想获取每个子样本的颜色值,你需要将纹理uniform采样器设置为sampler2DMS,而不是平常使用的sampler2D:

uniform sampler2DMS screenTextureMS;

使用texelFetch函数就能够获取每个子样本的颜色值了:

vec4 colorSample = texelFetch(screenTextureMS, TexCoords, 3);  // 第4个子样本

相关文章:

  • Spring 是如何解决循环依赖的
  • 火山引擎旗下防御有哪些
  • 东方博宜OJ ——2395 - 部分背包问题
  • 游戏引擎学习第228天
  • Mysql的查询
  • 2021-10-29 C++按天数返回年月日,按年月日求第几天。
  • Android 项目 Camera 问题:Fail to connect to camera service
  • std::condition_variable的使用说明(详细解释和使用示例)
  • YOLOv3损失函数与训练模块的源码解析
  • Web:Swagger 生成文档后与前端的对接
  • rebase master后会将master的commit历史加入这个分支吗
  • bat脚本执行完后自动删除
  • 第七讲、在Isaaclab中使用交互式场景
  • 微信小程序腾讯获得所在城市
  • Python multiprocessing模块Pool类介绍
  • DeepReaserch写的文献综述示例分享
  • 【Kubernetes基础--Pod深入理解】--查阅笔记2
  • vmcore分析锁问题实例(x86-64)
  • 站台候车,好奇铁道旁的碎石(道砟)为何总是黄色的?
  • Spark-SQL核心编程2
  • 江西省人大教育科学文化卫生委员会主任委员王水平被查
  • 重大虚开发票偷税骗补案被查处:价税2.26亿,涉700余名主播
  • 运油-20亮相中埃空军联训
  • 为溶血性疾病治疗提供新靶点,专家团队在《细胞》发文
  • 儿童阅读空间、残疾人友好书店……上海黄浦如何打造城市书房
  • 天文学家、民盟江苏省委会原常务副主委任江平逝世