Geometry shaders are a bit more complicated
than the shaders you’ve worked with so far. Unlike vertex and pixel
shaders, geometry shaders are able to output more or less than they
take in. Vertex shaders must accept a single vertex and output a single
vertex; pixel shaders work the same way. Geometry shaders, on the other
hand, can be used to remove or add vertices as they pass through this
portion of the pipeline. This is useful if you want to clip geometry
based on some set criteria or maybe you want to increase the resolution
of the object through tessellation.
Geometry
shaders exist within an effect file between the vertex and pixel shader
stages. Since geometry shaders are optional, you may commonly see them
set to a NULL value in effect techniques. When a geometry
shader is necessary though, it is set in an identical way as vertex and
pixel shaders. The technique shown next defines all three.
// Define the technique
technique10 Render
{
pass P0
{
SetVertexShader( CompileShader(vs_4_0, VS()) );
SetGeometryShader( CompileShader(gs_4_0, GS()) );
SetPixelShader( CompileShader(ps_4_0, PS()) );
}
}
To
give you an example of what geometry shaders can do, take a look at the
following code. It contains the full geometry shader function along
with the structures and constant buffer to support it. The job of this
particular shader is to take as input a single vertex position and
generate a full triangle to send along to the pixel shader.
// The corners of a triangle, used in the geometry
// shader to create each triangle
cbuffer TriangleVerts
{
float3 triPositions[3] =
{
float3( -0.25, 0.25, 0 ),
float3( 0.25, 0.25, 0 ),
float3( -0.25, -0.25, 0 ),
};
};
// PS_INPUT - input variables to the pixel shader
// This struct is created and filled in by the
// vertex shader
struct PS_INPUT
{
// Only the pixel shader input uses the fully transformed position
float4 Pos : SV_POSITION;
float4 Color : COLOR0;
};
// output structure for the vertex shader
struct VS_OUT
{
float4 Pos : POSITION;
float4 Color: COLOR0;
};
///////////////////////////////////////////////
// Geometry Shader
///////////////////////////////////////////////
[maxvertexcount(3)]
void GS(point VS_OUT input[1], inout TriangleStream<PS_INPUT> triangleStream)
{
PS_INPUT psInput;
// create the new triangles
for (int i = 0; i < 3; i++)
{
// hold the vertices for the triangle
float3 position = triPositions[i];
// move the triangle vertices based on the point position
position = position + input[0].Pos;
// Multiply the new vertices by the projection matrix
psInput.Pos = mul(float4(position, 1.0), Projection);
// pass the color on through
psInput.Color = input[0].Color;
// add this triangle to the triangle stream
triangleStream.Append(psInput);
}
}
Geometry Shader Function Declaration
Geometry
shaders are declared slightly differently than vertex and pixel
shaders. Instead of designating the return type for the function, the
vertices this shader outputs are done so in the parameter list. The
geometry shader itself has a return type of void.
Every
geometry shader needs to designate the number of vertices that it will
be returning and must be declared above the function using the maxvertexcount keyword. This particular function is meant to return a single triangle so three vertices are required.
[maxvertexcount(3)]
void GS(point VS_OUT input[1], inout TriangleStream<PS_INPUT> triangleStream)
Geometry shader functions take two parameters.
The
first parameter is an array of vertices for the incoming geometry. The
type of geometry being passed into this function is based on the
topology you used in your application code. Since this example used a
point list, the type of geometry coming into the function is a point,
and there is only one item in the array. If the application used a
triangle list then the type would be set as “triangle” and three
vertices would be in the array.
The second
parameter is the stream object. The stream object is the list of
vertices that are outputted from the geometry shader and passed to the
next shader stage. This list of vertices must use the structure format
that is used as the input to the pixel shader. Based on the type of
geometry you’re creating within this shader, there are three stream
object types available:
PointStream— The shader should expect to output a series of points.
TriangleStream— The shader will output a triangle strip.
LineStream— A series of lines.
When
adding vertices to a stream object, it will be occasionally necessary
to end the strip being created. In that instance, you should make a
call to the restartstrip function. This is useful when generating a series of interconnected triangles.
The Geometry Shader Explained
The
geometry shader in the previous example generates three vertices for
every vertex passed to it. The vertices are created by taking the
initial position vertex and merging it with the vertex positions found
in the triPositions variable. This variable holds a list of three vertices that are used to create a triangle at any position.
Because each triangle the shader is trying to create requires three vertices, a for loop within the shader loops three times generating a new vertex for each point of the triangle.
The
final triangle points are then multiplied by the projection matrix to
create the final positions. Each point in the triangle is added to the
triangle stream after its creation. Figure 1 shows what the generated triangles look like.