Chapter 21. Geometry and Tessellation Shaders

In this chapter, you examine two relatively new additions to the Direct3D graphics pipeline: geometry and tessellation shaders. These systems enable you to create vertices dynamically and even change the topology of a surface on the hardware. You learn how these systems work and implement a variety of interesting effects.

Motivation: Geometry Shaders

Geometry shaders have the capability to add and remove geometry from the graphics pipeline. That capability is absent from the pipeline stages we’ve discussed thus far and allows for some interesting applications. For example, lower-fidelity geometry can be sent to the pipeline, and the geometry shader can conjure up more vertices. Conversely, the geometry shader can excise entire primitives, or portions of them, from further processing.

Processing Primitives

The vertex shader operates on individual vertices, and the pixel shader operates on individual pixels, but the geometry shader operates on entire primitives. Recall that three basic primitive types exist: points, lines, and triangles (although they can be organized as lists and strips and can include adjacency data—see Chapter 1, “Introducing DirectX,” for a refresher). When programming a geometry shader, you must specify which type of primitive is to be processed using the keywords: point, line, triangle, lineadj, and triangleadj. That’s one of a few syntactical differences between geometry shaders and vertex or pixel shaders. The following code presents an example declaration of a geometry shader.

[maxvertexcount(3)]
void geometry_shader(point VS_OUTPUT IN[1], inout TriangleStream<GS_
OUTPUT> triStream) { // shader body }

The maxvertexcount attribute specifies the maximum number of vertices that can be output by the geometry shader (three, in this example). The shader’s first parameter defines the type of primitive (point), the structure of the input data (VS_OUTPUT), and how many vertices will be processed on each invocation of the geometry shader ([1]). The VS_OUTPUT data type is just the naming convention we’ve adopted for vertex shader output, and it can be any HLSL data type. Because the geometry shader stage is fed by the vertex shader stage (without active tessellation shaders), it makes sense that the output of the vertex shader becomes the input to the geometry shader. And with the inclusion of the geometry shader, the output of the geometry shader (by convention, GS_OUTPUT) becomes the input to the pixel shader.

The [n] array-style syntax, of the geometry shader’s first parameter, varies with the specified primitive type according to Table 21.1.

Image

Table 21.1 The Array Size of Geometry Shader Input, According to Primitive Type

The second parameter of the geometry shader is a StreamOutputObject<T> type and is always prefaced with the inout modifier. The stream-output object is a templated data type that comes in three flavors: PointStream, LineStream, and TriangleStream, corresponding to the primitive type you want to output. The PointStream type is analogous to the point list topology (it outputs just a set of points), but the LineStream and TriangleStream types output line and triangle strips, respectively.

The basic behavior of a geometry shader is to process incoming primitives and append vertices to the stream-output object. You append vertices with the StreamOutputObject<T>::Append() method. Extending the previous example, we define our geometry shader as accepting point primitives and outputting a triangle stream with a maximum of three vertices (just one triangle in the stream for each invocation of the geometry shader). The following code demonstrates the basic outline of such a shader:

struct VS_OUTPUT
{
    float4 ObjectPosition : POSITION;
};

struct GS_OUTPUT
{
    float4 Position : SV_Position;
};

[maxvertexcount(3)]
void geometry_shader(point VS_OUTPUT IN[1], inout TriangleStream<GS_
OUTPUT> triStream)
{
    GS_OUTPUT OUT = (GS_OUTPUT)0;

    for (int i = 0; i < 3; i++)
    {
        OUT.Position = // Some modification of the input point, followed
by transformation into homogeneous-clip space
        triStream.Append(OUT);
    }
}

A Point Sprite Shader

A common application of geometry shaders is point sprite expansion. A point sprite enables you to render a textured quad (or other shape) while specifying just a single vertex (a point). The point represents the center of the quad, and the geometry shader constructs the four surrounding vertices to form the quad. The quads could share a fixed size or could specify a custom size per-vertex. Furthermore, such a system could allow for a single texture (the same image mapped to every quad) or an array of textures (where the index into the array is specified per vertex). Listing 21.1 presents a point sprite shader with a fixed texture and per-vertex quad sizes. This shader billboards the quad; it positions the vertices so that the quad always faces the camera. Two billboarding techniques are common: spherical billboarding, which orients the object toward the camera, regardless of axis; and cylindrical billboarding, which constrains the rotation about a single axis (the y-axis, for example). Cylindrical billboarding is useful for simulating trees, where, for example, each tree should be “planted” on the ground but always oriented toward the camera to give the illusion of a 3D tree. The shader in Listing 21.1 uses spherical billboarding.

Listing 21.1 A Spherical Billboarding Point Sprite Shader


/************* Resources *************/

static const float2 QuadUVs[4] = {  float2(0.0f, 1.0f), // v0,
                                    lower-left
                                    float2(0.0f, 0.0f), // v1,
                                    upper-left

                                    float2(1.0f, 0.0f), // v2,
                                    upper-right

                                    float2(1.0f, 1.0f)  // v3,
                                    lower-right

                                 };

cbuffer CBufferPerFrame
{
    float3 CameraPosition : CAMERAPOSITION;
    float3 CameraUp;
}

cbuffer CBufferPerObject
{
    float4x4 ViewProjection;
}

Texture2D ColorTexture;

SamplerState ColorSampler
{
    Filter = MIN_MAG_MIP_LINEAR;
    AddressU = WRAP;
    AddressV = WRAP;
};

/************* Data Structures *************/

struct VS_INPUT
{
    float4 Position : POSITION;
    float2 Size : SIZE;
};

struct VS_OUTPUT
{
    float4 Position : POSITION;
    float2 Size : SIZE;
};

struct GS_OUTPUT
{
    float4 Position : SV_Position;
    float2 TextureCoordinate : TEXCOORD;
};

/************* Vertex Shader *************/

VS_OUTPUT vertex_shader(VS_INPUT IN)
{
    VS_OUTPUT OUT = (VS_OUTPUT)0;

    OUT.Position = IN.Position;
    OUT.Size = IN.Size;

    return OUT;
}

/************* Geometry Shader *************/

[maxvertexcount(6)]
void geometry_shader(point VS_OUTPUT IN[1], inout TriangleStream<GS_
OUTPUT> triStream)
{
    GS_OUTPUT OUT = (GS_OUTPUT)0;

    float2 halfSize = IN[0].Size / 2.0f;
    float3 direction = CameraPosition - IN[0].Position.xyz;
    float3 right = cross(normalize(direction), CameraUp);

    float3 offsetX = halfSize.x * right;
    float3 offsetY = halfSize.y * CameraUp;

    float4 vertices[4];
    vertices[0] = float4(IN[0].Position.xyz + offsetX - offsetY, 1.0f);
// lower-left
    vertices[1] = float4(IN[0].Position.xyz + offsetX + offsetY, 1.0f);
// upper-left
    vertices[2] = float4(IN[0].Position.xyz - offsetX + offsetY, 1.0f);
// upper-right
    vertices[3] = float4(IN[0].Position.xyz - offsetX - offsetY, 1.0f);
// lower-right

    // tri: 0, 1, 2
    OUT.Position = mul(vertices[0], ViewProjection);
    OUT.TextureCoordinate = QuadUVs[0];
    triStream.Append(OUT);

    OUT.Position = mul(vertices[1], ViewProjection);
    OUT.TextureCoordinate = QuadUVs[1];
    triStream.Append(OUT);

    OUT.Position = mul(vertices[2], ViewProjection);
    OUT.TextureCoordinate = QuadUVs[2];
    triStream.Append(OUT);
    triStream.RestartStrip();

    // tri: 0, 2, 3
    OUT.Position = mul(vertices[0], ViewProjection);
    OUT.TextureCoordinate = QuadUVs[0];
    triStream.Append(OUT);

    OUT.Position = mul(vertices[2], ViewProjection);
    OUT.TextureCoordinate = QuadUVs[2];
    triStream.Append(OUT);

    OUT.Position = mul(vertices[3], ViewProjection);
    OUT.TextureCoordinate = QuadUVs[3];
    triStream.Append(OUT);
}

/************* Pixel Shader *************/

float4 pixel_shader(GS_OUTPUT IN) : SV_Target
{
    return ColorTexture.Sample(ColorSampler, IN.TextureCoordinate);
}

/************* Techniques *************/

technique11 main11
{
    pass p0
    {
        SetVertexShader(CompileShader(vs_5_0, vertex_shader()));
        SetGeometryShader(CompileShader(gs_5_0, geometry_shader()));
        SetPixelShader(CompileShader(ps_5_0, pixel_shader()));
    }
}


No lighting model is applied in this example, in order to focus on the geometry shader. The pixel shader just samples the color texture. The vertex shader, too, is extremely simple—it merely passes through the position of the vertex and size of the quad to construct. The work is primarily done in the geometry shader, which takes in a single point, constructs four, and then outputs six total vertices (two triangles in a list, duplicating two of the constructed points). Because the incoming point represents the center of the quad, you build up the four surrounding vertex positions at half the size horizontally and vertically along the plane of quad. You compute the vector representing the angle of the plane (with regard to the camera) by first calculating the direction vector from the center point to the camera (this can also be used as the surface normal). In this example, the center points are already in world space and, therefore, do not need to be transformed before calculation against the camera position. The orthogonal surface vector (named right) is created from the cross product of the direction vector and the camera’s up vector. The four vertices are positioned along the right vector using the horizontal and vertical offsets.

Finally, the vertices of the two triangles are appended to the stream-output object with their positions transformed to homogeneous clip space and their texture coordinates set. Note the invocation of the StreamOutputObject<T>::RestartStrip() method. Recall that LineStream and TriangleStream types output strips, but you can emulate a list by restarting the strip between objects. You can rewrite this shader to output a triangle strip using the code in Listing 21.2.

Listing 21.2 Updated Point Sprite Geometry Shader Using Triangle Strips


static const float2 QuadStripUVs[4] = { float2(0.0f, 1.0f), // v0,                                         lower-left
                                        float2(0.0f, 0.0f), // v1,                                         upper-left
                                        float2(1.0f, 1.0f), // v2,                                         lower-right
                                        float2(1.0f, 0.0f)  // v3,                                         upper-right
                                      };

[maxvertexcount(4)]
void geometry_shader(point VS_OUTPUT IN[1], inout TriangleStream<GS_
OUTPUT> triStream)
{
    GS_OUTPUT OUT = (GS_OUTPUT)0;

    float2 halfSize = IN[0].Size / 2.0f;
    float3 direction = CameraPosition - IN[0].Position.xyz;
    float3 right = cross(normalize(direction), CameraUp);

    float3 offsetX = halfSize.x * right;
    float3 offsetY = halfSize.y * CameraUp;
    float4 vertices[4];
    vertices[0] = float4(IN[0].Position.xyz + offsetX - offsetY, 1.0f);
// lower-left
    vertices[1] = float4(IN[0].Position.xyz + offsetX + offsetY, 1.0f);
// upper-left
    vertices[2] = float4(IN[0].Position.xyz - offsetX - offsetY, 1.0f);
// lower-right
    vertices[3] = float4(IN[0].Position.xyz - offsetX + offsetY, 1.0f);
// upper-right

    [unroll]
    for (int i = 0; i < 4; i++)
    {
        OUT.Position = mul(vertices[i], ViewProjection);
        OUT.TextureCoordinate = QuadStripUVs[i];

        triStream.Append(OUT);
    }
}


Note how the vertex order has changed between the two iterations to accommodate the triangle strip.

Using the geometry shader from the CPU side requires no special coding constructs. In the geometry shader demo (full source code available online), a random set of points and quad sizes is created and rendered with the code in Listing 21.3.

Listing 21.3 Initialization and Rendering for the Geometry Shader Demo


void GeometryShaderDemo::Initialize()
{
    SetCurrentDirectory(Utility::ExecutableDirectory().c_str());

    // Initialize the material
    mEffect = new Effect(*mGame);
    mEffect->LoadCompiledEffect(L"Content\Effects\PointSprite.cso");
    mMaterial = new PointSpriteMaterial();
    mMaterial->Initialize(*mEffect);

    mPass = mMaterial->CurrentTechnique()->Passes().at(0);
    mInputLayout = mMaterial->InputLayouts().at(mPass);

    UINT maxRandomPoints = 100;
    float maxDistance = 10;
    float minSize = 2;
    float maxSize = 2;

    std::random_device randomDevice;
    std::default_random_engine randomGenerator(randomDevice());
    std::uniform_real_distribution<float> distanceDistribution
(-maxDistance, maxDistance);
    std::uniform_real_distribution<float> sizeDistribution(minSize,
maxSize);

    // Randomly generate points
    std::vector<VertexPositionSize> vertices;
    vertices.reserve(maxRandomPoints);
    for (UINT i = 0; i < maxRandomPoints; i++)
    {
        float x = distanceDistribution(randomGenerator);
        float y = distanceDistribution(randomGenerator);
        float z = distanceDistribution(randomGenerator);

        float size = sizeDistribution(randomGenerator);

        vertices.push_back(VertexPositionSize(XMFLOAT4(x, y, z, 1.0f),
XMFLOAT2(size, size)));
    }

    mVertexCount = vertices.size();
    mMaterial->CreateVertexBuffer(mGame->Direct3DDevice(),
&vertices[0], mVertexCount, &mVertexBuffer);

    std::wstring textureName = L"Content\Textures\BookCover.png";
    HRESULT hr = DirectX::CreateWICTextureFromFile(mGame->
Direct3DDevice(), mGame->Direct3DDeviceContext(), textureName.c_str(),
nullptr, &mColorTexture);
    if (FAILED(hr))
    {
        throw GameException("CreateWICTextureFromFile() failed.", hr);
    }
}

void GeometryShaderDemo::Draw(const GameTime& gameTime)
{
    ID3D11DeviceContext* direct3DDeviceContext =
mGame->Direct3DDeviceContext();
    direct3DDeviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_
TOPOLOGY_POINTLIST);
    direct3DDeviceContext->IASetInputLayout(mInputLayout);

    UINT stride = mMaterial->VertexSize();
    UINT offset = 0;
    direct3DDeviceContext->IASetVertexBuffers(0, 1, &mVertexBuffer,
&stride, &offset);

    mMaterial->ViewProjection() << mCamera->ViewMatrix() *
mCamera->ProjectionMatrix();
    mMaterial->CameraPosition() << mCamera->PositionVector();
    mMaterial->CameraUp() << mCamera->UpVector();
    mMaterial->ColorTexture() << mColorTexture;

    mPass->Apply(0, direct3DDeviceContext);

    direct3DDeviceContext->Draw(mVertexCount, 0);

    direct3DDeviceContext->GSSetShader(nullptr, nullptr, 0);
}


Figure 21.1 shows the output of this application.

Image

Figure 21.1 Output of the geometry shader demo.

Primitive IDs

Each primitive coming through the geometry shader can be tagged with a unique identifier (unique per draw call, but not between calls). This is done through the SV_PrimitiveID semantic attached to an unsigned integer. You can use it for some interesting effects. Listing 21.4 uses the primitive ID to generate a quad size instead of passing in the size per vertex.

Listing 21.4 Example of SV_PrimitiveID Usage


[maxvertexcount(4)]
void geometry_shader_nosize(point VS_NOSIZE_OUTPUT IN[1], uint
primitiveID : SV_PrimitiveID, inout TriangleStream<GS_OUTPUT>
triStream)
{
    GS_OUTPUT OUT = (GS_OUTPUT)0;

    float size = primitiveID + 1;

    float2 halfSize = size / 2.0f;

    // Remaining code identical to previous iteration, and removed for
brevity

}


If you update the vertex buffer creation code (to produce points at regular intervals along the x-axis), this shader creates the output in Figure 21.2.

Image

Figure 21.2 Output of the geometry shader demo, with sizes based on the primitive ID.

The vertex shader also supports an ID, using the SV_VertexID semantic, with analogous behavior. These IDs need not be used strictly within their corresponding shader stages. You could pass an ID through an output variable from one shader stage to the next.

Motivation: Tessellation Shaders

Direct3D 11 introduced hardware tessellation. Tessellation refers to the subdivision of surfaces—adding geometry to a surface to increase its fidelity. This allows low-poly models to be transmitted to the GPU without sacrificing the quality of high-poly rendering. The throughput of the bus between the CPU and the GPU is often a bottleneck, so reducing the amount of data sent over the bus is a good idea. Furthermore, without hardware tessellation, 3D models are often created at multiple resolutions (such as low, medium, and high) with a particular level of detail (LOD) rendered with respect to some metric (such as the distance of the object to the camera). This means additional work by an artist or an additional step in the content pipeline (to automatically produce the LODs), as well as more storage and processing requirements for the model. Hardware tessellation makes dynamic LODs possible using just one, low-poly version of the model.

Before the tessellation pipeline stages were added, the geometry shader was used for surface subdivision, but now there are dedicated tessellation stages: the hull-shader stage, the tessellation stage, and the domain-shader stage. Recall the graphics pipeline in Figure 21.3 and the tessellation stages between the vertex and geometry shader stages. The next sections discuss each of these stages and then provide demonstration applications.

Image

Figure 21.3 The Direct3D 11 graphics pipeline.

The Hull Shader Stage

With tessellation enabled, the vertex shader processes control point patch lists instead of the traditional point, line, or triangle primitives. A patch is a surface whose shape is defined by its associated control points. Direct3D 11 supports patches with 1 to 32 control points. A patch with three control points is merely a triangle, and a quad has four control points. Patches with more than four control points refer to surfaces constructed from Bezier curves. The hull shader stage accepts these input control points and then outputs a set of control points, tessellation factors, and any additional data required for future stages. Tessellation factors specify the number of subdivisions for each patch. Listing 21.5 presents an example hull shader for triangle patches.

Listing 21.5 A Hull Shader for Triangle Patches


struct VS_OUTPUT
{
    float4 ObjectPosition : POSITION;
};

struct HS_CONSTANT_OUTPUT
{
    float EdgeFactors[3] : SV_TessFactor;
    float InsideFactor : SV_InsideTessFactor;
};

struct HS_OUTPUT
{
    float4 ObjectPosition : POSITION;
};

[domain("tri")]
[partitioning("integer")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(3)]
[patchconstantfunc("constant_hull_shader")]
HS_OUTPUT hull_shader(InputPatch<VS_OUTPUT, 3> patch, uint controlPointID : SV_OutputControlPointID)
{
    HS_OUTPUT OUT = (HS_OUTPUT)0;

    OUT.ObjectPosition = patch[controlPointID].ObjectPosition;

    return OUT;
}


This listing resembles other HLSL shaders, but with syntax that’s specific to hull shaders. First, note that the initial parameter to the hull shader is of type InputPatch<T, n>, where T is the output type from the vertex shader and n is the number of control points in the input patch. The second parameter is the unique identifier of each output control point. The hull shader can redefine the patch topology, with either greater or fewer output control points than input points. The hull shader is invoked once per output control point, and the outputcontrolpoints attribute specifies the total number of output points. In this example, each hull shader output is defined by the HS_OUTPUT structure.

A number of attributes are associated with the hull shader, starting with the domain attribute. This attribute identifies the tessellation primitive to be processed by the hull shader. It can be tri (triangles), quad (rectangles), or isoline (rectangles made of sets of lines with multiple control points). The partitioning attribute specifies how the fractional components of tessellation factors are applied. A partitioning value of integer or pow2 rounds fractional or non-power-of-two tessellation factors to the next acceptable value, respectively. A value of fractional-even or fractional-odd does not replace the fractional component, and it facilitates a smoother transition between tessellation factors. The outputtopology attribute defines the primitive type produced by the tessellator and can be point, line, triangle_cw, or triangle_ccw. Finally, the patchconstantfunc attribute defines the function for computing patch constant data (tessellation factors), the so-called constant hull shader. Listing 21.6 gives an example of a constant hull shader.

Listing 21.6 A Constant Hull Shader


cbuffer CBufferPerFrame
{
    float TessellationEdgeFactors[3];
    float TessellationInsideFactor;
}

struct HS_CONSTANT_OUTPUT
{
    float EdgeFactors[3] : SV_TessFactor;
    float InsideFactor : SV_InsideTessFactor;
};

HS_CONSTANT_OUTPUT constant_hull_shader(InputPatch<VS_OUTPUT, 3> patch)
{
    HS_CONSTANT_OUTPUT OUT = (HS_CONSTANT_OUTPUT)0;

    [unroll]
    for (int i = 0; i < 3; i++)
    {
        OUT.EdgeFactors[i] = TessellationEdgeFactors[i];
    }

    OUT.InsideFactor = TessellationInsideFactor;

    return OUT;
}


The constant hull shader is invoked once per patch and must output values marked with the SV_TessFactor and SV_InsideTessFactor semantics. The SV_TessFactor semantic denotes edge tessellation factors—the number of subdivisions for each edge in the patch. The variable with the SV_InsideTessFactor semantic specifies the number of inside subdivisions. The number of edge and inside tessellation factors must match the patch topology. For example, triangle patches have three edge factors and one inside factor. Quad patches have four edge factors and two inside factors (for horizontal and vertical subdivisions). Each tessellation factor has the range [0, 64], and the values need not be uniformly applied. If any tessellation factor is 0, the patch is discarded from further processing. A tessellation factor of 1 implies no tessellation. The code in Listing 21.6 applies tessellation factors that are input from the application. These are used in the first full demonstration of tessellation, where you vary the edge and inside tessellation factors for triangle and quad patches.

The Tessellation Stage

The next stage is the tessellation stage, and it is not programmable. This stage performs the actual surface subdivision and is controlled by the output from the hull shader stage. Figures 21.4 and 21.5 show a tessellated triangle and quad with different edge and inside tessellation factors.

Image

Figure 21.4 A tessellated triangle with various tessellation factors.

Image

Figure 21.5 A tessellated quad with various tessellation factors.

The Domain Shader Stage

The domain shader is invoked for each vertex coming from the tessellation stage and outputs vertices in homogeneous clip space. The data from the domain shader is passed to the geometry shader (if one exists) or to the pixel shader (if the geometry shader stage is disabled). Thus, one of the outputs from the domain shader is typically marked with the SV_Position semantic, just as you would have done for the vertex shader output without tessellation. Listing 21.7 presents an example domain shader.

Listing 21.7 A Domain Shader


struct DS_OUTPUT
{
    float4 Position : SV_Position;
};

[domain("tri")]
DS_OUTPUT domain_shader(HS_CONSTANT_OUTPUT IN, float3 uvw : SV_
DomainLocation, const OutputPatch<HS_OUTPUT, 3> patch)
{
    DS_OUTPUT OUT = (DS_OUTPUT)0;

    float3 objectPosition = uvw.x * patch[0].ObjectPosition.xyz + uvw.y
* patch[1].ObjectPosition.xyz + uvw.z * patch[2].ObjectPosition.xyz;

    OUT.Position = mul(float4(objectPosition, 1.0f),
WorldViewProjection);

    return OUT;
}


As with the hull shader, the domain attribute specifies the patch topology. The first parameter to the domain shader is the output data type from the constant hull shader, and the hull shader output is available in the third parameter of type OutputPatch<T, n>. The second parameter describes the position of the vertex in patch space. For triangle patches, these are barycentric coordinates. In brief, barycentric coordinates allow any point within a triangle to be written as a weighted sum of the three triangle vertices. Specifically:

Positiontri = u*VertexPos0+v*VertexPos1+w*VertexPos2

For quads, this parameter is two-dimensional (uv) and mimics texture coordinate space (range: [0,1]; u: horizontal axis; v: vertical axis). Calculating the vertex position is a bilinear interpolation. Specifically:

Position0 = lerp(VertexPos0, VertexPos1,u)

Position1 = lerp(VertexPos2, VertexPos3,u)

Positionquad = lerp(Position0, Position1, v)

Listings 21.5, 21.6, and 21.7 put most of the pieces together for a basic tessellation demo using triangles. Listing 21.8 presents the complete shader for tessellating quads (source code for both shaders is available on the book’s companion website).

Listing 21.8 A Quad Tessellation Shader


/************* Resources *************/
static const float4 ColorWheat = { 0.961f, 0.871f, 0.702f, 1.0f };

cbuffer CBufferPerFrame
{
    float TessellationEdgeFactors[4];
    float TessellationInsideFactors[2];
}

cbuffer CBufferPerObject
{
    float4x4 WorldViewProjection;
}

/************* Data Structures *************/

struct VS_INPUT
{
    float4 ObjectPosition : POSITION;
};

struct VS_OUTPUT
{
    float4 ObjectPosition : POSITION;
};

struct HS_CONSTANT_OUTPUT
{
    float EdgeFactors[4] : SV_TessFactor;
    float InsideFactors[2] : SV_InsideTessFactor;
};

struct HS_OUTPUT
{
    float4 ObjectPosition : POSITION;
};

struct DS_OUTPUT
{
    float4 Position : SV_Position;
};

/************* Vertex Shader *************/

VS_OUTPUT vertex_shader(VS_INPUT IN)
{
    VS_OUTPUT OUT = (VS_OUTPUT)0;

    OUT.ObjectPosition = IN.ObjectPosition;

    return OUT;
}

/************* Hull Shaders *************/

HS_CONSTANT_OUTPUT constant_hull_shader(InputPatch<VS_OUTPUT, 4> patch,
uint patchID : SV_PrimitiveID)
{
    HS_CONSTANT_OUTPUT OUT = (HS_CONSTANT_OUTPUT)0;

    [unroll]
    for (int i = 0; i < 4; i++)
    {
        OUT.EdgeFactors[i] = TessellationEdgeFactors[i];
    }

    OUT.InsideFactors[0] = TessellationInsideFactors[0];
    OUT.InsideFactors[1] = TessellationInsideFactors[1];

    return OUT;
}

[domain("quad")]
[partitioning("integer")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(4)]
[patchconstantfunc("constant_hull_shader")]
HS_OUTPUT hull_shader(InputPatch<VS_OUTPUT, 4> patch, uint
controlPointID : SV_OutputControlPointID)
{
    HS_OUTPUT OUT = (HS_OUTPUT)0;

    OUT.ObjectPosition = patch[controlPointID].ObjectPosition;

    return OUT;
}

/************* Domain Shader *************/

[domain("quad")]
DS_OUTPUT domain_shader(HS_CONSTANT_OUTPUT IN, float2 uv : SV_
DomainLocation, const OutputPatch<HS_OUTPUT, 4> patch)
{
    DS_OUTPUT OUT;

    float4 v0 = lerp(patch[0].ObjectPosition, patch[1].ObjectPosition,
uv.x);
    float4 v1 = lerp(patch[2].ObjectPosition, patch[3].ObjectPosition,
uv.x);
    float4 objectPosition = lerp(v0, v1, uv.y);

    OUT.Position = mul(float4(objectPosition.xyz, 1.0f),
WorldViewProjection);

    return OUT;
}

/************* Pixel Shader *************/

float4 pixel_shader(DS_OUTPUT IN) : SV_Target
{
    return ColorWheat;
}

/************* Techniques *************/

technique11 main11
{
    pass p0
    {
        SetVertexShader(CompileShader(vs_5_0, vertex_shader()));
        SetHullShader(CompileShader(hs_5_0, hull_shader()));
        SetDomainShader(CompileShader(ds_5_0, domain_shader()));
        SetPixelShader(CompileShader(ps_5_0, pixel_shader()));
    }
}


In this listing, the vertex and hull shaders merely pass through the control points without modification. Then the constant hull shader applies the tessellation factors supplied by the CPU-side application. Next, the domain shader computes the tessellated position and transforms it into homogeneous clip space. Finally, the pixel shader just outputs a solid color for each pixel (because the demo application renders the model in wireframe mode).

A Basic Tessellation Demo

No particularly complicated C++ code is required to integrate the triangle and quad tessellation shaders from the last few sections. In a nutshell, you create the associated materials to integrate the two shaders, build vertex buffers for a single triangle and quad, and store some class members for the tessellation factors. In the demo, you can toggle between quad and triangle tessellation and modify the tessellation factors either uniformly or independently. Drawing a tessellated object follows the same patterns previously established but requires that you specify one of the control point patch list topologies. You use D3D11_PRIMITIVE_TOPOLOGY_3_CONTROL_POINT_PATCHLIST for triangles and D3D11_PRIMITIVE_TOPOLOGY_4_CONTROL_POINT_PATCHLIST for quads. Listing 21.9 presents the core rendering code for the demo.

Listing 21.9 Draw Code for the Basic Tessellation Demo


ID3D11DeviceContext* direct3DDeviceContext =
mGame->Direct3DDeviceContext();
direct3DDeviceContext->RSSetState(RasterizerStates::Wireframe);

if (mShowQuadTopology)
{
    direct3DDeviceContext->IASetInputLayout(mQuadInputLayout);
    direct3DDeviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_
TOPOLOGY_4_CONTROL_POINT_PATCHLIST);

    UINT stride = mQuadMaterial->VertexSize();
    UINT offset = 0;
    direct3DDeviceContext->IASetVertexBuffers(0, 1, &mQuadVertexBuffer,
&stride, &offset);

    mQuadMaterial->WorldViewProjection() << mCamera->ViewMatrix() *
mCamera->ProjectionMatrix();
    mQuadMaterial->TessellationEdgeFactors()
<< mTessellationEdgeFactors;
    mQuadMaterial->TessellationInsideFactors()
<< mTessellationInsideFactors;
    mQuadPass->Apply(0, direct3DDeviceContext);

    direct3DDeviceContext->Draw(4, 0);
}
else
{
    direct3DDeviceContext->IASetInputLayout(mTriInputLayout);
    direct3DDeviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_
TOPOLOGY_3_CONTROL_POINT_PATCHLIST);

    UINT stride = mTriMaterial->VertexSize();
    UINT offset = 0;
    direct3DDeviceContext->IASetVertexBuffers(0, 1, &mTriVertexBuffer,
&stride, &offset);

    std::vector<float> tessellationEdgeFactors(mTessellationEdge
Factors.begin(), mTessellationEdgeFactors.end() - 1);

    mTriMaterial->WorldViewProjection() << mCamera->ViewMatrix() *
mCamera->ProjectionMatrix();
    mTriMaterial->TessellationEdgeFactors() << tessellationEdgeFactors;
    mTriMaterial->TessellationInsideFactor()
<< mTessellationInsideFactors[0];
    mTriPass->Apply(0, direct3DDeviceContext);

    direct3DDeviceContext->Draw(3, 0);
}


Displacing Tessellated Vertices

The idea of tessellation is to yield high-fidelity rendering from low-fidelity models. But having a triangle or a quad subdivided on the same plane doesn’t provide that detail; a subdivided plane is still a plane. You have to displace the newly created vertices before your work can bear fruit. This involves the same concepts from Chapter 9, “Normal Mapping and Displacement Mapping”: a texture for the displacement values and a scale factor. Listing 21.10 presents a shader for tessellating a quad aligned on the xz-plane and displacing the vertices’ y-position according to the displacement map.

Listing 21.10 The QuadHeightmapTessellation.fx Effect


/************* Resources *************/
static const float4 ColorWheat = { 0.961f, 0.871f, 0.702f, 1.0f };

cbuffer CBufferPerFrame
{
    float TessellationEdgeFactors[4];
    float TessellationInsideFactors[2];
}

cbuffer CBufferPerObject
{
    float4x4 WorldViewProjection;
    float4x4 TextureMatrix;

    float DisplacementScale;
}

Texture2D Heightmap;

SamplerState TrilinearSampler
{
    Filter = MIN_MAG_MIP_LINEAR;
    AddressU = WRAP;
    AddressV = WRAP;
};

/************* Data Structures *************/

struct VS_INPUT
{
    float4 ObjectPosition : POSITION;
    float2 TextureCoordinate: TEXCOORD;
};

struct VS_OUTPUT
{
    float4 ObjectPosition : POSITION;
    float2 TextureCoordinate: TEXCOORD;
};

struct HS_CONSTANT_OUTPUT
{
    float EdgeFactors[4] : SV_TessFactor;
    float InsideFactors[2] : SV_InsideTessFactor;
};

struct HS_OUTPUT
{
    float4 ObjectPosition : POSITION;
    float2 TextureCoordinate: TEXCOORD;
};

struct DS_OUTPUT
{
    float4 Position : SV_Position;
};

/************* Vertex Shader *************/

VS_OUTPUT vertex_shader(VS_INPUT IN)
{
    VS_OUTPUT OUT = (VS_OUTPUT)0;

    OUT.ObjectPosition = IN.ObjectPosition;
    OUT.TextureCoordinate = mul(float4(IN.TextureCoordinate, 0, 1), TextureMatrix);

    return OUT;
}

/************* Hull Shaders *************/

HS_CONSTANT_OUTPUT constant_hull_shader(InputPatch<VS_OUTPUT, 4> patch,
uint patchID : SV_PrimitiveID)
{
    HS_CONSTANT_OUTPUT OUT = (HS_CONSTANT_OUTPUT)0;

    [unroll]
    for (int i = 0; i < 4; i++)
    {
        OUT.EdgeFactors[i] = TessellationEdgeFactors[i];
    }

    OUT.InsideFactors[0] = TessellationInsideFactors[0];
    OUT.InsideFactors[1] = TessellationInsideFactors[1];

    return OUT;
}

[domain("quad")]
[partitioning("integer")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(4)]
[patchconstantfunc("constant_hull_shader")]
HS_OUTPUT hull_shader(InputPatch<VS_OUTPUT, 4> patch, uint controlPointID : SV_OutputControlPointID)
{
    HS_OUTPUT OUT = (HS_OUTPUT)0;

    OUT.ObjectPosition = patch[controlPointID].ObjectPosition;
    OUT.TextureCoordinate = patch[controlPointID].TextureCoordinate;

    return OUT;
}

/************* Domain Shader *************/

[domain("quad")]
DS_OUTPUT domain_shader(HS_CONSTANT_OUTPUT IN, float2 uv : SV_
DomainLocation, const OutputPatch<HS_OUTPUT, 4> patch)
{
    DS_OUTPUT OUT;

    float4 v0 = lerp(patch[0].ObjectPosition, patch[1].ObjectPosition,
uv.x);
    float4 v1 = lerp(patch[2].ObjectPosition, patch[3].ObjectPosition,
uv.x);
    float4 objectPosition = lerp(v0, v1, uv.y);

    float2 texCoord0 = lerp(patch[0].TextureCoordinate, patch[1].
TextureCoordinate, uv.x);
    float2 texCoord1 = lerp(patch[2].TextureCoordinate, patch[3].
TextureCoordinate, uv.x);
    float2 textureCoordinate = lerp(texCoord0, texCoord1, uv.y);

    objectPosition.y = (2 * Heightmap.SampleLevel(TrilinearSampler,
textureCoordinate, 0).x - 1) * DisplacementScale;

    OUT.Position = mul(float4(objectPosition.xyz, 1.0f),
WorldViewProjection);

    return OUT;
}

/************* Pixel Shader *************/

float4 pixel_shader(DS_OUTPUT IN) : SV_Target
{
    return ColorWheat;
}

/************* Techniques *************/

technique11 main11
{
    pass p0
    {
        SetVertexShader(CompileShader(vs_5_0, vertex_shader()));
        SetHullShader(CompileShader(hs_5_0, hull_shader()));
        SetDomainShader(CompileShader(ds_5_0, domain_shader()));
        SetPixelShader(CompileShader(ps_5_0, pixel_shader()));
    }
}


This shader has a few interesting sections. First, observe that the texture coordinate, computed in the domain shader, is derived in the same way as the vertex position. Next, note how the displacement value is sampled and scaled to the range [-1, 1], where the grayscale value 0.5 represents no displacement. This allows dark values (less than 0.5) in the displacement map to recess and light values (greater than 0.5) to protrude from the surface. The displacement value is further modulated by the DisplacementScale shader variable.

Finally, notice how the texture coordinates are transformed in the vertex shader. This allows the texture to animate. For example, if you assign the TextureMatrix variable to a translation matrix (with horizontal and/or vertical translation) that updates as a function of time, your tessellated quad can be made to look like undulating waves. Figure 21.6 shows the output of the quad heightmap tessellation demo. The displacement map is rendered in the lower-left corner of the screen. Full source code for the demo application is available on the companion website.

Image

Figure 21.6 Output of the quad heightmap tessellation demo.

Dynamic Levels of Detail

You need not manually specify tessellation factors. Instead, you can base your tessellation amounts on the distance of the surface to the camera. This provides for a dynamic LOD system. Listing 21.11 presents the hull and domain shaders for such a system.

Listing 21.11 The Hull and Domain Shaders for a Dynamic Tessellation Effect


/************* Resources *************/

cbuffer CBufferPerFrame
{
    float3 CameraPosition : CAMERAPOSITION;
    int MaxTessellationFactor = 64;
    float MinTessellationDistance = 2.0f;
    float MaxTessellationDistance = 20.0f;
}

cbuffer CBufferPerObject
{
    float4x4 WorldViewProjection : WORLDVIEWPROJECTION;
    float4x4 World : WORLD;
}

/************* Hull Shaders *************/

HS_CONSTANT_OUTPUT distance_constant_hull_shader(InputPatch<VS_OUTPUT,
3> patch, uint patchID : SV_PrimitiveID)
{
    HS_CONSTANT_OUTPUT OUT = (HS_CONSTANT_OUTPUT)0;

    // Caclulate the center of the patch
    float3 objectCenter = (patch[0].ObjectPosition.xyz + patch[1].
ObjectPosition.xyz + patch[2].ObjectPosition.xyz) / 3.0f;
    float3 worldCenter = mul(float4(objectCenter, 1.0f), World).xyz;

    // Calculate uniform tessellation factor based on distance from the
camera

    float tessellationFactor = max(min(MaxTessellationFactor,
(MaxTessellationDistance - distance(worldCenter, CameraPosition))
/ (MaxTessellationDistance - MinTessellationDistance) *
MaxTessellationFactor), 1);
    [unroll]
    for (int i = 0; i < 3; i++)
    {
        OUT.EdgeFactors[i] = tessellationFactor;
    }

    OUT.InsideFactor = tessellationFactor;

    return OUT;
}

[domain("tri")]
[partitioning("integer")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(3)]
[patchconstantfunc("distance_constant_hull_shader")]
HS_OUTPUT distance_hull_shader(InputPatch<VS_OUTPUT, 3> patch, uint controlPointID : SV_OutputControlPointID)
{
    HS_OUTPUT OUT = (HS_OUTPUT)0;

    OUT.ObjectPosition = patch[controlPointID].ObjectPosition;
    OUT.TextureCoordinate = patch[controlPointID].TextureCoordinate;

    return OUT;
}

/************* Domain Shader *************/

[domain("tri")]
DS_OUTPUT domain_shader(HS_CONSTANT_OUTPUT IN, float3 uvw : SV_
DomainLocation, const OutputPatch<HS_OUTPUT, 3> patch)
{
    DS_OUTPUT OUT = (DS_OUTPUT)0;

    float3 objectPosition = uvw.x * patch[0].ObjectPosition.xyz + uvw.y
* patch[1].ObjectPosition.xyz + uvw.z * patch[2].ObjectPosition.xyz;
    float2 textureCoordinate = uvw.x * patch[0].TextureCoordinate
+ uvw.y * patch[1].TextureCoordinate + uvw.z * patch[2].
TextureCoordinate;

    OUT.Position = mul(float4(objectPosition, 1.0f),
WorldViewProjection);
    OUT.TextureCoordinate = textureCoordinate;

    return OUT;
}


The constant hull shader first calculates the center of the triangle patch and then transforms the position into world space. Next, a uniform tessellation factor is calculated using the maximum and minimum distances (the range) for tessellation. When the object is beyond the maximum distance, the tessellation factor is clamped at 1 (no tessellation). When the object is within the minimum distance, the tessellation factor is clamped at the passed-in MaxTessellationFactor variable. Between the minimum and maximum distances, the tessellation factor falls off as the distance increases. Figure 21.7 shows the output of this system against a sphere. As you can see, the surfaces closer to the camera are more tessellated than those farther away.

Image

Figure 21.7 Output of the dynamic tessellation demo.


Note

For performance reasons, it’s best to disable the tessellation stages when the object is far enough away that no tessellation will occur. You can do this by testing the distance between the camera and the object’s bounding volume on the CPU and switching between shader techniques.


Summary

In this chapter, you discovered geometry and tessellation shaders. You implemented a point sprite system to demonstrate geometry shaders and a number of applications for hardware tessellation.

Exercises

1. Experiment with geometry shaders. Modify the point sprite demo to output different vertex topologies.

2. Explore the three tessellation demos on the book’s companion website. For the basic tessellation shader demo (whose output is shown in Figures 21.4 and 21.5), vary the edge and inside tessellation factors uniformly and nonuniformly, and observe the results. For the dynamic level-of-detail tessellation demo, experiment with the max tessellation factor and minimum and maximum tessellation distances, and observe the results. For the heightmap tessellation demo, alter the heightmap and the transformation for animating the UVs, and observe the results.

3. Modify the dynamic level-of-detail tessellation shader to use an icosaheron model (included on the companion website). Run the demo and observe how the shape is not made more spherical as tessellation increases. Then modify the associated domain shader to displace the vertices along a unit sphere (to normalize the computed object position), and observe the results.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.12.160.63