Mesh Shader Output to Fragment Shader Input in GLSL

This article explains how GLSL mesh shaders route per-vertex output attributes to fragment shader location bindings. In modern GPU pipelines using GL_EXT_mesh_shader, mesh shaders replace traditional vertex and geometry stages by explicitly generating primitives and vertex data. By declaring per-vertex output blocks with specified location qualifiers and writing vertex topology through primitive index arrays, the hardware rasterizer interpolates these outputs and binds them seamlessly to matching fragment shader input locations.

Declaring Per-Vertex Outputs in the Mesh Shader

In GLSL mesh shaders, per-vertex outputs are defined using an interface block or individual variables qualified with the out vertices storage qualifier. To route data predictably to the fragment stage, each attribute is assigned an explicit index using layout(location = N).

#version 460
#extension GL_EXT_mesh_shader : require

layout(local_size_x = 32) in;
layout(triangles, max_vertices = 64, max_primitives = 126) out;

// Explicit location assignment on a per-vertex interface block
layout(location = 0) out VertexOutput {
    vec3 normal;
    vec2 uv;
} v_out[];

In this setup, v_out[] is an array sized by max_vertices. The attribute normal consumes location = 0, while uv automatically increments to location = 1 unless explicitly assigned otherwise.

Populating the Outputs and Defining Topology

Mesh shaders decouple vertex generation from primitive connectivity. You must set the actual output counts at runtime via SetMeshOutputsEXT(numVertices, numPrimitives). Mesh shader invocations then write directly to individual vertex indices within the output arrays:

uint vID = gl_LocalInvocationIndex;
if (vID < numVertices) {
    gl_MeshVerticesEXT[vID].gl_Position = transform * vec4(inPositions[vID], 1.0);
    v_out[vID].normal = inNormals[vID];
    v_out[vID].uv = inUVs[vID];
}

To form geometry, primitive indices are populated using the built-in index array (such as gl_PrimitiveTriangleIndicesEXT):

if (gl_LocalInvocationIndex < numPrimitives) {
    gl_PrimitiveTriangleIndicesEXT[gl_LocalInvocationIndex] = uvec3(i0, i1, i2);
}

Rasterization and Interpolation

The hardware rasterizer reads the indices stored in gl_PrimitiveTriangleIndicesEXT to gather vertex positions from gl_MeshVerticesEXT and corresponding attributes from v_out[].

For each fragment covered by a generated primitive, the rasterizer performs perspective-correct interpolation on the attributes defined at v_out[i0], v_out[i1], and v_out[i2]. Unless qualified with the flat interpolation modifier, floating-point vectors are interpolated across the face of the primitive according to the fragment's barycentric coordinates.

Binding to Fragment Shader Inputs

The fragment shader receives the interpolated attributes by declaring input variables with matching location qualifiers. The variable names in the fragment shader do not need to match those in the mesh shader; the linkage is determined solely by the location layout:

#version 460

layout(location = 0) in vec3 inNormal;
layout(location = 1) in vec2 inUV;

layout(location = 0) out vec4 fragColor;

void main() {
    fragColor = vec4(inNormal * 0.5 + 0.5, 1.0);
}

Because inNormal is bound to location = 0, it automatically receives the interpolated values of v_out[].normal. Similarly, inUV maps directly to location = 1.

Key Rules for Mesh-to-Fragment Routing

  1. Location Matching: The layout location = N in the fragment shader must match the base location assigned to the corresponding attribute in the mesh shader.
  2. Type and Qualifier Matching: The data type and interpolation qualifiers (such as flat, noperspective, or centroid) must match between the mesh shader's per-vertex output and the fragment shader's input.
  3. Distinction from Per-Primitive Data: Mesh shaders can also emit per-primitive attributes using the perprimitiveEXT qualifier. Per-primitive attributes also use layout(location = N), but their location namespace is shared with per-vertex attributes. You cannot assign the same location number to both a per-vertex and a per-primitive output within the same pipeline.