Linux OpenGL 实践篇-14-多实例渲染

多实例渲染

  本实践的源代码:https://github.com/xin-lover/opengl-learn/tree/master/chapter-13-geometryshader

  OpenGL的多实例渲染是一种连续执行多条相同的渲染命令的方法,并且每条命令产生的结果都有轻微的差异,通常用于渲染大量的几何物体。

  比如一个典型场景就是渲染行星带,我们需要渲染数以万记的星球,如果我们使用常规的做法,渲染的过程应该是是:

  1. glBindVertexArray;
  2. glDrawArrays或glDrawElements,

  这种方式非常容易达到计算机的性能瓶颈,哪怕是渲染的物体是最简单的面片。因为在绘制的整个过程中,绘制物体的时间其实非常的短,而渲染物体的准备工作时间是比较长的,即调用glBindVertexArray和glDrawArrays做的工作,如准备顶点数据,指定GPU从哪个缓冲区读取数据,GPU从哪找顶点属性等,而且这些工作都是在CPU到GPU的总线(CPU-GPU bus)上进行的,所以瓶颈往往是这不是GPU渲染的速度(这也是大多数引擎都会提供一个batch的技术来优化渲染性能的原因)。

  OpenGL的多实例渲染就是针对这种情况出现的。根据上述的情况我们知道要想提高渲染的效率,关键在于减少OpenGL API绘制指令的调用。基于这种思路,我们可以在一次绘制指令中传输尽量多的传输顶点数据,减少绘制指令的调用,即传输一次数据可以绘制多个物体。而这就是OpenGL中多实例渲染的完成的功能。

  OpenGL的多实例渲染最基本的两个渲染API是glDrawArraysInstanced和glDrawElementsInstanced。其它的如glDrawArraysInstancedBaseInstance的API都可以认为是基于这两个API实现的。

  对比以下glDrawArrays和glDrawArraysInstanced的函数原型:

  void glDrawArrays(GLenum mode, GLint first, GLsizei count);
  void glDrawArraysInstanced(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四个位置,使用glVertexAttribDivisor的时候也要调用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://www.cnblogs.com/xin-lover/p/9147905.html