GLSL Multiview: Avoid Redundant Vertex Transforms

Multiview rendering significantly enhances graphics efficiency in virtual reality and stereoscopic rendering by drawing a scene to multiple render targets simultaneously using a single draw call. By leveraging extensions such as GL_OVR_multiview and GL_OVR_multiview2, developers can execute vertex shaders that share common calculations across all views. This architecture directly addresses performance bottlenecks by decoupling view-independent geometry operations—such as skeletal animation and world-space transforms—from view-dependent projections, effectively eliminating redundant vertex processing passes.

The Redundancy of Traditional Multi-Pass Pipelines

In standard stereoscopic rendering, graphics applications typically generate images for each eye using one of two inefficient methods:

  1. Multiple Draw Calls: The application issues a separate draw call for each view. This forces the GPU to execute the entire vertex pipeline twice, repeating expensive view-invariant calculations like vertex skinning, morph target blending, physics transformations, and model-to-world matrix multiplications.
  2. Geometry Shader Instancing: A single draw call passes vertices to a geometry shader, which duplicates primitives and routes them to different array layers. While this avoids multiple API calls, geometry shaders frequently introduce pipeline stalls, expand memory bandwidth consumption, and cannot easily share vertex-stage calculations across duplicates.

Decoupling Computations via gl_ViewID_OVR

Multiview rendering resolves this problem at the vertex shader stage through the built-in variable gl_ViewID_OVR. When multiview is enabled, the vertex shader executes with awareness of which view it is currently computing, allowing the programmer to structure GLSL code so that identical calculations are only evaluated once.

Vertex transformations fall into two distinct categories:

  • View-Independent Transforms: Skeletal animation matrices, morphing, and transformations from object space to world space depend exclusively on the model and its animation state. These values are identical whether rendered for the left eye or the right eye.
  • View-Dependent Transforms: The transformation from world space to clip space (view and projection matrices) and camera-relative calculations (such as specular view vectors) vary by eye.

By isolating these categories in GLSL, the vertex shader evaluates the heavy view-independent transformations first, saves the intermediate result in world space, and then applies view-specific transformations conditionally based on gl_ViewID_OVR.

#version 300 es
#extension GL_OVR_multiview2 : enable

layout(num_views = 2) in;

layout(location = 0) in vec3 aPosition;
layout(location = 1) in vec3 aNormal;

uniform mat4 uModelMatrix;
uniform mat4 uViewProjection[2];

out vec3 vWorldNormal;

void main()
{
    // Common, view-independent transform: Computed once
    vec4 worldPosition = uModelMatrix * vec4(aPosition, 1.0);
    vWorldNormal = mat3(uModelMatrix) * aNormal;

    // View-dependent transform: Indexed via gl_ViewID_OVR
    gl_Position = uViewProjection[gl_ViewID_OVR] * worldPosition;
}

Hardware-Level Execution and Driver Optimization

Under the GL_OVR_multiview specification, the GPU driver can optimize shader execution beyond simple software indexing. Because the pipeline evaluates both views within the same drawing context, modern GPU architectures can execute the scalar and vector instructions corresponding to view-independent code in shared execution paths.

Rather than re-fetching vertex buffers, decompressing attributes, and recalculating skeletal weights per view, the hardware fetches and processes the raw vertex attributes once. The derived values are then broadcast to the viewport-routing hardware alongside the eye-specific clip-space positions.

Furthermore, using GL_OVR_multiview2 relaxes the constraint that requires all shader outputs to be view-invariant, allowing select interpolants to vary per view only when strictly necessary. This fine-grained control ensures memory bandwidth and register allocations are utilized solely for data that differs between viewpoints.