How Does EmitVertex Work in GLSL Geometry Shaders?
In the OpenGL Shading Language (GLSL), EmitVertex() is a
built-in function that allows geometry shaders to dynamically generate
and output vertex data to the graphics pipeline. Unlike vertex shaders,
which process a single incoming vertex and produce a single outgoing
vertex, geometry shaders operate on entire primitives and can output a
variable number of vertices. This makes EmitVertex()
essential for procedural geometry generation, particle expansion,
billboarding, and custom silhouette extrusion.
The Role of EmitVertex in the Graphics Pipeline
The geometry shader stage resides between primitive assembly (or tessellation evaluation) and rasterization. It takes an entire input primitive—such as a point, line, or triangle—and produces output primitives.
The output primitives are not returned as an array or returned
directly from main(). Instead, GLSL employs a stream-based
emission model. Every time the shader calls EmitVertex(),
the GPU takes the current values of gl_Position and any
user-defined out variables, snapshots them, and sends a
completed vertex to the current primitive assembly stream.
Output State and Variable Latching
When calling EmitVertex(), all output variables defined
in the geometry shader behave like state registers:
- Position Assignment: Setting
gl_Position = vec4(...)updates the internal position register. - Varying Attributes: Setting custom output variables
(e.g.,
out vec2 texCoord,out vec3 normal) updates their respective registers. - Latching: Invoking
EmitVertex()captures the values currently assigned to these registers. Modifying an output variable after emitting a vertex does not affect the already emitted vertex; it only affects subsequent calls toEmitVertex().
Because attribute states persist across calls unless explicitly
reassigned, failing to update an output variable before the next
EmitVertex() call causes the subsequent vertex to reuse the
previous value.
Pairing EmitVertex with EndPrimitive
Geometry shaders typically emit connected topologies using strip
layouts, such as points, line_strip, or
triangle_strip.
EmitVertex()adds a vertex to the current strip. For example, three successive calls in atriangle_stripoutput layout form a single triangle. A fourth call appends a second triangle sharing an edge with the first.EndPrimitive()signals the GPU that the current strip is complete. Any subsequentEmitVertex()call starts a brand-new, disconnected primitive. IfEndPrimitive()is not called explicitly before the shader completes, the hardware invokes it implicitly at the end of the shader execution for any active primitive.
Example GLSL Geometry Shader Implementation
The following example demonstrates how a geometry shader takes a
single point primitive and expands it into a screen-aligned quad (two
triangles using a triangle_strip):
#version 330 core
layout (points) in;
layout (triangle_strip, max_vertices = 4) out;
in VS_OUT {
vec3 color;
} gs_in[];
out vec3 fColor;
void main() {
fColor = gs_in[0].color;
// Bottom-Left
gl_Position = gl_in[0].gl_Position + vec4(-0.1, -0.1, 0.0, 0.0);
EmitVertex();
// Bottom-Right
gl_Position = gl_in[0].gl_Position + vec4( 0.1, -0.1, 0.0, 0.0);
EmitVertex();
// Top-Left
gl_Position = gl_in[0].gl_Position + vec4(-0.1, 0.1, 0.0, 0.0);
EmitVertex();
// Top-Right
gl_Position = gl_in[0].gl_Position + vec4( 0.1, 0.1, 0.0, 0.0);
EmitVertex();
EndPrimitive();
}In this code, four sequential calls to EmitVertex()
generate four vertices that form a quad via triangle_strip.
EndPrimitive() closes the quad.
Practical Considerations and Constraints
max_verticesDeclaration: Every geometry shader must specify amax_vertices = Nlayout qualifier. The shader cannot callEmitVertex()more times than this declared limit per invocation. Emitting more vertices than declared results in undefined behavior or dropped geometry.- Performance Impact: Hardware allocates fixed buffer
space for every invocation based on the declared
max_verticesand total output attribute size. Keepingmax_verticesas small as possible ensures higher GPU thread occupancy and better rendering performance. - Discarding Geometry: A geometry shader is not
required to call
EmitVertex(). Exitingmain()without callingEmitVertex()discards the incoming primitive entirely, serving as a dynamic primitive-culling mechanism.