OpenGL 圖形庫的使用(三十)—— 高級OpenGL之實例化Instancing

版本記錄

版本號 時間
V1.0 2018.01.17

前言

OpenGL 圖形庫項目中一直也沒用過,最近也想學著使用這個圖形庫,感覺還是很有意思,也就自然想著好好的總結一下,希望對大家能有所幫助。下面內容來自歡迎來到OpenGL的世界
1. OpenGL 圖形庫使用(一) —— 概念基礎
2. OpenGL 圖形庫使用(二) —— 渲染模式、對象、擴展和狀態機
3. OpenGL 圖形庫使用(三) —— 著色器、數據類型與輸入輸出
4. OpenGL 圖形庫使用(四) —— Uniform及更多屬性
5. OpenGL 圖形庫使用(五) —— 紋理
6. OpenGL 圖形庫使用(六) —— 變換
7. OpenGL 圖形庫的使用(七)—— 坐標系統之五種不同的坐標系統(一)
8. OpenGL 圖形庫的使用(八)—— 坐標系統之3D效果(二)
9. OpenGL 圖形庫的使用(九)—— 攝像機(一)
10. OpenGL 圖形庫的使用(十)—— 攝像機(二)
11. OpenGL 圖形庫的使用(十一)—— 光照之顏色
12. OpenGL 圖形庫的使用(十二)—— 光照之基礎光照
13. OpenGL 圖形庫的使用(十三)—— 光照之材質
14. OpenGL 圖形庫的使用(十四)—— 光照之光照貼圖
15. OpenGL 圖形庫的使用(十五)—— 光照之投光物
16. OpenGL 圖形庫的使用(十六)—— 光照之多光源
17. OpenGL 圖形庫的使用(十七)—— 光照之復習總結
18. OpenGL 圖形庫的使用(十八)—— 模型加載之Assimp
19. OpenGL 圖形庫的使用(十九)—— 模型加載之網格
20. OpenGL 圖形庫的使用(二十)—— 模型加載之模型
21. OpenGL 圖形庫的使用(二十一)—— 高級OpenGL之深度測試
22. OpenGL 圖形庫的使用(二十二)—— 高級OpenGL之模板測試Stencil testing
23. OpenGL 圖形庫的使用(二十三)—— 高級OpenGL之混合Blending
24. OpenGL 圖形庫的使用(二十四)—— 高級OpenGL之面剔除Face culling
25. OpenGL 圖形庫的使用(二十五)—— 高級OpenGL之幀緩沖Framebuffers
26. OpenGL 圖形庫的使用(二十六)—— 高級OpenGL之立方體貼圖Cubemaps
27. OpenGL 圖形庫的使用(二十七)—— 高級OpenGL之高級數據Advanced Data
28. OpenGL 圖形庫的使用(二十八)—— 高級OpenGL之高級GLSL Advanced GLSL
29. OpenGL 圖形庫的使用(二十九)—— 高級OpenGL之幾何著色器Geometry Shader

實例化

假設你有一個繪制了很多模型的場景,而大部分的模型包含的是同一組頂點數據,只不過進行的是不同的世界空間變換。想象一個充滿草的場景:每根草都是一個包含幾個三角形的小模型。你可能會需要繪制很多根草,最終在每幀中你可能會需要渲染上千或者上萬根草。因為每一根草僅僅是由幾個三角形構成,渲染幾乎是瞬間完成的,但上千個渲染函數調用卻會極大地影響性能。

如果我們需要渲染大量物體時,代碼看起來會像這樣:

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

如果像這樣繪制模型的大量實例(Instance),你很快就會因為繪制調用過多而達到性能瓶頸。與繪制頂點本身相比,使用glDrawArraysglDrawElements函數告訴GPU去繪制你的頂點數據會消耗更多的性能,因為OpenGL在繪制頂點數據之前需要做很多準備工作(比如告訴GPU該從哪個緩沖讀取數據,從哪尋找頂點屬性,而且這些都是在相對緩慢的CPU到GPU總線(CPU to GPU Bus)上進行的)。所以,即便渲染頂點非常快,命令GPU去渲染卻未必。

如果我們能夠將數據一次性發送給GPU,然后使用一個繪制函數讓OpenGL利用這些數據繪制多個物體,就會更方便了。這就是實例化(Instancing)

實例化這項技術能夠讓我們使用一個渲染調用來繪制多個物體,來節省每次繪制物體時CPU -> GPU的通信,它只需要一次即可。如果想使用實例化渲染,我們只需要將glDrawArraysglDrawElements的渲染調用分別改為glDrawArraysInstancedglDrawElementsInstanced就可以了。這些渲染函數的實例化版本需要一個額外的參數,叫做實例數量(Instance Count),它能夠設置我們需要渲染的實例個數。這樣我們只需要將必須的數據發送到GPU一次,然后使用一次函數調用告訴GPU它應該如何繪制這些實例。GPU將會直接渲染這些實例,而不用不斷地與CPU進行通信。

這個函數本身并沒有什么用。渲染同一個物體一千次對我們并沒有什么用處,每個物體都是完全相同的,而且還在同一個位置。我們只能看見一個物體!處于這個原因,GLSL在頂點著色器中嵌入了另一個內建變量,gl_InstanceID

在使用實例化渲染調用時,gl_InstanceID會從0開始,在每個實例被渲染時遞增1。比如說,我們正在渲染第43個實例,那么頂點著色器中它的gl_InstanceID將會是42。因為每個實例都有唯一的ID,我們可以建立一個數組,將ID與位置值對應起來,將每個實例放置在世界的不同位置。

為了體驗一下實例化繪制,我們將會在標準化設備坐標系中使用一個渲染調用,繪制100個2D四邊形。我們會索引一個包含100個偏移向量的uniform數組,將偏移值加到每個實例化的四邊形上。最終的結果是一個排列整齊的四邊形網格:

每個四邊形由2個三角形所組成,一共有6個頂點。每個頂點包含一個2D的標準化設備坐標位置向量和一個顏色向量。 下面就是這個例子使用的頂點數據,為了大量填充屏幕,每個三角形都很小:

float quadVertices[] = {
    // 位置          // 顏色
    -0.05f,  0.05f,  1.0f, 0.0f, 0.0f,
     0.05f, -0.05f,  0.0f, 1.0f, 0.0f,
    -0.05f, -0.05f,  0.0f, 0.0f, 1.0f,

    -0.05f,  0.05f,  1.0f, 0.0f, 0.0f,
     0.05f, -0.05f,  0.0f, 1.0f, 0.0f,   
     0.05f,  0.05f,  0.0f, 1.0f, 1.0f                   
};  

片段著色器會從頂點著色器接受顏色向量,并將其設置為它的顏色輸出,來實現四邊形的顏色:

#version 330 core
out vec4 FragColor;

in vec3 fColor;

void main()
{
    FragColor = vec4(fColor, 1.0);
}

到現在都沒有什么新內容,但從頂點著色器開始就變得很有趣了:

#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;
}

這里我們定義了一個叫做offsets的數組,它包含100個偏移向量。在頂點著色器中,我們會使用gl_InstanceID來索引offsets數組,獲取每個實例的偏移向量。如果我們要實例化繪制100個四邊形,僅使用這個頂點著色器我們就能得到100個位于不同位置的四邊形。

當前,我們仍要設置這些偏移位置,我們會在進入渲染循環之前使用一個嵌套for循環計算:

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

這里,我們創建100個位移向量,表示10x10網格上的所有位置。除了生成translations數組之外,我們還需要將數據轉移到頂點著色器的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]);
}

在這一段代碼中,我們將for循環的計數器i轉換為一個string,我們可以用它來動態創建位置值的字符串,用于uniform位置值的索引。接下來,我們會對offsets uniform數組中的每一項設置對應的位移向量。

現在所有的準備工作都做完了,我們可以開始渲染四邊形了。對于實例化渲染,我們使用glDrawArraysInstancedglDrawElementsInstanced。因為我們使用的不是索引緩沖,我們會調用glDrawArrays版本的函數:

glBindVertexArray(quadVAO);
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100);

glDrawArraysInstanced的參數和glDrawArrays完全一樣,除了最后多了個參數用來設置需要繪制的實例數量。因為我們想要在10x10網格中顯示100個四邊形,我們將它設置為100.運行代碼之后,你應該能得到熟悉的100個五彩的四邊形。


實例化數組

雖然之前的實現在目前的情況下能夠正常工作,但是如果我們要渲染遠超過100個實例的時候(這其實非常普遍),我們最終會超過最大能夠發送至著色器的uniform數據大小上限。它的一個代替方案是實例化數組(Instanced Array),它被定義為一個頂點屬性(能夠讓我們儲存更多的數據),僅在頂點著色器渲染一個新的實例時才會更新。

使用頂點屬性時,頂點著色器的每次運行都會讓GLSL獲取新一組適用于當前頂點的屬性。而當我們將頂點屬性定義為一個實例化數組時,頂點著色器就只需要對每個實例,而不是每個頂點,更新頂點屬性的內容了。這允許我們對逐頂點的數據使用普通的頂點屬性,而對逐實例的數據使用實例化數組。

為了給你一個實例化數組的例子,我們將使用之前的例子,并將偏移量uniform數組設置為一個實例化數組。我們需要在頂點著色器中再添加一個頂點屬性:

#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;
}

我們不再使用gl_InstanceID,現在不需要索引一個uniform數組就能夠直接使用offset屬性了。

因為實例化數組和position與color變量一樣,都是頂點屬性,我們還需要將它的內容存在頂點緩沖對象中,并且配置它的屬性指針。我們首先將(上一部分的)translations數組存到一個新的緩沖對象中:

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。這個函數告訴了OpenGL該什么時候更新頂點屬性的內容至新一組數據。它的第一個參數是需要的頂點屬性,第二個參數是屬性除數(Attribute Divisor)。默認情況下,屬性除數是0,告訴OpenGL我們需要在頂點著色器的每次迭代時更新頂點屬性。將它設置為1時,我們告訴OpenGL我們希望在渲染一個新實例的時候更新頂點屬性。而設置為2時,我們希望每2個實例更新一次屬性,以此類推。我們將屬性除數設置為1,是在告訴OpenGL,處于位置值2的頂點屬性是一個實例化數組。

如果我們現在使用glDrawArraysInstanced,再次渲染四邊形,會得到以下輸出:

這和之前的例子是完全一樣的,但這次是使用實例化數組實現的,這讓我們能夠傳遞更多的數據到頂點著色器(只要內存允許)來用于實例化繪制。

為了更有趣一點,我們也可以使用gl_InstanceID,從右上到左下逐漸縮小四邊形:

void main()
{
    vec2 pos = aPos * (gl_InstanceID / 100.0);
    gl_Position = vec4(pos + aOffset, 0.0, 1.0);
    fColor = aColor;
}

結果就是,第一個四邊形的實例會非常小,隨著繪制實例的增加,gl_InstanceID會越來越接近100,四邊形也就越來越接近原始大小。像這樣將實例化數組與gl_InstanceID結合使用是完全可行的。

如果你還是不確定實例化渲染是如何工作的,或者想看看所有代碼是如何組合起來的,你可以在這里找到程序的源代碼。

#include <glad/glad.h>  
#include <GLFW/glfw3.h>  
#include <learnopengl/shader.h>  
#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);

// settings
const unsigned int SCR_WIDTH = 1280;
const unsigned int SCR_HEIGHT = 720;

int main()
{
    // glfw: initialize and configure
    // ------------------------------
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X
#endif

    // glfw window creation
    // --------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);

    // glad: load all OpenGL function pointers
    // ---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    // configure global opengl state
    // -----------------------------
    glEnable(GL_DEPTH_TEST);

    // build and compile shaders
    // ---------------
    Shader shader("10.1.instancing.vs", "10.1.instancing.fs");
 // generate a list of 100 quad locations/translation-vectors
    // ---------------------------------------------------------
    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;
        }
    }

    // store instance data in an array buffer
    // --------------------------------------
    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);

    // set up vertex data (and buffer(s)) and configure vertex attributes
    // ------------------------------------------------------------------
    float quadVertices[] = {
        // positions     // colors
        -0.05f,  0.05f,  1.0f, 0.0f, 0.0f,
         0.05f, -0.05f,  0.0f, 1.0f, 0.0f,
        -0.05f, -0.05f,  0.0f, 0.0f, 1.0f,

        -0.05f,  0.05f,  1.0f, 0.0f, 0.0f,
         0.05f, -0.05f,  0.0f, 1.0f, 0.0f,
         0.05f,  0.05f,  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, 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 data
    glEnableVertexAttribArray(2);
    glBindBuffer(GL_ARRAY_BUFFER, instanceVBO); // this attribute comes from a different vertex buffer
    glVertexAttribPointer(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.


    // render loop
    // -----------
    while (!glfwWindowShouldClose(window))
    {
        // render
        // ------
        glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        // draw 100 instanced quads
        shader.use();
        glBindVertexArray(quadVAO);
        glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100); // 100 triangles of 6 vertices each
        glBindVertexArray(0);

        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // optional: de-allocate all resources once they've outlived their purpose:
    // ------------------------------------------------------------------------
    glDeleteVertexArrays(1, &quadVAO);
    glDeleteBuffers(1, &quadVBO);

    glfwTerminate();
    return 0;
}

// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // make sure the viewport matches the new window dimensions; note that width and 
    // height will be significantly larger than specified on retina displays.
    glViewport(0, 0, width, height);
}

雖然很有趣,但是這些例子并不是實例化的好例子。是的,它們的確讓你知道實例化是怎么工作的,但是我們還沒接觸到它最有用的一點,繪制巨大數量的相似物體。出于這個原因,我們將會在下一部分進入太空探險,見識實例化渲染真正的威力。

這些例子不是實例的好例子,不過挺有意思的。它們可以讓你對實例的工作方式有一個概括的理解,但是當繪制擁有極大數量的相同物體的時候,它極其有用,現在我們還沒有展示呢。出于這個原因,我們將在接下來的部分進入太空來看看實例渲染的威力。


小行星帶

想象這樣一個場景,在宇宙中有一個大的行星,它位于小行星帶的中央。這樣的小行星帶可能包含成千上萬的巖塊,在很不錯的顯卡上也很難完成這樣的渲染。實例化渲染正是適用于這樣的場景,因為所有的小行星都可以使用一個模型來表示。每個小行星可以再使用不同的變換矩陣來進行少許的變化。

為了展示實例化渲染的作用,我們首先會不使用實例化渲染,來渲染小行星繞著行星飛行的場景。這個場景將會包含一個大的行星模型,它可以在這里下載,以及很多環繞著行星的小行星。小行星的巖石模型可以在這里下載。

在代碼例子中,我們將使用在模型加載小節中定義的模型加載器來加載模型。

為了得到想要的效果,我們將會為每個小行星生成一個變換矩陣,用作它們的模型矩陣。變換矩陣首先將小行星位移到小行星帶中的某處,我們還會加一個小的隨機偏移值到這個偏移量上,讓這個圓環看起來更自然一點。接下來,我們應用一個隨機的縮放,并且以一個旋轉向量為軸進行一個隨機的旋轉。最終的變換矩陣不僅能將小行星變換到行星的周圍,而且會讓它看起來更自然,與其它小行星不同。最終的結果是一個布滿小行星的圓環,其中每一個小行星都與眾不同。

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

這段代碼看起來可能有點嚇人,但我們只是將小行星的x和z位置變換到了一個半徑為radius的圓形上,并且在半徑的基礎上偏移了-offset到offset。我們讓y偏移的影響更小一點,讓小行星帶更扁平一點。接下來,我們應用了縮放和旋轉變換,并將最終的變換矩陣儲存在modelMatrices中,這個數組的大小是amount。這里,我們一共生成1000個模型矩陣,每個小行星一個。

在加載完行星和巖石模型,并編譯完著色器之后,渲染的代碼看起來是這樣的:

// 繪制行星
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);
}  

我們首先繪制了行星的模型,并對它進行位移和縮放,以適應場景,接下來,我們繪制amount數量的巖石模型。在繪制每個巖石之前,我們首先需要在著色器內設置對應的模型變換矩陣。

最終的結果是一個看起來像是太空的場景,環繞著行星的是看起來很自然的小行星帶:

這個場景每幀包含1001次渲染調用,其中1000個是巖石模型。你可以在這里找到源代碼。

#include <glad/glad.h>  
#include <GLFW/glfw3.h>  
#include <stb_image.h>  
#include <glm/glm.hpp>  
#include <glm/gtc/matrix_transform.hpp>  
#include <glm/gtc/type_ptr.hpp>  
#include <[learnopengl/shader.h](https://learnopengl.com/code_viewer_gh.php?code=includes/learnopengl/shader.h)>  
#include <[learnopengl/camera.h](https://learnopengl.com/code_viewer_gh.php?code=includes/learnopengl/camera.h)> 
#include <[learnopengl/model.h](https://learnopengl.com/code_viewer_gh.php?code=includes/learnopengl/model.h)>  
#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void processInput(GLFWwindow *window);

// settings
const unsigned int SCR_WIDTH = 1280;
const unsigned int SCR_HEIGHT = 720;

// camera
Camera camera(glm::vec3(0.0f, 0.0f, 55.0f));
float lastX = (float)SCR_WIDTH / 2.0;
float lastY = (float)SCR_HEIGHT / 2.0;
bool firstMouse = true;

// timing
float deltaTime = 0.0f;
float lastFrame = 0.0f;

int main()
{
    // glfw: initialize and configure
    // ------------------------------
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X
#endif

    // glfw window creation
    // --------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
    glfwSetCursorPosCallback(window, mouse_callback);
    glfwSetScrollCallback(window, scroll_callback);

    // tell GLFW to capture our mouse
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

    // glad: load all OpenGL function pointers
    // ---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    // configure global opengl state
    // -----------------------------
    glEnable(GL_DEPTH_TEST);

    // build and compile shaders
    // -------------------------
    Shader shader("10.2.instancing.vs", "10.2.instancing.fs");
    // load models
    // -----------
    Model rock(FileSystem::getPath("resources/objects/rock/rock.obj"));
    Model planet(FileSystem::getPath("resources/objects/planet/planet.obj"));

    // generate a large list of semi-random model transformation matrices
    // ------------------------------------------------------------------
    unsigned int amount = 1000;
    glm::mat4* modelMatrices;
    modelMatrices = new glm::mat4[amount];
    srand(glfwGetTime()); // initialize random seed 
    float radius = 50.0;
    float offset = 2.5f;
    for (unsigned int i = 0; i < amount; i++)
    {
        glm::mat4 model;
        // 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 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. scale: Scale between 0.05 and 0.25f
        float scale = (rand() % 20) / 100.0f + 0.05;
        model = glm::scale(model, glm::vec3(scale));

        // 3. rotation: add random rotation around a (semi)randomly picked rotation axis vector
        float rotAngle = (rand() % 360);
        model = glm::rotate(model, rotAngle, glm::vec3(0.4f, 0.6f, 0.8f));

        // 4. now add to list of matrices
        modelMatrices[i] = model;
    }

    // render loop
    // -----------
    while (!glfwWindowShouldClose(window))
    {
        // per-frame time logic
        // --------------------
        float currentFrame = glfwGetTime();
        deltaTime = currentFrame - lastFrame;
        lastFrame = currentFrame;

        // input
        // -----
        processInput(window);

        // render
        // ------
        glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        // configure transformation matrices
        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 planet
        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);

        // draw meteorites
        for (unsigned int i = 0; i < amount; i++)
        {
            shader.setMat4("model", modelMatrices[i]);
            rock.Draw(shader);
        }     

        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    glfwTerminate();
    return 0;
}

// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);

    if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
        camera.ProcessKeyboard(FORWARD, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
        camera.ProcessKeyboard(BACKWARD, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
        camera.ProcessKeyboard(LEFT, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
        camera.ProcessKeyboard(RIGHT, deltaTime);
}

// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // make sure the viewport matches the new window dimensions; note that width and 
    // height will be significantly larger than specified on retina displays.
    glViewport(0, 0, width, height);
}

// glfw: whenever the mouse moves, this callback is called
// -------------------------------------------------------
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
    if (firstMouse)
    {
        lastX = xpos;
        lastY = ypos;
        firstMouse = false;
    }

    float xoffset = xpos - lastX;
    float yoffset = lastY - ypos; // reversed since y-coordinates go from bottom to top

    lastX = xpos;
    lastY = ypos;

    camera.ProcessMouseMovement(xoffset, yoffset);
}

// glfw: whenever the mouse scroll wheel scrolls, this callback is called
// ----------------------------------------------------------------------
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
    camera.ProcessMouseScroll(yoffset);
}

當我們開始增加這個數字的時候,你很快就會發現場景不再能夠流暢運行了,幀數也下降很厲害。當我們將amount設置為2000的時候,場景就已經慢到移動都很困難的程度了。

現在,我們來嘗試使用實例化渲染來渲染相同的場景。我們首先對頂點著色器進行一點修改:

#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;
}

我們不再使用模型uniform變量,改為一個mat4的頂點屬性,讓我們能夠存儲一個實例化數組的變換矩陣。然而,當我們頂點屬性的類型大于vec4時,就要多進行一步處理了。頂點屬性最大允許的數據大小等于一個vec4。因為一個mat4本質上是4個vec4,我們需要為這個矩陣預留4個頂點屬性。因為我們將它的位置值設置為3,矩陣每一列的頂點屬性位置值就是3、4、5和6。

接下來,我們需要為這4個頂點屬性設置屬性指針,并將它們設置為實例化數組:

// 頂點緩沖對象
unsigned int buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, amount * sizeof(glm::mat4), &modelMatrices[0], GL_STATIC_DRAW);

for(unsigned int i = 0; i < rock.meshes.size(); i++)
{
    unsigned int VAO = rock.meshes[i].VAO;
    glBindVertexArray(VAO);
    // 頂點屬性
    GLsizei vec4Size = sizeof(glm::vec4);
    glEnableVertexAttribArray(3); 
    glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)0);
    glEnableVertexAttribArray(4); 
    glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(vec4Size));
    glEnableVertexAttribArray(5); 
    glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(2 * vec4Size));
    glEnableVertexAttribArray(6); 
    glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(3 * vec4Size));

    glVertexAttribDivisor(3, 1);
    glVertexAttribDivisor(4, 1);
    glVertexAttribDivisor(5, 1);
    glVertexAttribDivisor(6, 1);

    glBindVertexArray(0);
}  

注意這里我們將MeshVAO從私有變量改為了公有變量,讓我們能夠訪問它的頂點數組對象。這并不是最好的解決方案,只是為了配合本小節的一個簡單的改動。除此之外代碼就應該很清楚了。我們告訴了OpenGL應該如何解釋每個緩沖頂點屬性的緩沖,并且告訴它這些頂點屬性是實例化數組。

接下來,我們再次使用網格的VAO,這一次使用glDrawElementsInstanced進行繪制:

// 繪制小行星
instanceShader.use();
for(unsigned int i = 0; i < rock.meshes.size(); i++)
{
    glBindVertexArray(rock.meshes[i].VAO);
    glDrawElementsInstanced(
        GL_TRIANGLES, rock.meshes[i].indices.size(), GL_UNSIGNED_INT, 0, amount
    );
}

這里,我們繪制與之前相同數量amount的小行星,但是使用的是實例渲染。結果應該是非常相似的,但如果你開始增加amount變量,你就能看見實例化渲染的效果了。沒有實例化渲染的時候,我們只能流暢渲染1000到1500個小行星。而使用了實例化渲染之后,我們可以將這個值設置為100000,每個巖石模型有576個頂點,每幀加起來大概要繪制5700萬個頂點,但性能卻沒有受到任何影響!

上面這幅圖渲染了10萬個小行星,半徑為150.0f,偏移量等于25.0f。你可以在這里找到實例化渲染的代碼。

#include <glad/glad.h>  
#include <GLFW/glfw3.h>  
#include <stb_image.h>  
#include <glm/glm.hpp>  
#include <glm/gtc/matrix_transform.hpp>  
#include <glm/gtc/type_ptr.hpp>  
#include <[learnopengl/shader.h](https://learnopengl.com/code_viewer_gh.php?code=includes/learnopengl/shader.h)>  
#include <[learnopengl/camera.h](https://learnopengl.com/code_viewer_gh.php?code=includes/learnopengl/camera.h)>  
#include <[learnopengl/model.h](https://learnopengl.com/code_viewer_gh.php?code=includes/learnopengl/model.h)>  
#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void processInput(GLFWwindow *window);

// settings
const unsigned int SCR_WIDTH = 1280;
const unsigned int SCR_HEIGHT = 720;

// camera
Camera camera(glm::vec3(0.0f, 0.0f, 155.0f));
float lastX = (float)SCR_WIDTH / 2.0;
float lastY = (float)SCR_HEIGHT / 2.0;
bool firstMouse = true;

// timing
float deltaTime = 0.0f;
float lastFrame = 0.0f;

int main()
{
    // glfw: initialize and configure
    // ------------------------------
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X
#endif

    // glfw window creation
    // --------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
    glfwSetCursorPosCallback(window, mouse_callback);
    glfwSetScrollCallback(window, scroll_callback);

    // tell GLFW to capture our mouse
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

    // glad: load all OpenGL function pointers
    // ---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    // configure global opengl state
    // -----------------------------
    glEnable(GL_DEPTH_TEST);

    // build and compile shaders
    // -------------------------
    Shader asteroidShader("[10.3.asteroids.vs](https://learnopengl.com/code_viewer_gh.php?code=src/4.advanced_opengl/10.3.asteroids_instanced/10.3.asteroids.vs)", "[10.3.asteroids.fs](https://learnopengl.com/code_viewer_gh.php?code=src/4.advanced_opengl/10.3.asteroids_instanced/10.3.asteroids.fs)"); 
    Shader planetShader("[10.3.planet.vs](https://learnopengl.com/code_viewer_gh.php?code=src/4.advanced_opengl/10.3.asteroids_instanced/10.3.planet.vs)", "[10.3.planet.fs](https://learnopengl.com/code_viewer_gh.php?code=src/4.advanced_opengl/10.3.asteroids_instanced/10.3.planet.fs)");
   // load models
    // -----------
    Model rock(FileSystem::getPath("resources/objects/rock/rock.obj"));
    Model planet(FileSystem::getPath("resources/objects/planet/planet.obj"));

    // generate a large list of semi-random model transformation matrices
    // ------------------------------------------------------------------
    unsigned int amount = 100000;
    glm::mat4* modelMatrices;
    modelMatrices = new glm::mat4[amount];
    srand(glfwGetTime()); // initialize random seed 
    float radius = 150.0;
    float offset = 25.0f;
    for (unsigned int i = 0; i < amount; i++)
    {
        glm::mat4 model;
        // 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 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. scale: Scale between 0.05 and 0.25f
        float scale = (rand() % 20) / 100.0f + 0.05;
        model = glm::scale(model, glm::vec3(scale));

        // 3. rotation: add random rotation around a (semi)randomly picked rotation axis vector
        float rotAngle = (rand() % 360);
        model = glm::rotate(model, rotAngle, glm::vec3(0.4f, 0.6f, 0.8f));

        // 4. now add to list of matrices
        modelMatrices[i] = model;
    }

    // configure instanced array
    // -------------------------
    unsigned int buffer;
    glGenBuffers(1, &buffer);
    glBindBuffer(GL_ARRAY_BUFFER, buffer);
    glBufferData(GL_ARRAY_BUFFER, amount * sizeof(glm::mat4), &modelMatrices[0], GL_STATIC_DRAW);

    // set transformation matrices as an instance vertex attribute (with divisor 1)
    // note: we're cheating a little by taking the, now publicly declared, VAO of the model's mesh(es) and adding new vertexAttribPointers
    // normally you'd want to do this in a more organized fashion, but for learning purposes this will do.
    // -----------------------------------------------------------------------------------------------------------------------------------
    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);
    }

    // render loop
    // -----------
    while (!glfwWindowShouldClose(window))
    {
        // per-frame time logic
        // --------------------
        float currentFrame = glfwGetTime();
        deltaTime = currentFrame - lastFrame;
        lastFrame = currentFrame;

        // input
        // -----
        processInput(window);

        // render
        // ------
        glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        // configure transformation matrices
        glm::mat4 projection = glm::perspective(glm::radians(45.0f), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 1000.0f);
        glm::mat4 view = camera.GetViewMatrix();
        asteroidShader.use();
        asteroidShader.setMat4("projection", projection);
        asteroidShader.setMat4("view", view);
        planetShader.use();
        planetShader.setMat4("projection", projection);
        planetShader.setMat4("view", view);
        
        // draw planet
        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));
        planetShader.setMat4("model", model);
        planet.Draw(planetShader);

        // draw meteorites
        asteroidShader.use();
        asteroidShader.setInt("texture_diffuse1", 0);
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, rock.textures_loaded[0].id); // note: we also made the textures_loaded vector public (instead of private) from the model class.
        for (unsigned int i = 0; i < rock.meshes.size(); i++)
        {
            glBindVertexArray(rock.meshes[i].VAO);
            glDrawElementsInstanced(GL_TRIANGLES, rock.meshes[i].indices.size(), GL_UNSIGNED_INT, 0, amount);
            glBindVertexArray(0);
        }

        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    glfwTerminate();
    return 0;
}

// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);

    if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
        camera.ProcessKeyboard(FORWARD, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
        camera.ProcessKeyboard(BACKWARD, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
        camera.ProcessKeyboard(LEFT, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
        camera.ProcessKeyboard(RIGHT, deltaTime);
}

// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // make sure the viewport matches the new window dimensions; note that width and 
    // height will be significantly larger than specified on retina displays.
    glViewport(0, 0, width, height);
}

// glfw: whenever the mouse moves, this callback is called
// -------------------------------------------------------
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
    if (firstMouse)
    {
        lastX = xpos;
        lastY = ypos;
        firstMouse = false;
    }

    float xoffset = xpos - lastX;
    float yoffset = lastY - ypos; // reversed since y-coordinates go from bottom to top

    lastX = xpos;
    lastY = ypos;

    camera.ProcessMouseMovement(xoffset, yoffset);
}

// glfw: whenever the mouse scroll wheel scrolls, this callback is called
// ----------------------------------------------------------------------
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
    camera.ProcessMouseScroll(yoffset);
}

在某些機器上,10萬個小行星可能會太多了,所以嘗試修改這個值,直到達到一個你能接受的幀率。

可以看到,在合適的環境下,實例化渲染能夠大大增加顯卡的渲染能力。正是出于這個原因,實例化渲染通常會用于渲染草、植被、粒子,以及上面這樣的場景,基本上只要場景中有很多重復的形狀,都能夠使用實例化渲染來提高性能。

后記

本篇結束,下一篇與抗鋸齒有關。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容