目录
查询功能——收集OpenGL管线相关信息
GPU查询对象是否被绘制在可视范围内
准备查询
void glGenQueries(GLsizei n, GLuint *ids);
创建查询对象(或查询对象数组)n为个数,ids是查询对象指针。
GLuint one_query;
GLuint ten_queries[10];
glGenQueries(1, &one_query);
glGenQueries(10, ten_queries);
若创建失败内容将是0,可安全校验是否创建成功,失败则通过glGetError查询原因。
销毁查询对象
void glDeleteQueries(GLsizei n, const GLuint *ids);
glDeleteQueries(10, ten_queries);
glDeleteQueries(1, &one_query);
发出查询
glBeginQuery(GL_SAMPLES_PASSED, one_query);
开始计数工作:GL_SAMPLES_PASSED准备将要询问片段通过深度测试的数量,one_query查询对象。OpenGL会在这段时间内的计数所有片段是否通过深度测试,会将结果存放在查询对象one_query上。
结束计数工作:
glEndQuery(GL_SAMPLES_PASSED);
取回查询结果
glGetQueryObjectuiv(the_query, GL_QUERY_RESULT, &result);
在glBeginQuery和glEndQuery之间渲染物体,再用glGetQueryObjectuiv查询结果放于result,若为0则物体不可见,否则可见。上述方法取回查询结果时是需要等待渲染物体的工作完成才取回的,有可能已经可以取得我们所需的查询结果了,但它仍然会等待渲染工作完全结束才取回,为此可用如下方法能时刻检查是否已经准备好查询结果,而不必等到完全结束工作才取回。
glGetQueryObjectuiv(the_query, GL_QUERY_RESULT_AVAILABLE, &result);
如果OpenGL还没有准备好结果,则result是GL_FALSE,否则返回GL_TRUE代表准备好了,当准备好时才进行查询结果,否则可能是可见或不可见的状态,一般我们可认为是可见的。
使用查询结果
在渲染一个及其复杂的物体时,我们希望物体完全不可见时不进行对它进行渲染,为此我们可以准备渲染出它的边框,但它的颜色不会输出(glColorMask),并通过查询是否边框可见,若不可见则代表物体不需渲染,否则要渲染;
glBeginQuery(GL_SAMPLES_PASSED, the_query);
RenderSimplifiedObject(object);
glEndQuery(GL_SAMPLES_PASSED);
glGetQueryObjectuiv(the_query, GL_QUERY,RESULT, &the_result);
if(the_result!=0)
RenderRealObject(object);
在Begin和End之间渲染了一个低精度物体,查询结果非0则渲染高精度物体。
上述代码会查询结果时等待工作结束,而我们要避免等待。
glBeginQuery(GL_SAMPLES_PASSED, the_query);
RenderSimplifiedObject(object);
glEndQuery(GL_SAMPLES_PASSED);
glGetQueryObjectuiv(the_query, GL_QUERY_RESULT_AVAILABLE, &the_result);
if (the_result != 0)
glGetQueryObjectuiv(the_query, GL_QUERY,RESULT, &the_result);
else
the_result = 1;
if(the_result!=0)
RenderRealObject(object);
上述代码是在查询结果之前,先查询结果是否准备好了,若准备好了才进行查询,否则直接认定为可见边框直接渲染高精度物体。
可进行多个活动的查询,来避免等待,例如准备10个查询对象同10个低精度物体渲染,最终也是查询10次看是否可见,可见则渲染它们对应的高精度物体。
int n;
for (n = 0; n < 10; n++)
{
glBeginQuery(GL_SAMPLES_PASSED, ten_queries[n]);
RenderSimplifiedObject(&object[n]);
glEndQuery(GL_SAMPLES_PASSED);
}
for (n = 0; n < 10; n++)
{
glGetQueryObjectuiv(ten_queries[n], GL_QUERY_RESULT, &the_result);
if (the_result != 0 )
RenderRealObject(&object[n]);
}
优化上述代码
int n;
for (n = 0; n < 10; n++)
{
glBeginQuery(GL_SAMPLES_PASSED, ten_queries[n]);
RenderSimplifiedObject(&object[n]);
glEndQuery(GL_SAMPLES_PASSED);
}
for (n = 0; n < 10; n++)
{
glGetQueryObjectuiv(ten_queries[n], GL_QUERY_RESULT_AVAILABLE, &the_result);
if (the_result != 0)
glGetQueryObjectuiv(ten_queries[n], GL_QUERY_RESULT, &the_result);
else
the_result = 1;
if (the_result != 0 )
RenderRealObject(&object[n]);
}
这种准备多个查询对象进行的行为是比起一个查询对象进行更好是因为,它可能在进行第一次时不会立马得到结果,但在之后的渲染对象时,可能就已经完成了前面低精度物体的渲染拿到正确的查询结果了。
若我们不关心查询结果的实际值,如之前的代码全是the_result == 0即没有任何像素通过深度测试才认为不可见,否则可见。我们可以放宽条件,可更快地查询出结果。
GL_ANY_SAMPLES_PASSED 它会返回0或非0,只要第一个像素被渲染在屏幕上即为真,当真时就能够停止计数,立即为我们返回正确结果了,若一直是假则还是会等待的。可直接将GL_SAMPLES_PASSED替换成GL_ANY_SAMPLES_PASSED即可,看到应用程序性能的提升了,前提是OpenGL支持它。
让OpenGL决定
在之前我们是通过查询来确定是否可见,在让OpenGL某种情况下再进行渲染,这种OpenGL传达消息给CPU应用程序再发送命令给OpenGL操作的手法可能带来的开销比所谓的“优化”前开销更大。我们可以直接由OpenGL在渲染时决定是否渲染,即条件渲染技术。
1、调用如下函数Begin和End,渲染物体的条件是the_query查询对象里的查询结果不为0才渲染,否则忽略渲染。(它完全由GPU执行 不会回到CPU所以不有延时问题)
glBeginConditionalRender(the_query, GL_QUERY_WAIT);
//渲染物体
... ...
glEndConditionalRender();
具体实例:
glBeginQuery(GL_SAMPLES_PASSED, the_query);
RenderSimplifiedObject(object);
glEndQuery(GL_SAMPLES_PASSED);
glBeginConditionalRender(the_query, GL_QUERY_WAIT);
RenderRealObject(object);
glEndConditionalRender();
上述代码是先进行RenderSimplifiedObject渲染低精度物体,后面开启等待查询结果判断非0才渲染高精度(它由GPU执行),假如在glBeginConditonalRender时用的是GL_QUERY_WAIT,它是必须等待查询结果确定可用才继续执行判断是否可进行高精度渲染。我们可以用GL_QUERY_NO_WAIT来替换GL_QUERY_WAIT来告诉OpenGL不需要等待结果是否可用了,如果不可用,那直接执行后续的;若可用,那还是判断非0才渲染。
glBeginQuery(GL_SAMPLES_PASSED, the_query);
RenderSimplifiedObject(object);
glEndQuery(GL_SAMPLES_PASSED);
glBeginConditionalRender(the_query, GL_QUERY_NO_WAIT);
RenderRealObject(object);
glEndConditionalRender();
多个查询对象的示例:
int n;
for (n = 0; n < 10; n++)
{
glBeginQuery(GL_SAMPLES_PASSED, ten_queries[n]);
RenderSimplifiedObject(&object[n]);
glEndQuery(GL_SAMPLES_PASSED);
}
for (n = 0; n < 10; n++)
{
glBeginConditionalRender(ten_queries[n], GL_QUERY_NO_WAIT);
RenderRealObject(&object[n]);
glEndConditionalRender();
}
测量执行命令所需时间
定时器查询
使用glBeingQuery和glEndQuery时传递GL_TIME_ELAPSED来查询出在Begin和End之间执行的所有命令的纳秒数,一般用于查看哪部分开销最大。
GLuint queries[3];
GLuint world_time;
GLuint objects_time;
GLuint HUD_time;
glGenQueries(3, queries);
glBeginQuery(GL_TIME_ELAPSED, queries[0]);
RenderWorld();
glEndQuery(GL_TIME_ELAPSED);
glBeginQuery(GL_TIME_ELAPSED, queries[1]);
RenderObjects();
glEndQuery(GL_TIME_ELAPSED);
glBeginQuery(GL_TIME_ELAPSED, queries[2]);
RenderHUD();
glEndQuery(GL_TIME_ELAPSED);
glGetQueryObjectuiv(queries[0], GL_QUERY_RESULT, &world_time);
glGetQueryObjectuiv(queries[1], GL_QUERY_RESULT, &objects_time);
glGetQueryObjectuiv(queries[2], GL_QUERY_RESULT, &HUD_time);
glDeleteQueries(3, queries);
获取当前OpenGL记录的时间戳
void glQueryCounter(GLuint id, GLenum target);
target:查询对象,会将时间戳存储到这个对象,id填写GL_TIMESTAMP
示例:
GLuint queries[4];
GLuint start_time;
GLuint world_time;
GLuint objects_time;
GLuint HUD_time;
glGenQueries(4, queries);
glQueryCounter(GL_TIMESTAMP, queries[0]);
RenderWorld();
glQueryCounter(GL_TIMESTAMP, queries[1]);
RenderObjects();
glQueryCounter(GL_TIMESTAMP, queries[2]);
RenderHUD();
glQueryCounter(GL_TIMESTAMP, queries[3]);
glGetQueryObjectuiv(queries[0], GL_QUERY_RESULT, &start_time);
glGetQueryObjectuiv(queries[1], GL_QUERY_RESULT, &world_time);
glGetQueryObjectuiv(queries[2], GL_QUERY_RESULT, &objects_time);
glGetQueryObjectuiv(queries[3], GL_QUERY_RESULT, &HUD_time);
HUD_time -= objects_time;
objects_time -= world_time;
world_time -= start_time;
glDeleteQueries(4, queries);
在GPU内存中存储数据
之前我们一直都是交给GLTools管理顶点数据(顶点位置、法线、颜色等),这些数据都是从CPU内存传输到GPU本地内存上(显存)的,这样效率会慢很多。当有多个相同数据的副本时,我们更希望直接将数据存到GPU本地内存上,能大大提升访问效率,接下来会学习如何进行这种GPU本地存储。
使用缓冲区存储顶点数据
VBO是顶点缓冲区对象,表示它存储的是一些顶点数据,我们在存储时可提示OpenGL将如何使用它并如何存储它,若多次使用OpenGL会将它复制到绑定的图形卡上的快速内存中(即自动存到显存)
由于一个复杂的程序可能不止需要一个VBO,还有很多顶点属性,所以VAO(顶点数组对象)诞生,用它来管理这些内容。
创建VAO
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
创建VBO
glGenBuffers(1, &one_buffer);
//glGenBuffers(10, ten_buffers); //创建10个缓冲区
//将它绑定到GL_ARRAY_BUFFER绑定点 才能让顶点数据保存到一个缓冲区或从缓冲区取回
glBindBuffer(GL_ARRAY_BUFFER, one_buffer);
示例说明:
对单个VBO进行定位和初始化
GLuint my_buffer;
static const GLfloat data[] = { 1.0f, 2.0f, 3.0f, 4.0f, ... };
glGenBuffers(1, &my_buffer);
glBindBuffer(GL_ARRAY_BUFFER, my_buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(data), data, GL_STATIC_DRAW);
//在上面创建缓冲区并捆绑到数组缓冲区且填充数据data设为GL_STATIC_DRAW模式使用之后,
//下面的就是设置顶点属性指针,开始索引位置为0,大小为4,浮点类型, 倒数第二个0是指步长为0,最后参数是被解释为一个到""的偏移,即数据的确是从偏移0开始的。
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, (const GLvoid *)0);
使用单个VBO来保存多个顶点属性
static const GLfloat positions[] = { /* ...vec4s */ };
static const GLfloat colors[] = { /* ...vec4s */ };
static const GLfloat normals[] = { /* ...vec3s */ };
//在已经初始化缓冲区基础上,做后面的分配操作,注意数据填的是NULL,这里只是分配缓冲区的空间
glBufferData(GL_ARRAY_BUFFER, sizeof(positions) + sizeof(colors) + sizeof(normals), NULL, GL_STATIC_DRAW);
//分别设置3个数据到缓冲区中
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(positions), positions);
glBufferSubData(GL_ARRAY_BUFFER, sizeof(positions), sizeof(colors), colors);
glBufferSubData(GL_ARRAY_BUFFER, sizeof(positions) + sizeof(colors), sizeof(normals), normals);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, (const GLvoid *)0);
glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, (const GLvoid *)sizeof(positions));
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 0, (const GLvoid *)(sizeof(positions) + sizeof(colors)));
交叉存储的属性数据示例:
struct VERTEX_t
{
vec4 position;
vec4 color;
vec3 normal;
};
typedef struct VERTEX_t VERTEX;
extern VERTEX vertices[];
glBufferData(GL_ARRAY_BUFFER, vertex_count * sizeof(VERTEX), vertices, GL_STATIC_DRAW);
//与非结构体情况区别是 倒数第二参数是数组每一个元素之间的步长填的是sizeof(VERTEX)即结构体大小 和 最后一个参数是不同属性相对于结构体的偏移量。
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, sizeof(VERTEX), (const GLvoid *)OFFSETOF(VERTEX, position));
glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, sizeof(VERTEX), (const GLvoid *)OFFSETOF(VERTEX, color));
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(VERTEX), (const GLvoid *)OFFSETOF(VERTEX, normal));
交叉存储的意思就是把数据打包在一个块去存储,而非交叉即分别同类型属性打包在一起存储。
解除绑定数组缓冲区
glBindBuffer(GL_ARRAY_BUFFER, 0);
在缓冲区中保存顶点索引
GL_ELEMENT_ARRAY_BUFFER元素数组缓冲区是一个存储顶点索引的缓冲区,提供glDrawElements和glDrawRangeElements这样的函数使用。它没有类似glVertexAttribPointer等价的函数对应,只是简单地捆绑,再进行分配和填充数据给它。
void glDrawElements(GLenum mode, GLsizei count, Glenum type, const GLvoid * indices);
indices是元素数组缓冲区中第一个索引的偏移量(即顶点索引数组指针? )。调用它时我们必须要将一个缓冲区绑定到GL_ELEMENT_ARRAY_BUFFER绑定点,才能将顶点索引拷贝到这个缓冲区。
void glDrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, GLvoid *indices, GLint basevertex);
允许指定顶点索引的偏移,在顶点缓冲区中读取数据之前添加到顶点索引上。例如:一个复杂模型,它使用几个顶点缓冲区——比如,一个存储位置数据,一个用来存储法线,2个或3个用来存储纹理坐标(注意这些都是顶点数据,所以存储到顶点缓冲区的叫法是没问题的)。假设有一个包含1000个顶点的模型,它有一个顶点动画帧的渲染流程,那么第一个帧就从偏移0开始,第二帧从1000开始,第三个从2000开始,以此类推。但我们可以不用从新绑定VBO或VAO来改变顶点索引开始位置,而是用上述的basevertex来指定为1000即可在每次调用帧的时候去偏移1000个位置开始。(具体案例没有估计是这样理解,书上解释的太难懂了 难搞)类似地glDrawElementsBaseVertex、glDrawRangeElementsBaseVertex、glDrawElementsInstancedBaseVertex或glMultiDrawElementsBaseVertex都必然有basevertex参数。
使用顶点数组对象来组织缓冲区
创建VAO
void glGenVertexArrays(GLsizei n, GLuint *arrays);
若创建失败会返回0。
删除VAO
void glDeleteVertexArrays(GLsizei n, GLuint *arrays);
删除无效VAO将会忽略这种操作
捆绑VAO
void glBindVertexArray(GLuint array);
相当于启用当前array(VAO)对象。之后的任何顶点属性缓冲区捆绑和设置顶点属性指针时的状态都会记录到VAO上,以备渲染时直接通过glBindVertexArray(array)来启用这个VAO所拥有的所有数据来渲染物体。
解除VAO
glBindVertexArray(0);
这样的好处是调用glDrawArrays这样的渲染函数时,我们只需调用glBindVertexArray绑定一个VAO作为顶点数据来源 和 glDrawElements作为顶点索引数据来源 即可。而且在渲染中间件时也十分有利,VAO是相当于一种独立的顶点数据块,它不会干扰当前环境。(具体案例没有,这里我也不太懂)
高效地绘制大量几何图形
组合绘制函数
若想绘制大量几何图形,传统做法如下:
for(int i = 0; i < num_objects; i++)
{
glDrawArrays(GL_TRIANGLES, object[n]->first_vertex, object[n]->vertex_count);
}
这种多次调用glDrawArrays确实能绘制多个几何图形,但会造成高额的开销,下面介绍一种组合绘制函数;
void glMultiDrawArrays(GLenum mode, GLint *first, GLsizei *count, GLsizei primcount);
void glMultiDrawElements(GLenum mode, Glsizei *count, GLenum type, GLvoid **indices, GLsizei primcount);
这2个函数十分类似glDrawArrays的参数,区别即first和count都是数组,并且多了一个primcount代表绘制次数。(即批处理行为)
改进上例子:
GLint first[];
GLsizei count[];
for (int i = 0; i < num_objects; i++)
{
first[i] = object[n]->first_vertex;
count[i] = object[n]->vertex_count;
}
glMultiDrawArrays(GL_TRIANGLES, first, count, num_objects);
使用图元重启对几何图形进行组合
尽可能地将三角形连成三角形带,能减少开销,例如:3个独立并几乎连在一起的三角形渲染是用到了9个顶点,而将它们连到一起后渲染只占5个顶点,即除了第一个三角形之外的后续三角形渲染都会沿用之前的2个顶点,并只再增1个顶点开销即可。
图元重启(primitive restart)是几乎得到最新图形硬件普遍支持的特性,并且是OpenGL的一部分。图元重启用于GL_TRIANGLE_STRIP、GL_TRIANGLE_FAN、GL_LINE_STRIP和GL_LINE_LOOP几何图形类型中。
开启和关闭图元重启方法:
glEnable(GL_PRIMITIVE_RESTART);
glDisable(GL_PRIMITIVE_RESTART);
图元重启是会忽略设定的重启索引指定的顶点,即在这里断开,它的前一个顶点作为前一个条带的结尾顶点,它的后一个顶点作为后一个条带的首部顶点,指定的索引是真实存在的顶点索引。
glPrimitiveRestartIndex(index);
例如:0->1->2>->3->4->5->6 三角形带索引,设置3为重启索引,则会生成2个三角形条带即 0->1->2 和 4->5->6。
实例渲染
例如渲染一片草地,每一个草对象都是一个4个三角形的三角形带组成。代码可以如下:
glBindVertexArray(grass_vao);
for (int n = 0; n < number_of_blades_of_grass; n++)
{
SetupGrassBladeParameters();
glDrawArrays(GL_TRIANGLE_STRIP, 0, 6);//4个三角形的三角形带需6个顶点
}
这样的处理是开销很大的,可优化如下:
void glDrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei primcount);
//或者是
void glDrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void *indices, GLsizei primcount);
与glDrawArrays和glDrawElements很像,除了primcount是告诉OpenGL绘制的副本次数之外。
gl_InstanceID是GLSL的内置变量,类似整数统一值一样形式使用。 在每次绘制顶点副本时都会自增1,即第一次绘制时是0,第二次是1,最终达到primcount-1。上限是200W(???书上这么说滴)
glDrawArraysInstanced函数的功能伪代码如下,但注意的是并不会发起多次请求OpenGL绘制,而是只进行一次准备工作发起请求OpenGL渲染多次相同的顶点。(glDrawElementsInstanced同理)
for (int n = 0; n < primcount; n++)
{
//设置gl_InstanceID统一值 gl_InstanceID变量是"gl_InstanceID"统一值的位置索引
glUniform1i(gl_InstanceID, n);
glDrawArrays(mode, first, count);
}
glDrawElementsInstanced函数的功能伪代码如下:
for (int n = 0; n < primcount; n++)
{
glUniform1i(gl_InstanceID, n);
glDrawElements(mode, count, type, indices);
}
注意上面只是个伪代码,gl_InstanceID是OpenGL管理的内建变量。所以草地的每一个草物体都是同样的顶点数据,我们怎么可能在同一个位置上绘制同样的重叠的草物体呢!所以gl_InstanceID索引值就显得很重要了,因为它在每一个草渲染时,它都代表着第x个草渲染,从而可利用这个索引去获取一些顶点偏移值、新的草颜色 扰动等数值来渲染这个小草。(此草非彼草~)
利用将gl_InstanceID转成(x,y)来代表小草的新位置,可简单地求%和/来计算出x和y值,并且乘以小草的尺寸来缩放到合适的位置。而草地长宽可简单地设置为草总数额的一半,即若是2的20次方个草,那则是2的10次方为宽和高。可直接用最低10位作为x坐标,高10位作为z坐标,草地在x-z平面上渲染。为了使得草的位置具有随机性,可用一个种子值乘以一个很大的数,然后取生成结果的所有位的子集,并用它作为一个函数的输入。伪随机函数进行2次迭代后就可得到比较可观的随机分布了。我们需要2个随机数来分别对x和y值进行随机偏移。同时,为了使草地个体稍微不同,可分配不同的颜色或纹理。我们可利用位置坐标(x,y)作为纹理坐标采样一个纹理贴图,它的颜色作为小草的新颜色 或者 它的RGBA分量其中的任意分量代表的含义都可以我们自定义,如 小草的高度、长度等。例如,我们可用R通道作为小草的长度(高度),利用G通道存储小草的Y轴旋转角度,来为每个小草进行绕Y轴旋转。我们看似已经最大化地完成了静态部分的工作,但草是需要动起来才好看的,因此我们要对草进行变换,例如弯曲小草,我们可利用B通道存储小草的弯曲因子,让草绕X轴旋转,我们可以合并Y和X的旋转变换为同一个旋转矩阵去渲染小草,当B是0时则完全竖直,为1是则完全平躺,所以纹理的B通道应该是偏小的值范围内波动,来模拟出被微风吹压的感觉。最后纹理还剩下alpha通道没有赋予含义,我们为了模拟草的颜色深浅不一,可用alpha通道作为一张1D渐变纹理的坐标使用,通过alpha值去采样1D渐变纹理即拿到草的颜色,这张1D纹理可以是任何你想设置的颜色渐变,例如从枯黄色到深绿色渐变。请记住,这些所有的数据都依赖于gl_InstanceID进行工作的,以它作为每颗草的区别,发送到OpenGL的几何图形只有6个点,而绘制草地中所有的草所需要的代码只调用一次glDrawArraysInstanced即可,这就是实例渲染。(OpenGL yyds~)
案例补充:https://blog.csdn.net/qq_39574690/article/details/116352248
自动获得数据
设置"实例化数组"
void glVertexAttribDivisor(GLuint index, GLuint divisor);
将属性索引传递到函数的index,并将divisor设置为每次从数组中读取新值之间进行传递的实例数,若divisor为0,数组会成为一个常规顶点属性,即每个顶点读取一个新值,若非0则会每隔几个实例都从这个数组读取一个新值,例如divisor为2,则每隔2个实例从这个数组获取一个新值。例如绘制不同颜色对象时,如下顶点着色器:
#version 150
precision highp float;
in vec4 position;
in vec4 color;
out Fragment
{
vec4 color;
} fragment;
uniform mat4 mvp;
void main(void)
{
gl_Position = mvp * position;
fragment.color = color;
}
每个顶点的颜色会不同,应用程序会提供一个顶点数组,长度与顶点数相同,例如:
glVertexAttribDivisor(index_of_color, 1);
将color设置为一个实例数组,index_of_color是color属性绑定到的槽的索引。这样,我们每个实例都会是不同的颜色,实例数组长度只需是实例总数即可。若divisor是3,那么颜色会每3个实例更新一次颜色color。即每3个物体渲染后就会变色渲染。
我们需要对这些物体改变位置才能看清楚 是怎么个情况,将着色器代码修改:
#version 150
precision highp float;
in vec4 position;
in vec4 instance_color;
in vec4 instance_position;
out Fragment
{
vec4 color;
} fragment;
uniform mat4 mvp;
void main(void)
{
gl_Position = mvp * (position + instance_position);
fragment.color = instance_color;
}
上述代码使用了 glVertexAttribDivisor(index_of_instance_position, 1); 来将instance_position输入属性设置为一个实例数组, index_of_instance_position是instance_position属性的位置索引。任意类型的输入属性都可使用glVertexAttribDivisor函数将其设置为实例数组。
设置属性实例
instancingProg = gltLoadShaderPair("instancing.vs", "instancing.fs");
glBindAttribLocation(instancingProg, 0, "position");
glBindAttribLocation(instancingProg, 1, "instance_color");
glBindAttribLocation(instancingProg, 2, "instance_position");
glLinkProgram(instancingProg);
应用实例:https://blog.csdn.net/qq_39574690/article/details/116332913
存储变换的顶点——变换反馈
变换反馈
它是OpenGL的一种特殊模式,允许将一个顶点着色器或几何着色器的结果保存到一个缓冲区中。若存在几何着色器,那么想获取顶点着色器的输出需要间接通过几何着色器来输出到这个缓冲区。
变换反馈缓冲区是存在于几何图形着色器输出和顶点装配阶段之间的,这缓冲区就是将顶点着色器或几何着色器的输出作为新的顶点装配阶段输入使用的。指定哪些着色器输出填充到变换反馈缓冲区用如下方法:
void glTransformFeedbackVaryings(GLuint program, Glsizei count, const GLchar **varyings, GLenum bufferMode);
program:着色器程序 即变换反馈缓冲区是各个程序独立的,即使使用相同的几何着色器输出,但只要是不同的着色程序则会是不同的变换反馈缓冲区。
count: 输出属性个数
varings: 输出属性名称数组
bufferMode: GL_SEPARATE_ATTRIBS 每个属性单独划分一个区域集体存储(书上好像说的是每个属性单独分一个缓冲区存储) / GL_INTERLEAVED_ATTRIBS 属性密集存储(即一个接一个地记录到单独的缓冲区中)
out vec4 vs_position_out;
out vec4 vs_color_out;
out vec3 vs_normal_out;
out vec3 vs_binormal_out;
out vec3 vs_tangent_out;
上述代码是顶点着色器输出,为指定它们存储到变换反馈缓冲区代码如下:
static const char * varying_names[] =
{
"vs_position_out",
"vs_color_out",
"vs_normal_out",
"vs_binormal_out",
"vs_tangent_out"
};
glTransformFeedbackVaryings(program, 5, varing_names, GL_INTERLEAVED_ATTRIBS);
在指定变换反馈缓冲区的填充属性后可用glLinkProgram(program)进行连接。在此之前,得准备好变换反馈缓冲区对象。
GLint buffer;
...
glGenBuffers(1, &buffer);
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, buffer); //捆绑变换反馈缓冲区绑定点
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, size, NULL, GL_DYNAMIC_COPY);//分配空间
除了直接用glBindBuffer绑定到通用绑定点之外,还可绑定其他的变换反馈绑定点,如下函数:
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, index, buffer);
index是GL_TRANSFORM_FEEDBACK_BUFFER绑定点的索引(??)
glBindBufferBase的高级版本函数,即glBindBufferRange
void glBindBufferRange(GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size);
其中target, index, buffer一样,offset是指定缓冲区开始位置,size是缓冲区大小。它是将缓冲区的一部分绑定到变换反馈绑定点。
若属性存储模式为GL_SEPARATE_ATTRIBS 则每个输出会单独划分为一个缓冲区存储(或使用glBindBufferRange在同一个缓冲区上划分多个区域部分存储),它所支持的最大缓冲区数量是通过GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS为参数的glGetIntegerv来查询的,这限制也适用于几何着色器的最大的输出给变换反馈缓冲区的属性数量。在使用GL_INTERLEAVED_ATTRIBS时,非分量类型属性则不存在这种限制,而分量类型属性存在个数限制,即vec3属性的支持个数比vec4支持的多,取决于图形硬件,可通过GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS为参数的glGetIntegerv来查询。不允许出现即存在交叉又存在独立存储的情况。
void glBeginTransformFeedback(GLenum primitiveMode);
通过调用上述方法激活变换反馈模式。设置什么图元模式,要求几何着色器输出的类型是对应的类型。
图元模式与绘制类型对应表如下:
图元模式的值 | 允许绘制的类型 |
GL_POINTS | GL_POINTS |
GL_LINES | GL_LINE_STRIP、GL_LINE_LOOP |
GL_TRIANGLES | GL_TRIANGLES、GL_TRIANGLE_STRIP、GL_TRIANGLE_FAN |
退出变换反馈模式
glEndTransformFeedback();
所有在glBeginTransformFeedback和glEndTransformFeedback之间的渲染结果都会存储到变换反馈缓冲区中。在这Begin和End期间时,保证不要解除变换反馈换缓冲区绑定和重新设定大小或重新分配缓冲区空间。
关闭光栅化
//关闭光栅化
glEnable(GL_RASTERIZER_DISCARD);
//开启光栅化
glDisable(GL_RASTERIZER_DISCARD);
关闭光栅化后,顶点着色器和几何着色器所生成的任何东西都不会创建任何片段,而片段着色器也不会运行。
这种操作有利于我们仅仅只是想使用变换反馈,而不需渲染任何东西时,如果没有使用变换反馈,又关闭光栅化的话,那效果就是OpenGL管线关闭。
使用图元查询对顶点进行计数
通过使用查询对象来进行GL_PRIMITIVES_GENERATED查询几何着色器输出的图元数量和GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTED查询输入变换反馈缓冲区的图元数量。对比两者差异,若相同则说明一切正常,若不相同,说明可能变换反馈缓冲区空间不足。
使用图元查询的结果
//写入一个变换反馈缓冲区的绘制数据
glBindBuffer(GL_ARRAY_BUFFER, buffer1);
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, buffer2);
glBeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, q);
glBeginTransformFeedback(GL_POINTS);
DrawSomePoints();
glEndTransformFeedback();
glEndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
//获取到的是DrawSomePoints输出给变换反馈buffer2缓冲区的图元数量
glGetQueryObjectuiv(q, GL_QUERY_RESULT, &vertices_to_render);
//将变换反馈buffer2指定为数据源
glBindBuffer(GL_ARRAY_BUFFER, buffer2);
//进行渲染GL_POINTS点,数量是vertices_to_render即变换反馈接收的实际数量
glDrawArrays(GL_POINTS, 0, vertices_to_render);
变换反馈的应用实例
存储中间结果
利用变换反馈存储中间结果的功能,能优化“实例数组”顶点着色器的性能,例如:准备2个顶点着色器,第一个仅进行一次为所有实例对象,它只进行了共同部分的工作将中间结果存储到变换反馈缓冲区中。第二个顶点着色器从变换反馈缓冲区获取中间结果,再进行实例数组属性部分的作业。第一个着色器渲染时应该关闭光栅化来优化。
迭代或递归算法
物理模拟普遍是迭代的算法,将迭代数据缓存到变换反馈缓冲区是十分合适的,因为变换反馈缓冲区的数据是允许直接作为顶点缓冲区进行绑定的,如上面刚刚就用了glBindBuffer(GL_ARRAY_BUFFER, buffer2)。例如:粒子系统模拟是循环算法。
更加深入的变换反馈示例——集群(Flocking)
后续有兴趣研读...too hard~ 案例:
裁剪并确定绘制内容
在几何图形从应用程序传递到顶点着色器变换空间到裁剪空间后,这时OpenGL会执行裁剪决定哪些在视口内,哪些在视口外。
要完成这个裁剪工作,OpenGL将3D空间拆分为6个“半空间”即左裁剪面、右裁剪面、上裁剪面、下裁剪面、近端裁剪面、远端裁剪面定义。每个顶点会进行计算出它位于这些平面的带符号距离,它若是正数则代表位于视口内,否则视口外,若它是0则恰好在平面上。我们只需检查这6个距离正负值即可知道顶点是否在视口内了,综合几个顶点的结果就可知道一个大块的几何图形是否可见。
① 单个三角形的所有顶点都位于任意平面之外则说明三角形完全在视口之外,可简单丢弃它。
② 单个三角形的任意顶点到任何一个平面都不为负,则说明三角形完全在视口之内。
③ 只有当一个三角形跨越一个平面时,才需要做进一步的工作,即部分可见三角形。 可以采用Sutherland-Hodgman裁剪算法将三角形分解成几个更小的三角形,或可将整个三角形进行简单的光栅化,并强制丢弃最终落在视口外部的片段。
裁剪距离——自定义裁剪空间
顶点着色器或几何着色器中可通过内建变量gl_ClipDistance[]写入自定义的裁剪距离。它是一个浮点类型数组,启动它需要调用glEnable(GL_CLIP_DISTANCE0 + n); n是启用裁剪距离的索引,有GL_CLIP_DISTANCES1、GL_CLIP_DISTANCES2,... GL_CLIP_DISTANCES5。可查询GL_MAX_CLIP_DISTANCES为标记的glGetIntegerv来查询最大支持的裁剪距离索引值。调用glDisable(GL_CLIP_DISTANCE0 + n)来禁用自定义裁剪距离。如果没有启用对应的裁剪索引,那么gl_ClipDistance[]对应的值就不会生效。
当一个顶点着色器输入的gl_Clip_Distance[]数组全为负数时,则代表它在裁剪空间之外,若一个三角形所有顶点都在裁剪区域外则代表三角形不可见直接抛弃,否则可能是可见或部分可见,若是部分可见会根据裁剪距离进行线性插值确定每个像素的可见性,它由顶点着色器进行。
计算顶点到平面的距离可简单通过点乘运算计算得到。
gl_Clip_Distance[]数组可通过顶点着色器传递给片段着色器使用,例如通过它进行基于平面距离的alpha递减形成越靠近裁剪平面越暗淡的效果,并且能防止裁剪边缘的锯齿现象出现。注意:若图元的顶点都位于裁剪空间之外会立即消除的。
裁剪可能会导致的问题是对于点、线的裁剪,因为裁剪是基于顶点进行的,当点的大小或线的宽度很大时,可能有部分片段仍然在视口内,但点或线图元的顶点都位于视口之外时,在原本还位于视口之内的片段会突然消失,因为图元的顶点全位于裁剪空间之外时会导致图元完全被舍弃。
可关闭远端裁剪和近端裁剪,并将其原被裁剪的深度截取回[0,1]范围内来实现真实世界的情况,即远处的物体理论上是绝对可见的。它称之为深度截取。我们可开启深度截取(默认关闭)调用:
glEnable(GL_DEPTH_CLAMP); //开启
//glDisable(GL_DEPTH_CLAMP); //关闭
注意:深度截取不是指深度裁剪,截取是指将原本被裁剪的图元深度截取回[0,1]范围,从而让它可见。虽然深度值从技术上说是已经不真实了,但在某些情况渲染单物体情况上,效果能达到更好。
在OpenGL开始绘制时进行同步
强制开始,即到目前为止发出的任何命令至少会放入OpenGL管线的始端,并且最终会被执行。但无法得知这些命令执行状态的信息——我们只知道它们最终会被执行。
glFlush();
和
强制结束,保证所有发出的命令都被完整执行,而OpenGL管线则是空的。在保证完整执行之后,它将清空OpenGL管线,导致泡沫,降低性能,有时甚至极大地降低性能。
glFinish();
在OpenGL和OpenGL之间共享数据时,我们可能需要了解某些时刻为止OpenGL是不是完成了命令的执行。同步对象(sync object) 能管理这些同步信息。它有两种可能的状态,即标记状态和未标记状态。默认是未标记,而当某些特定事件发生时,会转成标记状态。由什么事件触发这种转变取决于它们的类型。例如:围栏同步对象(fence sync)可通过调用如下方法创建。
GLsync glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
第一个参数是监听事件名,即GL_SYNC_GPU_COMMANDS_COMPLETE,即完成所有GPU命令时触发;第二个参数是标志字段,设为0,因为没有这种类型的同步对象相关的标记。返回值是一个新的GLsync对象(同步对象)。在它被创建后,以未标记状态进入OpenGL管线,并与所有其他命令一起进行处理,而不会使OpenGL产生延迟,也不会消耗大量资源。当它到达管线终点时,会像其他命令一样“执行”,而这样就会将它的状态设置为标记状态。
查询同步对象是否已经为标记状态可调用:
glGetSynciv(sync, GL_SYNC_STATUS, sizeof(GLint), NULL, &result);
若是标记状态下的,那么结果会返回GL_SIGNALED, 否则是GL_UNSIGNALED。
GLint result = GL_UNSIGNALED;
glGetSynciv(sync, GL_SYNC_STATUS, sizeof(GLint), NULL, &result);
while (result != GL_SIGNALED)
{
DoSomeUsefulWork();
glGetSynciv(sync, GL_SYNC_STATUS, sizeof(GLint), NULL, &result);
}
这段代码会循环进行做一些事情,只要没有进行标记之前。
要让OpenGL实际地等待一个同步对象变成标记状态(并且由此在同步完成前等待管线中的命令)可使用如下2个函数:
glClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, timeout);
//或
glWaitSync(sync, 0, GL_TIMEOUT_IGNORED);
sync是同步对象,第二个参数和第三个参数均是同样的含义,但必须设置不同的值。其中,GL_SYNC_FLUSH_COMMAND_BIT是确保同步对象在开始等待它变成标记状态之前已经进入到OpenGL管线。如果没有这个限制,就可能会出现OpenGL等待一个还没有发送到管线中的同步对象的情况,最终导致应用程序永远等待并挂起。第三参数是一个需要等待的超时值,以纳秒为单位。如果同步对象在这些时间内没有变成标记状态,则返回一个状态代码来对此进行标记。如下4个状态代码。
ClientWaitSync的返回状态 | 含义 |
GL_ALREADY_SIGNALED | 在调用glClientWaitSync时同步对象已经为标记状态,所以函数立即返回 |
GL_TIMEOUT_EXPIRED | 已经达到超时参数所限定的超时值,意味着在允许时间内同步对象不会变成标记状态 |
GL_CONDITION_SATISFIED | 同步对象在允许时间内变成标记状态(但是在glClientWaitSync调用时还不是标记状态) |
GL_WAIT_FAILED | 出现一个错误(例如同步并不是一个有效同步对象),而用户应该检查glGetError()的结果已获得更多信息 |
关于超时值,注意的是因为是纳秒级别的数值,它可能会忽略精度,例如超时值为1纳秒时,可能会被当成1毫秒甚至更长时间看待。如果超时值是0,若同步对象已经标记则返回GL_ALREADY_SIGNALED,反之则返回GL_TIMEOUT_EXPIRED,它是不会返回GL_CONDITION_SATISFIED的。
glWaitSync是GPU等待同步对象被标记,而应用程序则直接返回。因为它是直接立即返回应用程序的,即不存在挂起应用操作,所以第二和第三个参数超时值就无意义,所以第二参数填0,第三参数是填写GL_TIMEOUT_IGNORED来忽略超时值。我们知道glClientWaitSync是要求等待OpenGL管线中的同步对象被标记后才返回应用程序继续执行的,不然就是挂起状态,所以它具有了意义;而glWaitSync是没有对应用程序进任何挂起操作的,它只是让GPU去等待管线中的同步对象被标记,而且它肯定是会被执行到而被标记的,所以这样做的目的有什么意义?这样思考,同步对象在单个环境是无意义,但它可以是多个环境共享的对象,我们可以在一个环境调用glFenceSync创建同步对象,接着在另一个环境中调用glWaitSync(或者glClientWaitSync)进行等待,它等待的是其他环境的OpenGL工作完成。
例如:要求一个OpenGL环境推迟某些渲染,直到其他环境完成某些操作为止,这样就需要同步对象来为2个环境进行这种同步。一个应用程序可以有2个线程和2个环境(或更多)。如果我们在每个环境中创建一个同步对象,然后在每个环境中使用任意glClientWaitSync来等待来自其他环境的同步对象,当所有函数都返回时,所有这些环境都会与另一个同步。结合由操作系统提供的线程同步图元(信号量) 我们可以让多个窗口的渲染保持同步。
删除同步对象
glDeleteSync(sync);
删除操作并不会立马执行,而是会等待它被标记后或抵达管线终点或超时之后才会真正进行删除。