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 to EmitVertex().

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 a triangle_strip output 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 subsequent EmitVertex() call starts a brand-new, disconnected primitive. If EndPrimitive() 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_vertices Declaration: Every geometry shader must specify a max_vertices = N layout qualifier. The shader cannot call EmitVertex() 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_vertices and total output attribute size. Keeping max_vertices as small as possible ensures higher GPU thread occupancy and better rendering performance.
  • Discarding Geometry: A geometry shader is not required to call EmitVertex(). Exiting main() without calling EmitVertex() discards the incoming primitive entirely, serving as a dynamic primitive-culling mechanism.