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
- Hardware Limits: Query
GL_MAX_DRAW_BUFFERSandGL_MAX_COLOR_ATTACHMENTSwithglGetIntegervto ensure target hardware supports the required attachment count (minimum 8 in modern desktop OpenGL). - Format Consistency: Avoid writing floating-point
values into normalized integer targets (
GL_RGBA8) if values exceed the \([0.0, 1.0]\) range or require negative values. - Framebuffer Completeness: Always call
glCheckFramebufferStatus(GL_FRAMEBUFFER)after configuring attachments to verify compatibility across all render targets.