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

Posted xin_l12

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux OpenGL 实践篇-14-多实例渲染相关的知识,希望对你有一定的参考价值。

多实例渲染

  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-多实例渲染的主要内容,如果未能解决你的问题,请参考以下文章

实践解析 | 如何用 OpenGL 实现跨平台应用高效渲染

Linux OpenGL 实践篇-9 模型

Android MediaCodec+OpenGL视频编解码实践笔记

Android MediaCodec+OpenGL视频编解码实践笔记

实例渲染 OpenGL

如何通过实例渲染将每个实例数据(例如定位)传递给 OpenGL 3.2 中的着色器?