1. 程式人生 > >OpenGL學習腳印:建立更多的例項(instancing object)

OpenGL學習腳印:建立更多的例項(instancing object)

寫在前面
前面我們學習了模型載入的相關內容,併成功載入了模型,令人十分興奮。那時候載入的是少量的模型,如果需要載入多個模型,就需要考慮到效率問題了,例如下圖所示的是載入了400多個納米戰鬥服機器人的效果圖:

更多的納米戰鬥服

渲染一個模型更多的例項,需要使用到例項化技術,就是本節要介紹的instancing object方法。本節示例程式碼均可以從我的github下載

渲染多個例項的方法

要渲染多個例項,基本的想法就是,在主程式中使用迴圈,在不同位置繪製多個物體,虛擬碼如下所示:

   for(GLuint i = 0; i < instanceCount; ++i)
   {
      // 分別設定每個物體的模型變換矩陣 model matrix
// glDrawArrays(GL_TRIANGLES, ...) }

這種方式存在的缺點是,當要渲染多個模型的例項時,需要多次呼叫glDraw這類命令,而這類命令從CPU–>GPU是需要花費時間的,因為使用繪製命令時OpenGL需要做一些工作,例如通知GPU從哪個buffer裡面讀取資料。雖然GPU繪圖很快,但是CPU–>GPU的命令傳送,當量比較大時還是會成為瓶頸。

因此OpenGL提供了glDrawArrays和glDrawElements的繪製例項版本,分別對應為glDrawArraysInstanced和glDrawElementsInstanced 。例項版本的函式,多了一個引數,就是最後一個指定渲染多少個例項的引數。

下面以一個簡單的繪製多個矩形的例子作為引例,開始熟悉繪製多個例項。

使用多個uniform傳遞例項資料

假設我們要繪製100個矩形,在頂點著色器中,我們使用一個uniform陣列:

   #version 330 core

layout(location = 0) in vec2 position;
layout(location = 1) in vec3 color;

uniform vec2 offsets[100]; // 每個例項的位移量

out vec3 fColor;

void main()
{
    vec2 offset = offsets[gl_InstanceID]; // 通過gl_InstanceID索引每個例項的位移量
gl_Position = vec4(position + offset, 0.5f, 1.0f); fColor = color; }

通過gl_InstanceID來索引每個例項,而在主程式中,我們通過迴圈設定這個uniform陣列的內容:

   //準備多個例項的位移量資料
glm::vec2 translations[100];
int index = 0;
GLfloat offset = 0.1f;
for (GLint y = -10; y < 10; y += 2)
{
for (GLint x = -10; x < 10; x += 2)
{
    glm::vec2 translation;
    translation.x = (GLfloat)x / 10.0f + offset;
    translation.y = (GLfloat)y / 10.0f + offset;
    translations[index++] = translation;
}
}
// 接著 向shader傳遞這100個translate uniform

最後通過例項版本函式繪製多個矩形:

shader.use();
glBindVertexArray(quadVAOId);
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100); // 使用instance方法繪製

得到的效果如下圖所示:

繪製多個矩形

我們看到使用這個方法,確實渲染了多個矩形,但存在的問題時GLSL中支援的uniform受到限制,可以使用 GL_MAX_VERTEX_UNIFORM_COMPONENTS等列舉通過glGetIntegerv​函式查詢。一般情況下uniforms陣列也夠用,但是對於需要例項比較多的情形,這種方案變得不合適。

使用instance array 傳遞例項資料

同頂點屬性中位置、紋理座標等其他屬性一樣,我們可以通過VBO來充當一個instance array,傳遞每個例項的資料。一般地頂點屬性,當頂點著色器執行時需要獲取每個頂點的這些屬性資訊,而充當instance array的頂點屬性需要每個例項更新一次。這是instance array與普通頂點屬性之間的差別。

建立一個instance array的包括兩個步驟,第一步同普通頂點屬性一樣,建立VBO,填充資料;第二步是通知OpenGL如何解析VBO中的資料。在頂點著色器中,我們定義一個layout=2表示這個instance array,如下:

#version 330 core

layout(location = 0) in vec2 position;
layout(location = 1) in vec3 color;
layout(location = 2) in vec2 offset; // 通過VBO傳遞位移量

// uniform vec2 offsets[100];  // 不再使用

out vec3 fColor;

void main()
{
    gl_Position = vec4(position + offset, 0.5f, 1.0f);
    fColor = color;
}

在主程式中,建立VBO,填充translations陣列的資料,如下:

GLuint instanceVBOId;
glGenBuffers(1, &instanceVBOId);
glBindVertexArray(quadVAOId);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBOId);
glBufferData(GL_ARRAY_BUFFER, sizeof(glm::vec2) * 100, &translations[0], GL_STATIC_DRAW);

並通知OpenGL解析這個VBO資料的方式:

glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 0, NULL);
glEnableVertexAttribArray(2);
glVertexAttribDivisor(2, 1); // 注意這裡 指定1表示每個例項更新一次資料
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);

這裡關鍵是使用glVertexAttribDivisor來指定資料更新方式,第一個引數2表示layout索引,第二個引數指定頂點屬性的更新方式,預設是0表示著色器每次執行時更新屬性資料,填寫1表示每個例項更新一次屬性資料,填寫2則表示每2個例項更新一次屬性資料,依次類推。上面填寫1則通知了OpenGL這是一個instance array,每個例項更新一次資料。

執行上述程式碼,我們得到的效果與上面相同,當設定:

glVertexAttribDivisor(2, 4);

每4個例項更新一次資料時,我們將會得到100 / 4 =25個矩形,因為每4個矩形的模型變換矩陣相同,因此放在了同一個位置,重合了,效果如下圖所示:

divisor=4

上面是一個簡單的引例,下面我們通過兩個案例,深入對比下instance array方式的效能差別。

繪製行星帶

通過載入一個行星模型和石頭模型來模擬一個行星帶,這裡我們通過下面的函式,來構造一個石頭模型隨機環繞行星的模型變換矩陣:

 // 這裡通過隨機方式 構造多個石頭模型的模型變換矩陣
   void prepareInstanceMatrices(std::vector<glm::mat4>& modelMatrices, const int amount)
{
srand(glfwGetTime()); // 初始化隨機數的種子
GLfloat radius = 50.0;
GLfloat offset = 2.5f;
for (GLuint i = 0; i < amount; i++)
{
glm::mat4 model;
// 1. 平移
GLfloat angle = (GLfloat)i / (GLfloat)amount * 360.0f;
GLfloat displacement = (rand() % (GLint)(2 * offset * 100)) / 100.0f - offset;
GLfloat x = sin(angle) * radius + displacement;
displacement = (rand() % (GLint)(2 * offset * 100)) / 100.0f - offset;
GLfloat y = displacement * 0.4f; 
displacement = (rand() % (GLint)(2 * offset * 100)) / 100.0f - offset;
GLfloat z = cos(angle) * radius + displacement;
model = glm::translate(model, glm::vec3(x, y, z));

// 2. 縮放 在 0.05 和 0.25f 之間
GLfloat scale = (rand() % 20) / 100.0f + 0.05;
model = glm::scale(model, glm::vec3(scale));

// 3. 旋轉
GLfloat rotAngle = (rand() % 360);
model = glm::rotate(model, rotAngle, glm::vec3(0.4f, 0.6f, 0.8f));

// 4. 新增作為模型變換矩陣
modelMatrices.push_back(model);
}
}

上面隨機方式構造變換矩陣的計算細節,可以不用深究,我們需要重點理解的是對比使用普通方式和使用instance array的效率問題。

不使用instance array的繪製方式

構造了多個例項的矩陣後,我們使用普通的繪製方式如下:

// 這裡填寫場景繪製程式碼
shader.use();
glUniformMatrix4fv(glGetUniformLocation(shader.programId, "projection"),
1, GL_FALSE, glm::value_ptr(projection));
glUniformMatrix4fv(glGetUniformLocation(shader.programId, "view"),
1, GL_FALSE, glm::value_ptr(view));
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));
glUniformMatrix4fv(glGetUniformLocation(shader.programId, "model"),
1, GL_FALSE, glm::value_ptr(model));

planet.draw(shader); // 先繪製行星

// 繪製多個小行星例項
for (std::vector<glm::mat4>::size_type i = 0; i < modelMatrices.size(); ++i)
{
glUniformMatrix4fv(glGetUniformLocation(shader.programId, "model"),
1, GL_FALSE, glm::value_ptr(modelMatrices[i]));
rock.draw(shader);
}

使用instance array的繪製方式

同上面使用的instance array有些不同,這裡使用的instance array是mat4型別的矩陣,因為頂點屬性允許的最大資料為vec4,因此我們需要使用4 * vec4表示這個mat4型別的instance array。在頂點著色器中定義這個mat4 instance array如下:

   #version 330 core

layout(location = 0) in vec3 position;
layout(location = 1) in vec2 textCoord;
layout(location = 2) in vec3 normal;
layout(location = 3) in mat4 instanceMatrix;  // 頂點屬性最多vec4 輸入 實際上有4個vec4輸入構造這個mat4

uniform mat4 projection;
uniform mat4 view;

out vec2 TextCoord;

void main()
{
    gl_Position = projection * view * instanceMatrix * vec4(position, 1.0);
    TextCoord = textCoord;
}

同時我們還需要在主程式中向著色器傳遞這個instance array。之前設計的mesh.h類,需要少量修改,允許獲取mesh相關資訊,修改後的mesh.h類。我們這裡不去大量修改mesh類,採用的策略是為每個mesh使用這個instance array,實現如下:

   void prepareInstanceMatrices(std::vector<glm::mat4>& modelMatrices, 
    const int amount, const Model& instanceModel)
{
    // 構造modelMatrices 同上面函式實現
    // 建立instance array
    GLuint modelMatricesVBOId;
    glGenBuffers(1, &modelMatricesVBOId);
    glBindBuffer(GL_ARRAY_BUFFER, modelMatricesVBOId);
    glBufferData(GL_ARRAY_BUFFER, sizeof(glm::mat4) * amount, &modelMatrices[0], GL_STATIC_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    // 為模型裡每個mesh 傳遞model matrix
    // 用4個vec4傳遞這個mat4型別
    const std::vector<Mesh>& meshes = instanceModel.getMeshes();
    for (std::vector<Mesh>::size_type i = 0; i < meshes.size(); ++i)
    {
        glBindVertexArray(meshes[i].getVAOId());
        glBindBuffer(GL_ARRAY_BUFFER, modelMatricesVBOId);
        // 第一列
        glEnableVertexAttribArray(3);
        glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 
            4 * sizeof(glm::vec4), (GLvoid*)0);
        // 第二列
        glEnableVertexAttribArray(4);
        glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, 
            4 * sizeof(glm::vec4), (GLvoid*)(sizeof(glm::vec4)));
        // 第三列
        glEnableVertexAttribArray(5);
        glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, 
            4 * sizeof(glm::vec4), (GLvoid*)(2 * sizeof(glm::vec4)));
        // 第四列
        glEnableVertexAttribArray(6);
        glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE,
            4 * sizeof(glm::vec4), (GLvoid*)(3 * sizeof(glm::vec4)));

        // 注意這裡需要設定例項資料更新選項 指定1表示 每個例項更新一次
        glVertexAttribDivisor(3, 1);
        glVertexAttribDivisor(4, 1);
        glVertexAttribDivisor(5, 1);
        glVertexAttribDivisor(6, 1);

        glBindVertexArray(0);
    }
}

這個地方稍微有點繞,關鍵一點就是每個mesh都包含了這個modelMatrices資料,因此每個mesh繪製三角形時,都會在每個例項上更新modelMatrix,從而整體上繪製出的模型也用了這些模型變換矩陣。

上面繪製的效果如下圖所示:

行星帶效果

使用上面兩種方法渲染包含1000, 10000, 100000個石頭模型的行星帶,在NVIDIA Graphics 上粗略的一個對比資料(這不是基準測試結果),如下表1所示:

例項數目 普通繪製 instancing方法
1000 0.05s 0.01s
10,000 0.45s 0.12s
100,000 4.0s 1.25s

這個計時是通過glfwGetTime來實現的,更科學的對比可能是使用幀率,暫時不細究這個問題了。通過對比,可以看到使用instance array渲染多個例項速度比普通方式快了4到5倍。

渲染更多的納米戰鬥服機器人

再給出一個使用instance方法,繪製多個機器人的方法,我們指定了要繪製的機器人數量,然後平鋪在鋼鐵紋理上。繪製9個機器人的效果如下圖所示:

9個機器人

121個機器人效果如下圖所示:

121個機器人

渲染的441個機器人效果如下圖所示:

441個機器人

你可以根據需要將機器人的擺放成其他形式,例如同心圓、心形圖案等,可以自己玩會兒了。

最後的說明

本節學習了instance例項的方法,並對比了普通渲染方式和它在效能上的差別。實際應用中,instance例項一般應用在草地、樹木等模型上面,來構成遊戲場景中很好的佈景。