1. 程式人生 > >Linux OpenGL 實踐篇-14-多實例渲染

Linux OpenGL 實踐篇-14-多實例渲染

問題 location 正常 cpu 理解 rda osi num use

多實例渲染

  OpenGL的多實例渲染是一種連續執行多條相同的渲染命令的方法,並且每條命令產生的結果都有輕微的差異,通常用於渲染大量的幾何物體。

  設想一個場景,比如太空,我們需要渲染數以萬記的星球,如果我們使用常規的做法,渲染的過程應該是是:繪制第一個星球glBindVertexArray——glDrawArrays或glDrawElements,然後使用同樣的流程繪制其它的星球。但這種方式非常容易達到計算機的性能瓶頸,就算是渲染的物體是最簡單的面片,因為在繪制的整個過程中,繪制物體的時間其實非常的短,而渲染物體的準備工作時間是比較長的,即調用glBindVertexArray和glDrawArrays做的工作,如準備頂點數據,指定GPU從哪個緩沖區讀取數據,GPU從哪找頂點屬性等,而且這些工作都是在CPU到GPU的總線(CPU-GPU bus)上進行的,所以就算是GPU渲染的速度足夠快,但調用繪制指令次數過多,就會影響渲染的效率。

  OpenGL的多實例渲染就是針對這種情況出現的。根據上述的情況我們知道要想提高渲染的效率,關鍵在於減少OpenGL API繪制指令的調用。基於這種思路,我們可以在一次繪制指令中傳輸盡量多的傳輸頂點數據,減少繪制指令的調用,即傳輸一次數據可以繪制多個物體。而這就是OpenGL中多實例渲染的完成的功能。

  OpenGL的多實例渲染最基本的兩個渲染API是glDrawArraysInstanced和glDrawElementsInstanced。其它的如glDrawArraysInstancedBaseInstance的API都可以認為是基於這兩個API實現的。

  對比以下glDrawArrays和glDrawArraysInstanced:

  void glDrawArrays(GLenum mode, GLint first, GLsizei count);

  void glDrawArrays(GLenum mode, GLint first GLsizei count, GLzsizei primCount);

  glDrawArraysInstanced多了一個primCount的參數,即渲染實例的個數。當OpenGL執行這個函數的時候實際上它會執行glDrawArrays的primCount次拷貝,每次的mode,first,count都是直接傳入的。

  下面我們看一個多實例渲染的簡單例子:

       glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

       glBindVertexArray(vao);
       dShader
->Use(); glDrawArraysInstanced(GL_TRIANGLES,0,6,10);

  生成頂點數組對象和緩存對象照舊,關鍵在於glDrawArraysInstanced的調用,在這裏我們傳入10表示繪制10次實例。

  下面是頂點著色器:

#version 330 core

layout(location = 0) in vec3 iPos;
layout(location = 1) in vec3 iColor;
uniform mat4 model;
uniform mat4 view;
uniform mat4 proj;

out vec4 fColor;

void main()
{
        fColor = vec4(iColor,1);
        vec3 pos = iPos;
        pos = pos + vec3(0.1,0.2f,-0.1) * gl_InstanceID;
        gl_Position= proj * view * model * vec4(pos,1);
}                                                               

  這個著色器中關鍵在於gl_InstanceID這個內置變量,這個內置變量是一個整數,表示當前實例數,它從0開始計數。gl_InstanceID一直存在於頂點著色器中,就算不使用多實例渲染,此時它的值為0。所以在頂點著色器中可使用gl_InstanceID來做索引,引用一些uniform的數組元素。在上面的著色器例子中,我們使用gl_Instanced來對物體的位置進行移動,做一個偏差。當然我們也可以傳入一個uniform的數組,使用gl_InstanceID來引用,如

#version 330 core

layout(location = 0) in vec3 iPos;
layout(location = 1) in vec3 iColor;

uniform mat4 model;
uniform mat4 view;
uniform mat4 proj;

uniform vec3 offset[10];

out vec3 fColor;

void main()
{
        fColor = iColor;                                       
        vec3 pos = iPos + offset[gl_InstanceID];
        gl_Position = proj * view * model * vec4(pos,1.0);
}

  我們聲明了一個offset數組,然後在應用程序中使用如下代碼為offset數組賦值。

for(int i=0;i<10;++i)
{
    stringstream ss;
    ss >> i;
    GLint loc = glGetUniformLocation(program,("offset[ "+ ss.str() + "]").c_str());
    glUniform2f(loc,offset.x,offset.y);
}

  效果如圖:

  技術分享圖片

多實例的頂點屬性

  在上面的例子中我們使用了offset數組和gl_InstanceID來渲染實例,但這種方法有個問題就是數組的大小非常容易達到uniform數據大小的上限。為此,我們可以使用另一種方法,就是多實例的頂點屬性,它和正規的頂點屬性是類似的,在頂點著色器中的聲明和數據配置方法完全一致。唯一的區別就是頂點屬性針對的是單一頂點,而多實例頂點屬性針對的是一個圖元實例。簡單的理解就是頂點著色器的輸入正常情況是一個頂點屬性對應一個頂點,而所實例的頂點屬性是一個屬性對以一個圖元(圖元中所有的頂點的這一條屬性共用同一個數據),即每個實例更新一次這個屬性的數據。為了實現這個功能,我們需要一個函數:

  glVertexAttribDivisor(GLuint index,GLuint divisor);

  這個函數是用於設置頂點著色器中index索引的頂點屬性如何分配值到每一個實例的。divisor表示每divisor個實例更新一次頂點屬性。如果divisor的值是0,表示多實例特性被禁用。下面我們用一個頂點著色器的例子來說明;

#version 330 core

layout(location = 0) in vec3 iPos;
layout(location = 1) in vec3 iColor;

uniform mat4 model;
uniform mat4 view;
uniform mat4 proj;

out vec3 fColor;

void main()
{
        fColor = iColor;
        vec3 pos = iPos;                                       
        gl_Position = proj * view * model * vec4(pos,1.0);
}       

  應用程序調用:

glVertexAttribDivisor(1,1);

  這個頂點著色器中有一個iColor的屬性,索引是1,按正常的頂點屬性來理解的話,這個屬性每個頂點更新一次。調用glVertexDisivor設置多實例特性後,iColor屬性是每個實例(每三個頂點即一個三角形)變換一次。第一個1表示索引,第二個1表示每個實例更新一次iColor數據。

  效果跟上面的一致,但這個時候我們沒有使用gl_InstanceID。不過在使用所實例頂點屬性的時候有一點要註意,一個頂點屬性數據最大等於一個vec4,所以一個mat4會占用多個索引位置,比如layout(location=1) in mat4 m, 這個m會占用1,2,3,4四個位置,使用glUniform4fv的時候也要調用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);
} 

  這個段代碼參照:https://learnopengl-cn.github.io/04%20Advanced%20OpenGL/10%20Instancing/

  本實踐的源代碼:https://github.com/xin-lover/opengl-learn/tree/master/chapter-13-geometryshader

Linux OpenGL 實踐篇-14-多實例渲染