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個子樣本

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/79243.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/79243.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/79243.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

idea報錯java: 非法字符: ‘\ufeff‘解決方案

解決方案步驟以及說明 BOM是什么&#xff1f;1. BOM的作用2. 為什么會出現 \ufeff 錯誤&#xff1f;3. 如何解決 \ufeff 問題&#xff1f; 最后重新編譯&#xff0c;即可運行&#xff01;&#xff01;&#xff01; BOM是什么&#xff1f; \ufeff 是 Unicode 中的 BOM&#xff0…

open webui 介紹 是一個可擴展、功能豐富且用戶友好的本地部署 AI 平臺,支持完全離線運行。

AI MCP 系列 AgentGPT-01-入門介紹 Browser-use 是連接你的AI代理與瀏覽器的最簡單方式 AI MCP(大模型上下文)-01-入門介紹 AI MCP(大模型上下文)-02-awesome-mcp-servers 精選的 MCP 服務器 AI MCP(大模型上下文)-03-open webui 介紹 是一個可擴展、功能豐富且用戶友好的…

Log4j2遠程命令執行(CVE-2021-44228)復現

這里選擇使用vulfocue的靶場來進行復現 描述: Apache Log4j2 是一個基于 Java 的日志記錄工具。該工具重寫了 Log4j 框架&#xff0c;并且引入了大量豐富的特性。該日志框架被大量用于業務系統開發&#xff0c;用來記錄日志信息。 在大多數情況下&#xff0c;開發者可能會將用…

模型提示詞

一 提示詞 &#xff08;一&#xff09; 提示詞&#xff08;Prompt&#xff09;是用戶發送給大語言模型的問題、指令或請求&#xff0c;** 1 來明確地告訴模型用戶想要解決的問題或完成的任務&#xff0c;是大語言模型理解用戶需求并據此生成相關、準確回答或內容的基礎。對于…

深度學習算法:從基礎到實踐

簡介 深度學習作為人工智能領域的一個重要分支&#xff0c;近年來在多個領域取得了顯著的成就。本文將從基礎概念出發&#xff0c;探討深度學習算法的核心原理&#xff0c;并介紹一些實際應用案例。 深度學習算法的核心概念 深度學習算法基于人工神經網絡&#xff0c;通過構…

5.9 《GPT-4調試+測試金字塔:構建高可靠系統的5大實戰策略》

5.4 測試與調試:構建企業級質量的保障體系 關鍵詞:測試金字塔模型、GPT-4調試助手、LangChain調試模式、異步任務驗證 測試策略設計(測試金字塔實踐) #mermaid-svg-RblGbJVMnCIShiCW {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill…

Visio繪圖工具全面科普:解鎖專業圖表繪制新境界[特殊字符]

Visio繪圖工具全面科普&#xff1a;解鎖專業圖表繪制新境界&#x1f31f; 在信息爆炸的時代&#xff0c;清晰、直觀地呈現復雜信息變得至關重要。無論是繪制流程圖&#x1f4ca;、組織結構圖&#x1f465;&#xff0c;還是規劃網絡拓撲&#x1f5a7;&#xff0c;一款強大的繪圖…

ShellScript腳本編程

語法基礎 腳本結構 我們先從這個小demo程序來窺探一下我們shell腳本的程序結構 #!/bin/bash# 注釋信息echo_str"hello world"test(){echo $echo_str }test echo_str 首先我們可以通過文本編輯器(在這里我們使用linux自帶文本編輯神器vim)&#xff0c;新建一個文件…

你了解哪些Java限流算法?

大家好&#xff0c;我是鋒哥。今天分享關于【你了解哪些Java限流算法?】面試題。希望對大家有幫助&#xff1b; 你了解哪些Java限流算法? 1000道 互聯網大廠Java工程師 精選面試題-Java資源分享網 在 Java 中&#xff0c;限流算法廣泛用于控制流量、避免過載和保護系統的穩…

prime-2 靶場筆記(vuInhub靶場)

前言&#xff1a; 在本次靶場環境中涉及的知識點&#xff0c;主要包含LFI和SMB以及Lxd組提權&#xff0c;具體內容包括主機探測、端口掃描、目錄掃描、wpscan掃描、反彈shell、一句話木馬、容器、linux各種提權和維持。 環境介紹&#xff1a; 本靶場使用了kali&#xff08;192…

SparseDrive---論文閱讀

純視覺下的稀疏場景表示 算法動機&開創性思路 算法動機&#xff1a; 依賴于計算成本高昂的鳥瞰圖&#xff08;BEV&#xff09;特征表示。預測和規劃的設計過于直接&#xff0c;沒有充分利用周圍代理和自我車輛之間的高階和雙向交互。場景信息是在agent周圍提取&#xff…

旅游特種兵迪士尼大作戰:DeepSeek高精準路徑優化

DeepSeek大模型高性能核心技術與多模態融合開發 - 商品搜索 - 京東 隨著假期的腳步日漸臨近&#xff0c;環球影城等備受矚目的主題游樂場&#xff0c;已然成為大人與孩子們心中不可或缺的節日狂歡圣地。然而&#xff0c;隨之而來的龐大客流&#xff0c;卻總讓無數游客在歡樂的…

android rtsp 拉流h264 h265,解碼nv12轉碼nv21耗時卡頓問題及ffmpeg優化

一、 背景介紹及問題概述 項目需求需要在rk3568開發板上面&#xff0c;通過rtsp協議拉流的形式獲取攝像頭預覽&#xff0c;然后進行人臉識別 姿態識別等后續其它操作。由于rtsp協議一般使用h.264 h265視頻編碼格式&#xff08;也叫 AVC 和 HEVC&#xff09;是不能直接用于后續處…

運維面試題(十四)

6.將日志從一臺服務器保存到另一臺服務器中的方法 1.使用 rsync 同步日志文件 2.使用 scp 手動或腳本化傳輸 3.配置日志服務&#xff08;如 syslog 或 rsyslog &#xff09;遠程傳輸 ? 4.編寫腳本定時上傳&#xff1a;結合 cron 定時任務和傳輸工具&#xff0c;編…

永磁同步電機控制中,滑模觀測器是基于反電動勢觀測轉子速度和角度的?擴展卡爾曼濾波觀測器是基于什么觀測的?擴展卡爾曼濾波觀測器也是基于反電動勢嗎?

滑模觀測器在PMSM中的應用&#xff1a; 滑模觀測器是一種非線性觀測器&#xff0c;利用切換函數設計&#xff0c;使得狀態估計誤差迅速趨近于零&#xff0c;實現快速響應和對外部干擾的魯棒性。 在永磁同步電機&#xff08;PMSM&#xff09;無傳感器控制中&#xff0c;滑模觀測…

【前端】Vue一本通 ESLint JSX

近幾天更新完畢&#xff0c;不定期持續更新&#xff0c;建議關注收藏點贊。 目錄 工具推薦vscode插件vue-devtoolsESLint JSX語法擴展簡介設計模式快速入門 vue/cli腳手架使用vue指令 工具推薦 工欲善其事&#xff0c;必先利其器。 vscode插件 Vetur&#xff1a;vue代碼高亮…

【adb】bat批處理+adb 自動亮屏,自動解鎖屏幕,啟動王者榮耀

準備adb 下載 需要確認是否安裝了adb.exe文件,可以在: 任務管理器 -->詳細信息–>找一下后臺運行的adb 安裝過anroid模擬器,也存在adb,例如:雷電安裝目錄 D:\leidian\LDPlayer9 單獨下載adb 官方下載地址:[官方網址] 下載目錄文件: 測試adb USB連接手機 首先在設置界…

微信小程序轉為App實踐篇 FinClip

參考下面鏈接先 開始實踐 微信小程序轉為App并上架應用市場_微信小程序生成app-CSDN博客 首先在FinClip 官網上下載應用 小程序開發工具下載_小程序sdk下載資源-FinClip資源下載|泰坪小程序開放平臺 下載到本地安裝 打開導入自己的小程序項目&#xff1b;導入時會解析自己的…

arco design框架中的樹形表格使用中的緩存問題

目錄 1.問題 2.解決方案 1.問題 arco design框架中的樹形表格使用中的緩存問題&#xff0c;使用了樹形表格的load-more懶加載 點擊展開按鈕后&#xff0c;點擊關閉&#xff0c;再次點擊展開按鈕時&#xff0c;沒有調用查詢接口&#xff0c;而是使用了緩存的數據。 2.解決方…

100個GEO基因表達芯片或轉錄組數據處理023.GSE24807

100個GEO基因表達芯片或轉錄組數據處理 寫在前邊 雖然現在是高通量測序的時代&#xff0c;但是GEO、ArrayExpress等數據庫儲存并公開大量的基因表達芯片數據&#xff0c;還是會有大量的需求去處理芯片數據&#xff0c;并且建模或驗證自己所研究基因的表達情況&#xff0c;芯片…