Afternoon all,
Any of you who follow my
journal (
D3D11 Tessellation Pipeline,
SO for debugging,
Summit write-up) will probably be aware that I've been playing around with the Direct3D 11 tessellation features (specifically the Domain and Hull Shader stages).
Over the last couple of evenings I've been implementing an adaptation of Greg Snook's "Simplified terrain using interlocking tiles" article from Game Programming Gems 2.
So I put together a couple of videos of my implementation (
I'll be writing it up properly later):
(Click for YouTube video)
Wanted to throw it out here for the GP&T forum to comment on. Particularly interested in whether people see it as a useful and viable rendering technique.
In a nutshell it examines each quad patch and decides an LOD value for it. Currently I do this just by scaling according to distance from the camera, but a previous (software-only) implementation I made added a bias factor based on the standard deviation of the patch - e.g. a flat piece of terrain was biased towards a lower LOD and a bumpy piece of terrain was given a higher LOD.
The Hull Shader evaluates this for the current patch as well as the four adjacent patches and uses the adjacency information to create the 'skirts' that join patches of different LOD's.
This allows the fixed function tessellator to create a grid of vertices, something like:

The Domain Shader then performs displacement mapping by sampling a conventional greyscale heightmap to create the actual terrain as finally rendered. I also threw in a quick Sobel filter to generate the per-vertex normals from the heightmap.
Hull Shader Code
float3 ComputePatchMidPoint(float3 cp0, float3 cp1, float3 cp2, float3 cp3)
{
return (cp0 + cp1 + cp2 + cp3) / 4.0f;
}
float ComputePatchLOD(float3 midPoint)
{
return lerp(minLOD, maxLOD, (1.0f - (saturate(distance(cameraPosition, midPoint) / maxDistance))));
}
HS_PER_PATCH_OUTPUT hsPerPatch( InputPatch<VS_OUTPUT, 12> ip, uint PatchID : SV_PrimitiveID )
{
HS_PER_PATCH_OUTPUT o = (HS_PER_PATCH_OUTPUT)0;
float3 midPoints[] =
{
ComputePatchMidPoint( ip[0].position, ip[1].position, ip[2].position, ip[3].position )
, ComputePatchMidPoint( ip[2].position, ip[3].position, ip[4].position, ip[5].position )
, ComputePatchMidPoint( ip[1].position, ip[3].position, ip[6].position, ip[7].position )
, ComputePatchMidPoint( ip[0].position, ip[1].position, ip[8].position, ip[9].position )
, ComputePatchMidPoint( ip[0].position, ip[2].position, ip[10].position, ip[11].position )
};
float dist[] =
{
ComputePatchLOD( midPoints[0] )
, ComputePatchLOD( midPoints[1] )
, ComputePatchLOD( midPoints[2] )
, ComputePatchLOD( midPoints[3] )
, ComputePatchLOD( midPoints[4] )
};
o.insideTesselation[0] =
o.insideTesselation[1] = dist[0];
o.edgeTesselation[0] = min( dist[0], dist[4] );
o.edgeTesselation[1] = min( dist[0], dist[3] );
o.edgeTesselation[2] = min( dist[0], dist[2] );
o.edgeTesselation[3] = min( dist[0], dist[1] );
return o;
}
Not exactly complicated, is it? [grin]
It also works very nicely as it requires no CPU intervention - a single draw call blasts off a grid of vertices and associated patch indices and the rest is handled entirely in shaders. No need to upload any pre-computed LOD information and no need for multiple draw calls to change constants between different patches.
There are a couple of problems though:
- Popping - watch the videos and you can easily see the transitions [wink]. The HS/DS stages should be able to provide information necessary for geomorphing, but I've not tried that yet.
- Access to the final mesh - Being totally GPU rendered it is at best tricky for any physics or animation algorithms on the CPU to know the actual shape of the surface. Stream Out can resolve this, but you then have to decode the stream as well as handle latency...
Thoughts?
Cheers,
Jack