第十章 几何着色器
创建几何着色器
使用glCreateShader()结合参数GL_GEOMETRY_SHADER创建几何着色器。
如果不需要启用光栅化可以使用glEnable(GL_RASTERIZER_DISCARD)来关闭光栅化阶段。
几何着色器的图元类型与对应的绘制模式:
几何着色器图元 | 对应的绘制命令模式 |
points | GL_POINTS, GL_PATCHES |
lines | GL_LINES, GL_LINE_STRIP, GL_LINE_LOOP, GL_PATCHES |
triangles | GL_TRIANGLES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_PATCHES |
lines_adjacency | GL_LINES_ADJACENCY, GL_LINE_STRIP_ADJACENCY |
triangles_adjacency | GL_TRIANGLES_ADJACENCY, GL_TRIANGLES_STRIP_ADJACENCY |
其中,如果几何着色器已经存在,并且对应的细分模式可以将patch转换到兼容的几何着色器输入类型,那么此时我们可以使用GL_PATCHES参数。
如果调用EndPrimitive()或者达到着色器代码的末尾,那么当前所有没有完成的图元将被直接抛弃。
几何着色器的输入和输出
输入信息
几何着色器对于每个输入的图元都会运行一次,前一个阶段的输出数据在几何着色器中综艺数组的形式出现。这些数据包括所有用户定义的输入和内置输入gl_in。
gl_in的声明
1 2 3 4 5 | in gl_PerVertex{ vec4 gl_Position; float gl_PointSize; float gl_ClipDistance[]; } gl_in[]; |
图元类型 | 输入数组的大小 |
points | 1 |
lines | 2 |
triangles | 3 |
lines_adjacency | 4 |
triangles_adjacency | 6 |
其他用户自定义的输入也会声明为数组。
1 2 3 4 5 6 7 | // 顶点着色器 out vec4 position; out vec3 normal; // 几何着色器 in vec4 position[]; in vec3 normal[]; |
在GLSL4.3之前,不支持二维数组,如果需要传递二维数组则需要通过接口块,gl_in中的gl_ClipDistance就是一个例子。
其他内置输入变量:
gl_PrimitiveIDIn,等价于片元着色器的gl_PrimitiveID
gl_InvocationID,用于几何着色器实例化时指定当前实例化序号。
带有邻接信息的线段
lines_adjacency对应OpenGL中的两种邻接图元:GL_LINES_ADJACENCY和GL_LINES_STRIP_ADJACENCY。前者表示使用的4个顶点是不重叠的,例如第一个图元是A、B、C、D,第二个图原始E、F、G、H。后者第一个图元由A、B、C、D组成第二个图元由B、C、D、E以此类推。
对于4个顶点的图元,第一个和最后一个顶点表示附加的邻接信息,第二个和第三个表示线段本身。如果几何着色器不存在,那么邻接顶点将被抛弃。
带有邻接信息的三角形
triangle_adjacency对应OpenGL的两种图元:GL_TRIANGLES_ADJACENCY和GL_TRIANGLE_STRIP_ADJACENCY。
两种模式的顶点序列:
其中GL_TRIANGLE_STRIP_ADJACENCY的三角形条带顺序如下:
可以使用gl_DrawElements来生成邻接图元的数据。
输出信息
几何着色器输出的隐式声明:
1 2 3 4 5 6 | out gl_PerVertex { vec4 gl_Position; float gl_PointSize; float gl_ClipDistance[]; }; |
因为每个几何着色器的请求都可以创建多个输出顶点,所以我们必须显示的通过EmitVertex()函数来产生顶点。当调用EmitVertex()之后,几何着色器输出的所有当前值都会被记录下来,构成一个新的顶点,EmitVertex()调用完成之后,几何着色器的所有输出值都是未定义的状态,因此我们必须在几何着色器中写出所有的输出所有的输出信息来产生顶点,哪怕写出的信息与输入的顶点信息是完全一致的。这个规则的唯一例外是使用flat关键字。此时,只有provoking vertex的顶点值会用于后继的阶段,所以只要未定义部分的结果未被使用就没有问题。
设置provoking vertex
1 | void glProvokingVertex(GLenum provokeMode); |
provokeMode可以是GL_LAST_VERTEX_CONVENTION或者GL_FIRST_VERTEX_CONVENTION,默认是GL_LAST_VERTEX_CONVENTION。
除了使用glProvokingVertex设置模式,不同图元也有不同得人处理方式:
除了内置的和用户自定义的逐顶点输出之外,几何着色器还有三个内置输出:gl_PrimitiveID、gl_Layer和gl_ViewportIndex。
gl_PrimitiveID:指定属于哪个图元。
产生图元
几何体的裁剪
如果一个几何着色器什么都不做,那么就会产生裁剪效果。
几何体的扩充
几何着色器可以输出的顶点数的最大值可以使用gl_MaxGeometryOutputVertices来获取,在应用程序中可以通过glGetIngegerv()读取GL_MAX_GEOMETRY_OUTPUT_VERTICES来获取。最小值是256。
transform feedback高级篇
transform feedback可以捕获顶点着色器的输出并记录到一个或者多个缓存对象当中,可以用来作后续渲染或者利用glMapBuffer和glGetBufferSubData()等读回到CPU端。
多重输出流
几何着色器可以指定多组输出的顶点数据流,可以使用stream布局限定符来声明。stream的最大数量可以通过GL_MAX_VERTEX_STREAMS来获取。默认的输出流总是0。
首先可以使用全局布局限定符来设置数据流映射:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // 此处的设置是多余的,默认就是0 layout (stream=0) out; // foo和bar都输出到数据流0 out vec4 foo; out vec4 bar; // 切换到1 layout (stream=1) out; out vec4 proton; flat out float electron; // 输出流声明对输入是没有影响的 in vec2 elephant; // 重新切回到0 layout (stream=0) out; out vec4 baz; // 跳过2直接跳转到数据流3 layout (stream=3) out; flat out int iron; out vec2 copper; |
也可以使用数据接口块的放射进行映射:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | layout (stream=0) out stream0 { vec4 foo; vec4 bar; vec4 baz; }; layout (stream=1) out stream1 { vec4 proton; flat float electron; }; lyaout (stream=3) out stream3 { float int iron; vec2 copper; }; |
在多输出流中如果要发射顶点和结束图元需要使用EmitStreamVertex(int stream)和EndStreamPrimitive(int stream)。
发射顶点的错误方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // 设置流0的变量值 foo = vec4(1, 2, 3, 4) ... // 其他变量 // 数据流1 proton = aton; electron = 2.0; // 数据流3 iron = 4; copper = shiny; EmitStreamVertex(0); EmitStreamVertex(1); EmitStreamVertex(3); |
因为如之前所述EmitStreamVertex的调用会重置所有变量值,所以第一次调用之后数据流1和3的数据就是未定义的了,正确方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // 设置流0的变量值 foo = vec4(1, 2, 3, 4) ... // 其他变量 EmitStreamVertex(0); // 数据流1 proton = aton; electron = 2.0; EmitStreamVertex(1); // 数据流3 iron = 4; copper = shiny; EmitStreamVertex(3); |
在指定transform feedback的输出变量是通过glTransformFeedbackVaryings()来进行映射的。如果要将单一流的全部或者部分变量以交错的方式写入单一缓存中,需要使用保留变量名gl_NextBuffer来通知之后的输出变量记录到下一个transform feedback绑定点对应的缓存对象当中。对于之前的离职,将第一个输出流变量(foo、bar和baz)记录到第一个transform feedback缓存绑定点对应的缓存对象中,将第二个流变量记录到第二个绑定点的缓存最后将输出流3的变量记录到第三个缓存绑定点对象的缓存对象中:
1 2 3 4 5 6 7 8 9 10 11 | static const char * const vars[] = { "foo", "bar", "baz", "gl_NextBuffer", "proton", "electron", "gl_NextBuffer", "iron", "copper" }; glTransformFeedbackVaryings(prog, sizeof(vars) / sizeof(vars[0]), varyings, GL_INTERLEAVED_ATTRIBS); glLinkProgram(prog); |
如果开启光栅化,并且已经有一个片元着色器,那么只有数据流0的输出变量将被用来构建光栅化的图元,其他流的输出变量在片元着色器中是不可见的。如果使用了多重输出流那么他们的图元类型必须是points。
图元查询
由于几何着色器会额外产生新的顶点,所以在使用之前是无法获得transform feedback缓存大小的。可以使用查询命令来计算几何着色器生成图元的数量,包括GL_PRIMITIVE_GENERATED和GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN。GL_PRIMITIVE_GENERTATED查询几何着色器产生的顶点数量,它在任何时候都是可用的,即使transform feedback没有被启用,GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN查询实际写入transform feedback缓存的顶点数目。
由于几何着色器可以输出到多个流当中,所以要建立图元查询的索引,每个查询都有多个不同的绑定点分别用于不同的输出流。
开启某个图元数据流的一次查询
1 | void glBeginQueryIndexed(GLenum target, GLuint index, GLuint id); |
id是查询对象,target和index指定查询目标点
结束查询
1 | void glEndQueryIndexed(GLenum target, GLuint index); |
target可以指定为GL_PRIMITIVE_GENERATED或者GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN。index是执行查询的图元查询绑定点的索引。
查询之后可以通过glGenQueries()并设置pname为GL_QUERY_RESULT来查询结果。
使用transform feedback结果
如果使用glDrawArrays()来使用获取的顶点数据,就会出现CPU和GPU相互等待的情况,要避免这个情况,可以使用以下命令:
1 2 | void glDrawTransformFeedback(GLenum mode, GLuint id); void glDrawTransformFeedbackStream(GLenum mode, GLuint id, GLuint stream); |
id为transform feedback的对象id,mode为图元类型,默认绘制first为0,count为transform feedback不活的图元数目,默认通过GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN查询,这个操作会自动完成。
函数的实例化版本:
1 2 | void glDrawTransformFeedbackInstanced(GLenum mode, GLuint id, GLsizei instancecount); void glDrawTransformFeedbackStreamInstanced(GLenum mode, GLuint id, GLuint stream, GLsizei instancecount); |
几何着色器的多实例化
几何着色器的实例化只影响从几何着色器开始的阶段,而不是整个渲染管线,可以通过invocations布局限定符来指定。
1 | layout (triangles, invocations = 4) in; |
可以使用内置的输入变量gl_InvocationID来进行区别
多视口和分层渲染
多视口渲染
gl_ViewportIndex用来指定视口变换中要使用哪一组视口参数。
设置给定视口的包围范围
1 2 3 | void glViewportIndexedf(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h); void glViewportIndexedfv(GLuint index, const GLfloat* value); void glDepthRangeIndexed(GLuint index, GLclamped n, GLclamped f); |
设置视口index的右上角(x, y)、宽高和远近平面值。
设置多个视口的包围范围
1 2 | void glViewportArrayv(GLuint first, GLsizei count, const GLfloat* v); void glDepthRangeArrayv(GLuint first, GLsizei count, const GLdouble* v); |
first为第一个视口的索引值,count是视口数量。
设置视口的剪切矩形
1 2 | void glScissorIndexed(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height); void glScissorIndexedv(GLuint index, const GLint* v); |
设置index视口的剪切矩形的左下角为(left, botton),宽高分别为width和height
试着多个视口的剪切矩形
1 | void glScissorArrayv(GLuint first, GLsizei count, const GLint* v); |
分层渲染
当渲染到帧缓存对象中,可以使用2维数组作为颜色附件,然后通过几何着色器渲染到某个切片。
使用帧缓存的分层附件时的一个限制是帧缓存的所有附件都必须是分层的,并且同一个分层帧缓存的所有附件必须是同样的纹理类型(1维2维数组纹理或者是cubemap纹理等)。如果有这样的帧缓存对象那么glCheckFramebufferStatus()会返回GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS的错误。
在几何着色器中可以将结果分别渲染到数组的各个切片中,同时设置gl_Layer变量