多实例渲染

  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,,,);

  生成顶点数组对象和缓存对象照旧,关键在于glDrawArraysInstanced的调用,在这里我们传入10表示绘制10次实例。

  下面是顶点着色器:

#version  core

layout(location = ) in vec3 iPos;
layout(location = ) in vec3 iColor;
uniform mat4 model;
uniform mat4 view;
uniform mat4 proj; out vec4 fColor; void main()
{
fColor = vec4(iColor,);
vec3 pos = iPos;
pos = pos + vec3(0.1,0.2f,-0.1) * gl_InstanceID;
gl_Position= proj * view * model * vec4(pos,);
}

  这个着色器中关键在于gl_InstanceID这个内置变量,这个内置变量是一个整数,表示当前实例数,它从0开始计数。gl_InstanceID一直存在于顶点着色器中,就算不使用多实例渲染,此时它的值为0。所以在顶点着色器中可使用gl_InstanceID来做索引,引用一些uniform的数组元素。在上面的着色器例子中,我们使用gl_Instanced来对物体的位置进行移动,做一个偏差。当然我们也可以传入一个uniform的数组,使用gl_InstanceID来引用,如

#version  core

layout(location = ) in vec3 iPos;
layout(location = ) in vec3 iColor; uniform mat4 model;
uniform mat4 view;
uniform mat4 proj; uniform vec3 offset[]; 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=;i<;++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  core

layout(location = ) in vec3 iPos;
layout(location = ) 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(,);

  这个顶点着色器中有一个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(, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, amount * sizeof(glm::mat4), &modelMatrices[], GL_STATIC_DRAW); for(unsigned int i = ; i < rock.meshes.size(); i++)
{
unsigned int VAO = rock.meshes[i].VAO;
glBindVertexArray(VAO);
// 顶点属性
GLsizei vec4Size = sizeof(glm::vec4);
glEnableVertexAttribArray();
glVertexAttribPointer(, , GL_FLOAT, GL_FALSE, * vec4Size, (void*));
glEnableVertexAttribArray();
glVertexAttribPointer(, , GL_FLOAT, GL_FALSE, * vec4Size, (void*)(vec4Size));
glEnableVertexAttribArray();
glVertexAttribPointer(, , GL_FLOAT, GL_FALSE, * vec4Size, (void*)( * vec4Size));
glEnableVertexAttribArray();
glVertexAttribPointer(, , GL_FLOAT, GL_FALSE, * vec4Size, (void*)( * vec4Size)); glVertexAttribDivisor(, );
glVertexAttribDivisor(, );
glVertexAttribDivisor(, );
glVertexAttribDivisor(, ); glBindVertexArray();
}

  这个段代码参照: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-多实例渲染的更多相关文章

  1. Linux OpenGL 实践篇-16 文本绘制

    文本绘制 本文主要射击Freetype的入门理解和在OpenGL中实现文字的渲染. freetype freetype的官网,本文大部分内容参考https://www.freetype.org/fre ...

  2. Linux OpenGL 实践篇-5 纹理

    纹理 在之前的实践中,我们所渲染的物体的表面颜色都是纯色或者根据顶点位置计算出的一个颜色,这种方式在表现物体细节方面是比较吃资源的,因为我们每增加一个细节,我们就需要定义更多的顶点及其属性.所以美术人 ...

  3. Linux OpenGL 实践篇-4 坐标系统

    OpenGL中顶点经过顶点着色器后会变为标准设备坐标系.标准设备坐标系的各坐标的取值范围是[-1,1],超过这个范围的点将会被剔除.而这个变换的过程可描述为顶点在几个坐标系统的变换,这几个坐标系统为: ...

  4. Linux OpenGL 实践篇-3 绘制三角形

    本次实践是绘制两个三角形,重点理解顶点数组对象和OpenGL缓存的使用. 顶点数组对象 顶点数组对象负责管理一组顶点属性,顶点属性包括位置.法线.纹理坐标等. OpenGL缓存 OpenGL缓存实质上 ...

  5. Linux OpenGL 实践篇-2 创建一个窗口

    OpenGL 作为一个图形接口,并没有包含窗口的相关内容,但OpenGL使用必须依赖窗口,即必须在窗口中绘制.这就要求我们必须了解一种窗口系统,但不同的操作系统提供的创建窗口的API都不相同,如果我们 ...

  6. Linux OpenGL 实践篇-15-图像数据操作

    OpenGL图像数据操作 之前的实践中,我们在着色器中的输入输出都是比较固定的.比如在顶点或片元着色器中,顶点属性的输入和帧缓存的颜色值:虽然我们可以通过纹理或者纹理缓存对象(TBO)来读取任意的内存 ...

  7. Linux OpenGL 实践篇-13-geometryshader

    几何着色器 几何着色器是位于图元装配和片元着色器之前的一个着色器阶段,是一个可选阶段.它的输入是一个图元的完整的顶点信息,通常来自于顶点着色器,但如果细分计算着色器启用的话,那输入则是细分计算着色器的 ...

  8. Linux OpenGL 实践篇-12-procedural-texturing

    程序式纹理 简单的来说程序式纹理就是用数学公式描述物体表面的纹路 .而实现这个过程的着色器我们称之为程序纹理着色器,通常在这类着色器中我们能使用的输入信息也就是顶点坐标和纹理坐标. 程序式纹理的优点 ...

  9. Linux OpenGL 实践篇-11-shadow

    OpenGL 阴影 在三维场景中,为了使场景看起来更加的真实,通常需要为其添加阴影,OpenGL可以使用很多种技术实现阴影,其中有一种非常经典的实现是使用一种叫阴影贴图的实现,在本节中我们将使用阴影贴 ...

随机推荐

  1. 做一名开源社区的扫地僧——从Bug report到Google Summer of Code(GSoC):从200个bug到5000美金

    今年的软件自由日(SFD),我在广州Linux用户组的线下活动上做了一个分享,主题叫做<做一名开源社区的扫地僧(上)>.我把演讲的内容重新整理扩充, 写出了文字版, 希望可以跟更多朋友分享 ...

  2. 前端HTML 与css 整理(未完)

    HTML 中的标签存放于文本文件中 需要按照以下固定的文档结构组织:<!DOCTYPE HTML><html> <head>头部相关信息 </head> ...

  3. Metatable In Lua 浅尝辄止

    http://www.cnblogs.com/simonw/archive/2007/01/17/622032.html 什么是Metatable Lua中Metatable这个概念, 国内将他翻译为 ...

  4. HTML学习笔记(三)样式CSS

    1.样式 外部样式表(通过css文件定义样式,整体样式) 当样式需要被应用到很多页面的时候,使用外部样式表,在 head 部分<link>. <head> <link r ...

  5. MFC对话框程序 屏蔽ESC和ENTER键关闭对话框的方法

    http://blog.csdn.net/xgx198831/article/details/6713651 MFC对话框程序  屏蔽ESC和ENTER键关闭对话框的方法 或许还有其它更好的办法,但下 ...

  6. bzoj 4541: [Hnoi2016]矿区【平面图转对偶图+生成树】

    首先平面图转对偶图,大概思路是每条边存正反,每个点存出边按极角排序,然后找每条边在它到达点的出边中极角排序的下一个,这样一定是这条边所属最小多边形的临边,然后根据next边找出所有多边形,用三角剖分计 ...

  7. Luogu P1514引水入城【搜索】 By cellur925

    题目传送门 这道题开始看好像并没有什么思路,和搜索好像也并没有什么关系.但是我们手玩下样例就会发现,思路其实就三句话:(写这道题的时候在代码里写的) //我们想知道从第1行的每列往下到干旱区的范围 / ...

  8. 条件运算符?:接受三个操作数,是C#中唯一的三元运算符(转)

    int i = 10; int j = i == 10 ? 1 : 2; //转换成if选择结果如下 if (i == 10) { j = 1; } else { j = 2; } 需要根据还可以嵌套 ...

  9. js中的同步和异步的个人理解(转)

    你应该知道,javascript语言是一门“单线程”的语言,不像java语言,类继承Thread再来个thread.start就可以开辟一个线程,所以,javascript就像一条流水线,仅仅是一条流 ...

  10. 调试PHP

    echo "<br/>"; print_r($array);