How Did GLSL Replace Attribute and Varying with In and Out?
When the OpenGL Shading Language (GLSL) introduced version 1.30
alongside OpenGL 3.0, it fundamentally changed how data flows between
pipeline stages by deprecating the legacy attribute and
varying qualifiers in favor of unified in and
out keywords. This architectural shift modernized shader
design by removing stage-specific terminology, establishing a consistent
syntax across all shader types, and laying the groundwork for complex
programmable pipelines that include geometry, tessellation, and compute
stages.
The Legacy Model: Stage-Specific Keywords
In early versions of GLSL (such as GLSL 1.10 and 1.20), data routing across the rendering pipeline depended heavily on fixed naming conventions:
attribute: Used strictly within vertex shaders to denote per-vertex inputs supplied from Vertex Buffer Objects (VBOs), such as vertex positions, normals, or texture coordinates.varying: Declared identically in both vertex and fragment shaders to pass interpolated data across the rasterizer. The vertex shader wrote to avaryingvariable, the rasterization hardware interpolated that data across the primitive's surface, and the fragment shader read the resulting value.- **
gl_FragColor/gl_FragData**: Built-in variables used in fragment shaders to output final pixel colors rather than explicit user-defined outputs.
While straightforward for fixed two-stage pipelines (vertex to fragment), this keyword structure became rigid and confusing as OpenGL evolved.
The Unified in
and out Interface
With the release of GLSL 1.30 (and later GLSL ES 3.00 for embedded systems), the language adopted a stage-agnostic directional model. Instead of relying on specialized keywords tied to specific stages, variables are defined strictly by their direction of flow relative to the current shader execution.
// Legacy GLSL (1.20)
// Vertex Shader:
attribute vec3 aPos;
varying vec2 vTexCoord;
void main() {
vTexCoord = aPos.xy;
gl_Position = vec4(aPos, 1.0);
}
// Fragment Shader:
varying vec2 vTexCoord;
void main() {
gl_FragColor = vec4(vTexCoord, 0.0, 1.0);
}// Modern GLSL (1.30+ / Core Profile)
// Vertex Shader:
#version 330 core
layout(location = 0) in vec3 aPos;
out vec2 vTexCoord;
void main() {
vTexCoord = aPos.xy;
gl_Position = vec4(aPos, 1.0);
}
// Fragment Shader:
#version 330 core
in vec2 vTexCoord;
out vec4 FragColor;
void main() {
FragColor = vec4(vTexCoord, 0.0, 1.0);
}Key Architectural Advantages
The transition brought several technical improvements to the graphics programming workflow:
- Pipeline Extensibility: As OpenGL added geometry,
tessellation control, tessellation evaluation, and compute shaders, a
directional model scaled seamlessly. An
outvariable from any prior stage automatically maps to a matchinginvariable in the subsequent stage without requiring new language keywords. - Explicit Fragment Outputs: Replacing built-in
globals like
gl_FragColorwith explicitoutvariables simplified Multiple Render Targets (MRT), allowing developers to output to specific framebuffer attachments cleanly. - Layout Qualifiers: The unified model integrates
directly with explicit layout bindings (such as
layout(location = 0)), reducing the need for CPU-side reflection queries likeglGetAttribLocationorglGetFragDataLocation. - Interface Blocks: The syntax facilitated the
addition of uniform blocks and named interface blocks (e.g.,
in VertexData { vec2 uv; } inData;), streamlining the sharing of structured datasets between stages.
By standardizing inputs and outputs as directional interfaces rather than stage-specific declarations, GLSL achieved a cleaner, more modular architecture that aligns with modern GPU hardware pipelines and continues to define modern shader standards.