Skip to content

Wireframe Geometry Shader

wireframe_geometry.png

This example shows advanced wireframe rendering that produces smooth, anti-aliased lines of constant pixel width regardless of camera distance. Traditional VK_POLYGON_MODE_LINE produces jagged wireframes and can't overlay on solid fills. This technique uses geometry shaders to compute barycentric coordinates and fragment shaders to calculate distance-to-edge, enabling anti-aliased wireframes with controllable thickness that can overlay solid geometry.

The example uses the KDGpuExample helper API for simplified setup.

Overview

What this example demonstrates:

  • Geometry shaders for per-triangle processing
  • Barycentric coordinates for edge distance calculation
  • Anti-aliased wireframe lines using fwidth()
  • Constant pixel-width lines independent of depth
  • Wireframe overlay on solid geometry
  • Viewport-aware thickness control

Use cases:

  • CAD/modeling applications
  • Debugging geometry
  • Technical visualization
  • Stylized rendering (toon shading + wireframe)
  • Mesh inspection tools

Vulkan Requirements

  • Vulkan Version: 1.0+
  • Extensions: None
  • Features: geometryShader (check device support)
  • Limits: Check maxGeometryOutputVertices

Key Concepts

Traditional Wireframe Problems:

Using VK_POLYGON_MODE_LINE:

  • Lines are jagged (no anti-aliasing)
  • Line width in clip space (thicker when closer)
  • Can't combine with solid fill in single pass
  • Limited line width support (often max 1.0)

Geometry Shader Wireframe:

Solution:

  1. Render triangles normally (solid fill)
  2. Geometry shader computes barycentric coords for each vertex
  3. Fragment shader calculates distance to nearest edge
  4. If distance < threshold, draw wire color; else solid color
  5. Use fwidth() for anti-aliasing

Result: Smooth, anti-aliased wireframes of constant pixel width.

Barycentric Coordinates:

For a triangle with vertices A, B, C, barycentric coords (u, v, w) satisfy:

  • Point P = u*A + v*B + w*C
  • u + v + w = 1
  • Distance to edge opposite vertex = corresponding coordinate

Example: u = 0 means point is on edge BC.

Geometry shader assigns:

  • Vertex 0: (1, 0, 0)
  • Vertex 1: (0, 1, 0)
  • Vertex 2: (0, 0, 1)

Fragment interpolates these → distance to each edge.

Anti-Aliasing with fwidth():

fwidth(x) returns rate of change of x across pixels:

1
2
3
float edgeDist = min(min(baryCoord.x, baryCoord.y), baryCoord.z);
float delta = fwidth(edgeDist);  // Rate of change
float alpha = smoothstep(lineWidth - delta, lineWidth + delta, edgeDist);

This creates smooth 1-pixel anti-aliased edges.

Spec: https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.4.60.html#built-in-functions (fwidth)

Implementation

Per-Vertex Data:

1
2
3
4
5
struct Vertex {
    glm::vec3 position;
    glm::vec3 normal;
    float excludeEdge; // Set to 1.0 to exclude edge opposite this vertex from the wireframe, 0.0 to include
};

Filename: wireframe_geometry/wireframe_geometry.cpp

The excludeEdge flag allows hiding specific edges (e.g., for quad diagonals).

Shader Wireframe Calculation:

1
2
3
4
5
6
7
8
    // Create a vertex, geometry and fragment shader
    auto vertexShaderPath = KDGpuExample::assetDir().file("shaders/examples/wireframe_geometry/wireframe_geometry.vert.spv");
    auto vertexShader = m_device.createShaderModule(KDGpuExample::readShaderFile(vertexShaderPath));
    auto geometryShaderPath = KDGpuExample::assetDir().file("shaders/examples/wireframe_geometry/wireframe_geometry.geom.spv");
    auto geometryShader = m_device.createShaderModule(KDGpuExample::readShaderFile(geometryShaderPath));

    auto fragmentShaderPath = KDGpuExample::assetDir().file("shaders/examples/wireframe_geometry/wireframe_geometry.frag.spv");
    auto fragmentShader = m_device.createShaderModule(KDGpuExample::readShaderFile(fragmentShaderPath));

Filename: wireframe_geometry/wireframe_geometry.cpp

Key steps:

  1. Calculate minimum distance to any edge
  2. Use fwidth() for anti-aliasing kernel size
  3. smoothstep() creates smooth transition
  4. Mix wireframe and solid colors

Viewport Matrix for Constant Width:

Pass viewport dimensions to maintain constant pixel width regardless of depth. The geometry shader can transform edge distances from clip space to screen space.

Performance Notes

Geometry Shader Cost:

  • GPU: Geometry shaders are typically slower than vertex/fragment
  • Throughput: Lower than modern mesh shaders
  • Mobile: Often unsupported or very slow

Optimization Tips:

  • Use only when visual quality justifies cost
  • Consider compute-based alternatives for high-performance needs
  • On mobile, use VK_POLYGON_MODE_LINE if acceptable
  • Batch wireframe draws separately from solid geometry

Alternative Techniques:

  • Mesh shaders: Modern replacement for geometry shaders (faster)
  • Compute shaders: Generate wireframe geometry on GPU
  • Instancing: Draw lines as instanced quads
  • Conservative rasterization: Hardware edge detection

See Also

Further Reading


Updated on 2026-03-31 at 00:02:07 +0000