Geometry Shaders

A geometry shader (GS) runs once per primitive, receives all its vertices, and can emit zero or more primitives to a stream.

A geometry shader (GS) runs once per primitive, receives all its vertices, and can emit zero or more primitives to a stream. Uses: wireframe overlays, normal visualization, layered rendering (cube-map faces, cascades) in one pass, point-to-quad expansion, stream output. On modern GPUs GS is often slow for amplification — prefer instancing, compute, or mesh shaders for heavy work.

25.1 Syntax

Input primitive types: point, line, triangle, lineadj, triangleadj. Output streams: PointStream<T>, LineStream<T>, TriangleStream<T>.

struct GSIn  { float4 pos : SV_Position; float3 normalW : NORMAL; float3 posW : POSITION_WS; };
struct GSOut { float4 pos : SV_Position; float3 color : COLOR; };

cbuffer GSData : register(b0) { float4x4 gViewProjGS; float gNormalLength; };

// Draw each vertex normal as a line
[maxvertexcount(6)]
void NormalsGS(triangle GSIn tri[3], inout LineStream<GSOut> stream)
{
    [unroll] for (int i = 0; i < 3; ++i)
    {
        GSOut a, b;
        a.pos = mul(gViewProjGS, float4(tri[i].posW, 1));
        a.color = float3(1, 1, 0);
        b.pos = mul(gViewProjGS, float4(tri[i].posW + tri[i].normalW * gNormalLength, 1));
        b.color = float3(1, 0, 0);
        stream.Append(a);
        stream.Append(b);
        stream.RestartStrip();          // end this line strip
    }
}

25.2 Point sprites (point → quad)

struct PointIn { float3 posW : POSITION; float size : PSIZE; float4 color : COLOR; };
struct QuadOut { float4 pos : SV_Position; float2 uv : TEXCOORD0; float4 color : COLOR; };
cbuffer Cam : register(b1) { float4x4 gVP; float3 gRight; float p0; float3 gUp; float p1; };

[maxvertexcount(4)]
void SpriteGS(point PointIn p[1], inout TriangleStream<QuadOut> stream)
{
    static const float2 corners[4] = { float2(-1, -1), float2(-1, 1), float2(1, -1), float2(1, 1) };
    [unroll] for (int i = 0; i < 4; ++i)
    {
        QuadOut o;
        float3 w = p[0].posW + (gRight * corners[i].x + gUp * corners[i].y) * p[0].size;
        o.pos = mul(gVP, float4(w, 1));
        o.uv = corners[i] * float2(0.5, -0.5) + 0.5;
        o.color = p[0].color;
        stream.Append(o);
    }
}

25.3 Layered rendering with GS instancing

On hardware with VPAndRTArrayIndexFromAnyShaderFeedingRasterizer, you can write SV_RenderTargetArrayIndex directly from the VS with instancing and skip the GS.

cbuffer CubeCB : register(b2) { float4x4 gFaceVP[6]; };
struct LayerOut { float4 pos : SV_Position; uint layer : SV_RenderTargetArrayIndex; };

[instance(6)]                  // run the GS 6 times per triangle
[maxvertexcount(3)]
void CubeGS(triangle GSIn tri[3], uint face : SV_GSInstanceID, inout TriangleStream<LayerOut> s)
{
    [unroll] for (int i = 0; i < 3; ++i)
    {
        LayerOut o;
        o.pos = mul(gFaceVP[face], float4(tri[i].posW, 1));
        o.layer = face;
        s.Append(o);
    }
}

25.4 Stream output

The GS (or VS) output can be written to buffers via stream output (D3D12_STREAM_OUTPUT_DESC) — historically used for GPU particles; compute shaders are now the usual choice.