This tutorial shows how to use hardware tessellation to implement simple adaptive terrain rendering algorithm.
Hardware tessellation allows 3D application to generate geometry entirely on the GPU by using two programmable stages (hull shader and domain shader) and a fixed-function tessellator. This tutorial shows how to program these stages to generate simple adaptive terrain tessellation. It loads 1k x 1k height map and breaks it up into 32x32 blocks. For every edge of every block, hull shader computes tessellation factors based on the distance to the camera. Tessellator then takes these factors to generate triangulation, and domain shader evaluates position for every point in it.
Puget Sound height map and texture are downloaded from this page.
When tessellation is enabled, vertex shader processes every point in an input patch and can implement things like animation. We do not animate our terrain, so the vertex shader is almost pass-through. The only thing it does is computing the offset of the current block using the instance Id.
#include "structures.fxh"
cbuffer VSConstants
{
GlobalConstants g_Constants;
};
struct TerrainVSIn
{
uint BlockID : SV_VertexID;
};
void TerrainVS(in TerrainVSIn VSIn,
out TerrainVSOut VSOut)
{
uint BlockHorzOrder = VSIn.BlockID % g_Constants.NumHorzBlocks;
uint BlockVertOrder = VSIn.BlockID / g_Constants.NumHorzBlocks;
float2 BlockOffset = float2(
float(BlockHorzOrder) / g_Constants.fNumHorzBlocks,
float(BlockVertOrder) / g_Constants.fNumVertBlocks
);
VSOut.BlockOffset = BlockOffset;
}
Note that the vertex shader includes structures.fxh
file that contains definitions of structures
used by the shaders.
The hull shader consists of two parts. The first part processes all input patch control points (in contrast to the vertex shader that handles individual control points). It can generate another set of control points. In our case the shader input as well as output is one-control-point patch, so we only need to pass the block offset to the domain shader. Here we also need to define the properties of tessellation generated by the fixed-function stage:
[domain("quad")]
[partitioning("fractional_even")]
[outputtopology("triangle_ccw")]
[outputcontrolpoints(1)]
[patchconstantfunc("ConstantHS")]
[maxtessfactor( (float)(BLOCK_SIZE) )]
TerrainHSOut TerrainHS(InputPatch<TerrainVSOut, 1> inputPatch, uint uCPID : SV_OutputControlPointID )
{
TerrainHSOut HSOut = {inputPatch[0].BlockOffset};
return HSOut;
}
The second part is called constant function, whose purpose is to compute tessellation factors for the patch edges and interior. This tutorial uses a very simple method to evlaute the factors for every edge of every block: the edge factor is inversely proportional to the distance from the edge center to the camera:
float2 BlockOffset = inputPatch[0].BlockOffset;
float4 UV = float4(0.0, 0.0, 1.0, 1.0) / float2(g_Constants.fNumHorzBlocks, g_Constants.fNumVertBlocks).xyxy + BlockOffset.xyxy;
float2 leftEdgeCntrUV = float2(UV.x, (UV.y + UV.w)/2.0);
// Compute edge center position
float3 leftEdgeCntr = float3((leftEdgeCntrUV - float2(0.5, 0.5)) * g_Constants.LengthScale, 0);
// Sample height map at the location of the edge center
leftEdgeCntr.z = g_HeightMap.SampleLevel(g_HeightMap_sampler, leftEdgeCntrUV, 0) * g_Constants.HeightScale;
// Transform to camera space
float3 leftEdgeCntrViewSpace = mul(float4(leftEdgeCntr.xzy, 1.0), g_Constants.WorldView).xyz;
// Compute distance to camera
float distToLeftEdge = length(leftEdgeCntrViewSpace);
Out.Edges[0] = clamp( g_Constants.TessDensity / distToLeftEdge, 2.0, g_Constants.fBlockSize);
Interior factors are then computes as maximum of edge factors
Out.Inside[0] = min(Out.Edges[1], Out.Edges[3]);
Out.Inside[1] = min(Out.Edges[0], Out.Edges[2]);
The purpose of the domain shader is to evaluate the position of every vertex generated by the tessellator. It essentially replaces the vertex shader in the tessellation pipeline.
[domain("quad")]
/* partitioning = fractional_even, outputtopology = triangle_cw */
TerrainDSOut TerrainDS( TerrainHSConstFuncOut ConstFuncOut,
OutputPatch<TerrainHSOut, 1> QuadPatch,
float2 DomainUV : SV_DomainLocation)
{
TerrainDSOut Out;
float2 BlockOffset = QuadPatch[0].BlockOffset;
// Scale domain UV by the block size and add offset
float2 UV = DomainUV / float2(g_Constants.fNumHorzBlocks, g_Constants.fNumVertBlocks) + BlockOffset;
// Scale to world units
float2 XY = (UV - float2(0.5,0.5)) * g_Constants.LengthScale;
// Sample the height map
float Height = g_HeightMap.SampleLevel(g_HeightMap_sampler, UV, 0) * g_Constants.HeightScale;
float4 PosWorld = float4(XY, Height, 1.0);
// Apply world-view-projection matrix:
Out.Pos = mul(PosWorld.xzyw, g_Constants.WorldViewProj);
// Pass uv coordinates to the pixel shader
Out.uv = UV;
return Out;
}
Note that special comment on top of the function body is used by the HLSL->GLSL converter to generate proper tessellation evaluation shader code in OpenGL mode. Please visit this page for more details.
Pixel shader simply samples the color texture and is quite straightforward:
#include "structures.fxh"
Texture2D g_Texture;
SamplerState g_Texture_sampler;
float4 TerrainPS(TerrainDSOut ps_in) : SV_TARGET
{
return g_Texture.Sample(g_Texture_sampler, ps_in.uv);
}
The tutorial implements the technique shown in Tutorial07 to render wireframe. It uses geometry shader that goes after domain shader rather than vertex shader when tessellation is enabled to compute the distances to triangle edges. The distances are used by the pixel shader to generate smooth wireframe.
Pipeline state initialization is done in the same way as in previous tutorials. The only difference is that primitive topology is one-control-point patchlist:
PSOCreateInfo.GraphicsPipeline.PrimitiveTopology = PRIMITIVE_TOPOLOGY_1_CONTROL_POINT_PATCHLIST;
Two pipeline state objects are created. The first one renders terrain in normal mode, the second one initializes all 5 shader stages to render wireframe overlay.
Rendering is done as usual, with one primitive being one patch:
DrawAttribs DrawAttrs;
DrawAttrs.NumVertices = NumHorzBlocks * NumVertBlocks;
DrawAttrs.Flags = DRAW_FLAG_VERIFY_ALL;
m_pImmediateContext->Draw(DrawAttrs);