How to Use Custom GLSL Out Variables for MRTs?

Multiple Render Targets (MRTs) enable a single fragment shader execution to output data across several color buffers simultaneously, serving as a foundational technique for deferred rendering and post-processing pipelines. In modern OpenGL and GLSL, MRTs are implemented by declaring custom fragment shader output variables bound to explicit location indices using the layout(location = N) qualifier, paired with a host-side Framebuffer Object (FBO) configured with glDrawBuffers.

Declaring Custom Outputs in GLSL

In modern core profile GLSL (version 330 and newer), built-in outputs like gl_FragColor and gl_FragData are deprecated and removed. Instead, fragment shaders use user-defined out variables paired with the layout(location = N) qualifier. Each location corresponds directly to an index within the active draw buffer array.

#version 330 core

// Custom output variables mapped to draw buffer slots
layout (location = 0) out vec4 gPosition;
layout (location = 1) out vec3 gNormal;
layout (location = 2) out vec4 gAlbedoSpec;

in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoords;

uniform sampler2D diffuseTexture;
uniform sampler2D specularTexture;

void main()
{
    // Write world/view-space position to target 0
    gPosition = vec4(FragPos, 1.0);

    // Write normalized surface normal to target 1
    gNormal = normalize(Normal);

    // Write diffuse color (RGB) and specular intensity (A) to target 2
    gAlbedoSpec.rgb = texture(diffuseTexture, TexCoords).rgb;
    gAlbedoSpec.a = texture(specularTexture, TexCoords).r;
}

The location index specified in the layout qualifier dictates the slot index in the framebuffer's draw buffer mapping, not the physical GL_COLOR_ATTACHMENT number directly.

Host-Side Framebuffer Configuration

To route the fragment shader outputs to physical textures or renderbuffers, the application must set up an FBO with multiple color attachments and tell OpenGL which attachments correspond to each output location.

1. Generating and Binding Textures

Each target requires a dedicated texture attachment formatted to store the intended data type:

GLuint gBuffer;
glGenFramebuffers(1, &gBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);

GLuint gPosition, gNormal, gAlbedoSpec;

// Position buffer: 16-bit or 32-bit floating point
glGenTextures(1, &gPosition);
glBindTexture(GL_TEXTURE_2D, gPosition);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, width, height, 0, GL_RGBA, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, gPosition, 0);

// Normal buffer: 16-bit floating point
glGenTextures(1, &gNormal);
glBindTexture(GL_TEXTURE_2D, gNormal);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, width, height, 0, GL_RGB, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, gNormal, 0);

// Albedo and Specular buffer: standard 8-bit normalized RGBA
glGenTextures(1, &gAlbedoSpec);
glBindTexture(GL_TEXTURE_2D, gAlbedoSpec);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_TEXTURE_2D, gAlbedoSpec, 0);

2. Mapping Outputs with glDrawBuffers

By default, an FBO only routes output to GL_COLOR_ATTACHMENT0. The glDrawBuffers function defines the mapping between the shader's layout(location = N) indices and the framebuffer attachments.

unsigned int attachments[3] = {
    GL_COLOR_ATTACHMENT0, // layout(location = 0)
    GL_COLOR_ATTACHMENT1, // layout(location = 1)
    GL_COLOR_ATTACHMENT2  // layout(location = 2)
};

glDrawBuffers(3, attachments);

If the array contains GL_NONE at index 1, writes to layout(location = 1) are discarded while location = 0 and location = 2 remain active.

Dynamic Location Binding Alternative

If layout qualifiers are omitted inside the GLSL source, locations can be assigned dynamically before linking the shader program using glBindFragDataLocation:

glBindFragDataLocation(shaderProgram, 0, "gPosition");
glBindFragDataLocation(shaderProgram, 1, "gNormal");
glBindFragDataLocation(shaderProgram, 2, "gAlbedoSpec");
glLinkProgram(shaderProgram);

Explicit layout(location = N) qualifiers inside the shader code are generally preferred because they eliminate link-order dependencies and provide self-documenting shader interfaces.

Best Practices and Constraints