GLSL Mesh Shader Lines: gl_PrimitiveLineIndicesEXT

This article explains how to configure and generate line network primitives in GLSL using mesh shaders via the GL_EXT_mesh_shader extension. It covers the required layout qualifiers, primitive count allocations, and how to populate the gl_PrimitiveLineIndicesEXT index buffer to define custom line topologies, segments, and connected wireframes efficiently on the GPU.

Pipeline Configuration and Layout Qualifiers

To output lines instead of triangles, the mesh shader must declare the lines output primitive topology alongside the maximum bounds for vertices and primitives. This requires enabling the GL_EXT_mesh_shader extension.

#version 460
#extension GL_EXT_mesh_shader : require

layout(local_size_x = 32) in;
layout(lines, max_vertices = 64, max_primitives = 63) out;
  • lines: Sets the primitive assembly mode to discrete line segments.
  • max_vertices: Defines the upper limit of vertices this workgroup can export.
  • max_primitives: Defines the maximum number of line segments generated by the workgroup.

Setting Mesh Outputs

Before writing index data, the mesh shader workgroup must specify the exact number of active vertices and primitives for the current execution. This is done collectively using SetMeshOutputsEXT.

uint numVertices = 64;
uint numLines = 63;
SetMeshOutputsEXT(numVertices, numLines);

Every active primitive index written beyond numLines will be discarded, and invocations must complete this call before the primitive stage assembles geometry.

Configuring Topology with gl_PrimitiveLineIndicesEXT

In a mesh shader configured for lines, primitive connectivity is determined by the built-in array gl_PrimitiveLineIndicesEXT[].

Each element in this array is a two-component unsigned integer vector (uvec2), representing the start and end vertex indices of a single line segment:

out uvec2 gl_PrimitiveLineIndicesEXT[];

To form a continuous line network, polyline, or arbitrary graph, assign the corresponding vertex indices to each entry.

1. Continuous Line Strips (Polylines)

For a connected sequence of lines where vertex \(i\) connects to vertex \(i + 1\):

uint threadId = gl_LocalInvocationIndex;

if (threadId < numLines) {
    gl_PrimitiveLineIndicesEXT[threadId] = uvec2(threadId, threadId + 1);
}

2. Disjoint Line Segments

For disconnected pairs of vertices forming isolated segments:

uint threadId = gl_LocalInvocationIndex;

if (threadId < numLines) {
    uint baseIndex = threadId * 2;
    gl_PrimitiveLineIndicesEXT[threadId] = uvec2(baseIndex, baseIndex + 1);
}

3. Arbitrary Line Networks (Grids or Graphs)

Because gl_PrimitiveLineIndicesEXT gives direct indexing control per primitive, complex graphs can be indexed arbitrarily without repeating shared vertices:

// Example: Creating a closed loop of 4 vertices
if (threadId == 0) gl_PrimitiveLineIndicesEXT[0] = uvec2(0, 1);
if (threadId == 1) gl_PrimitiveLineIndicesEXT[1] = uvec2(1, 2);
if (threadId == 2) gl_PrimitiveLineIndicesEXT[2] = uvec2(2, 3);
if (threadId == 3) gl_PrimitiveLineIndicesEXT[3] = uvec2(3, 0);

Complete Implementation Example

#version 460
#extension GL_EXT_mesh_shader : require

layout(local_size_x = 32) in;
layout(lines, max_vertices = 33, max_primitives = 32) out;

void main() {
    uint threadId = gl_LocalInvocationIndex;
    uint vertexCount = 33;
    uint lineCount = 32;

    SetMeshOutputsEXT(vertexCount, lineCount);

    // Compute vertex positions
    if (threadId < vertexCount) {
        float x = float(threadId) / float(vertexCount - 1);
        gl_MeshVerticesEXT[threadId].gl_Position = vec4(x * 2.0 - 1.0, sin(x * 6.28), 0.0, 1.0);
    }

    // Configure line topology
    if (threadId < lineCount) {
        gl_PrimitiveLineIndicesEXT[threadId] = uvec2(threadId, threadId + 1);
    }
}

Unlike traditional geometry shaders, mesh shaders allow threads within the workgroup to write both vertex data (gl_MeshVerticesEXT) and line topological connections (gl_PrimitiveLineIndicesEXT) in parallel, eliminating redundant vertex processing across adjacent lines in the network.