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.