How Does the Flat Qualifier Work in GLSL?
The flat interpolation qualifier in GLSL disables the
rasterizer's default barycentric interpolation when passing varying data
from a vertex (or geometry) shader to a fragment shader. Instead of
blending values smoothly across the surface of a primitive, the
rasterizer assigns the exact output value of a single designated
"provoking vertex" to all fragments generated by that primitive. This
mechanism is essential for passing discrete non-numeric or integral
data, such as material IDs, bitmasks, and unblended flat-shaded surface
normals.
Default Interpolation vs. Flat Interpolation
During the fixed-function rasterization stage, hardware interpolates vertex attributes across primitives (triangles, lines, or points) before they reach the fragment shader.
GLSL provides three primary interpolation qualifiers:
smooth(Default): Performs perspective-correct interpolation across the primitive using the fragments' barycentric coordinates and depth.noperspective: Performs linear interpolation in screen-space, ignoring perspective foreshortening.flat: Completely bypasses the interpolation hardware. Every fragment generated by a primitive receives identical data from a single vertex.
The Provoking Vertex Mechanism
Because a triangle consists of three distinct vertices, the pipeline needs a rule to determine which vertex provides the flat value to the fragments. This source vertex is known as the provoking vertex.
- OpenGL Default Convention: In core OpenGL, the
provoking vertex for standard triangles (
GL_TRIANGLES) is typically the last vertex specified for that primitive. - Direct3D/Vulkan Convention: These APIs typically use the first vertex as the provoking vertex.
- Control via API: OpenGL allows developers to
configure this convention using the
glProvokingVertex(GL_FIRST_VERTEX_CONVENTION)orglProvokingVertex(GL_LAST_VERTEX_CONVENTION)API call.
For triangle strips or fans, the provoking vertex shifts dynamically according to the winding rules defined by the graphics specification.
Syntax and Mandatory Type Constraints
When using the flat qualifier, the declaration must
match identically between the sending and receiving shader stages.
Vertex Shader Example
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in int aMaterialID;
flat out int vMaterialID;
flat out vec3 vFacetNormal;
void main() {
vMaterialID = aMaterialID;
vFacetNormal = aPos; // Evaluated at the provoking vertex
gl_Position = vec4(aPos, 1.0);
}Fragment Shader Example
#version 330 core
flat in int vMaterialID;
flat in vec3 vFacetNormal;
out vec4 FragColor;
void main() {
if (vMaterialID == 1) {
FragColor = vec4(vFacetNormal, 1.0);
} else {
FragColor = vec4(0.5, 0.5, 0.5, 1.0);
}
}Type Rules for Integers
In GLSL, all integer types (int, uint, and
their vector variants like ivec2, uvec4)
passed between stages must be qualified with
flat. Because interpolating discrete integer IDs across
continuous fragments results in fractional values that cannot be
represented as integers, the GLSL specification enforces compile-time
errors if integer varyings omit the flat qualifier.
Common Use Cases
- Flat Shading: Achieving a faceted, low-poly aesthetic where entire polygon faces share a single constant surface normal without creating redundant, split vertices in vertex buffers.
- Indexed Lookups: Passing entity IDs, texture array indices, or material indices directly to the fragment shader for storage buffer or texture array lookups.
- Bitfield Flags: Sending packed boolean flags, render state bitmasks, or instance metadata across the pipeline without corruption from interpolation.