What Is gl_ViewportIndex in GLSL Viewports?
The built-in variable gl_ViewportIndex in the OpenGL
Shading Language (GLSL) allows shaders to dynamically route individual
primitives to a specific viewport within a multi-viewport array. By
assigning an integer index to gl_ViewportIndex, a single
draw call can project and clip geometry across multiple viewports
simultaneously. This article explores how gl_ViewportIndex
operates, how to configure multiple viewports on the host side, its
implementation across shader stages, and the primary rendering
techniques that benefit from it.
Understanding Multi-Viewport Rendering
Standard OpenGL pipelines map normalized device coordinates (NDC) to
window coordinates using a single viewport configured via
glViewport. Modern rendering engines frequently require
rendering the same scene or geometry from different perspectives or into
distinct regions of a frame, such as:
- Stereoscopic and VR Rendering: Rendering left-eye and right-eye views concurrently into a single side-by-side framebuffer.
- Cubemap and Omnidirectional Shadow Generation: Rendering geometry across all six faces of a cubemap simultaneously.
- Cascaded Shadow Maps (CSM): Directing geometry across multiple depth cascade viewports.
- Split-Screen Multiplayer: Rendering distinct player cameras in a unified render pass.
Traditionally, achieving this required multiple draw calls or
CPU-side viewport state changes. OpenGL introduced array-based viewport
functions—such as glViewportArrayv and
glViewportIndexedf—allowing applications to define an array
of independent viewports (indexed from 0 to
GL_MAX_VIEWPORTS - 1). The variable
gl_ViewportIndex serves as the shader-side mechanism to
select which viewport in that array processes a given primitive.
Role and Behavior of gl_ViewportIndex
gl_ViewportIndex is an integer output variable
(out int gl_ViewportIndex;). When a primitive is processed,
the rasterizer inspects the value of gl_ViewportIndex
associated with the primitive's provoking vertex:
- Viewport Transformation: The geometry is
transformed from NDC to the specific pixel rectangle defined by the
viewport at index
gl_ViewportIndex. - Scissor Testing: If scissor testing is enabled, the
primitive is clipped against the corresponding scissor rectangle at
index
gl_ViewportIndex(configured viaglScissorArrayvorglScissorIndexed). - Out-of-Bounds Handling: If the shader writes an
index greater than or equal to the maximum supported viewports, the
behavior is undefined, though typical drivers clamp the index to
0.
In the fragment shader, gl_ViewportIndex is available as
a flat-interpolated input (flat in int gl_ViewportIndex;),
allowing the fragment stage to identify which viewport generated the
current fragment.
Utilizing gl_ViewportIndex Across Shader Stages
Geometry Shader Approach
In core OpenGL (version 4.1 and higher),
gl_ViewportIndex is natively supported in the Geometry
Shader (GS). The GS can clone input primitives and emit them with
different viewport indices and transformation matrices.
#version 410 core
layout(triangles) in;
layout(triangle_strip, max_vertices = 6) out;
out int gl_ViewportIndex;
uniform mat4 u_ProjectionMatrices[2];
void main()
{
// Emit primitive to Viewport 0 (e.g., Left Eye)
gl_ViewportIndex = 0;
for(int i = 0; i < 3; ++i)
{
gl_Position = u_ProjectionMatrices[0] * gl_in[i].gl_Position;
EmitVertex();
}
EndPrimitive();
// Emit primitive to Viewport 1 (e.g., Right Eye)
gl_ViewportIndex = 1;
for(int i = 0; i < 3; ++i)
{
gl_Position = u_ProjectionMatrices[1] * gl_in[i].gl_Position;
EmitVertex();
}
EndPrimitive();
}Vertex Shader Direct Routing
Geometry shaders introduce pipeline overhead when primitive
amplification is not strictly necessary. Using extensions such as
GL_ARB_shader_viewport_layer_array or
GL_AMD_vertex_shader_viewport_index (core in Vulkan and
available on modern OpenGL hardware), gl_ViewportIndex can
be written directly from the Vertex Shader:
#version 450 core
#extension GL_ARB_shader_viewport_layer_array : require
layout(location = 0) in vec3 in_Position;
layout(location = 1) in int in_InstanceViewportIndex;
out int gl_ViewportIndex;
uniform mat4 u_ViewProjections[4];
void main()
{
gl_ViewportIndex = in_InstanceViewportIndex;
gl_Position = u_ViewProjections[in_InstanceViewportIndex] * vec4(in_Position, 1.0);
}This approach allows instanced rendering to distribute batches of geometry across different viewports without activating the geometry stage.
Key Considerations and Performance
- Provoking Vertex Rule: Because viewport routing is
per-primitive rather than per-vertex, the value of
gl_ViewportIndexmust match across all vertices of a single emitted primitive, or the rasterizer will strictly adhere to the value set at the provoking vertex. - Array Bounds: Hardware limits queryable via
glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports)typically provide 16 viewports. Exceeding this count leads to invalid memory access or dropped geometry. - State Synchronization: Viewports and scissors must be set up simultaneously using array functions on the CPU side to ensure correct depth bounds and coordinate scaling across every target index.