D3D11 HW Tessellation for Terrain Rendering

Started by
4 comments, last by jollyjeffers 15 years ago
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))));
}

/*
Geometry is provided by the application in the following format:

     10   11
     |    |
     |    |
8----0----2----4
     |    |
     |    |
9----1----3----5
     |    |
     |    |
     6    7
	 
The quad 0-1-2-3 is the one actually being rendered (the Domain Shader gets UV's for this
region) but the other 8 are used for adjacency information.
*/

HS_PER_PATCH_OUTPUT hsPerPatch( InputPatch<VS_OUTPUT, 12> ip, uint PatchID : SV_PrimitiveID )
{	
    HS_PER_PATCH_OUTPUT o = (HS_PER_PATCH_OUTPUT)0;
    
    // Determine the mid-point of this patch
    float3 midPoints[] =
    {
		// Main quad
		ComputePatchMidPoint( ip[0].position, ip[1].position, ip[2].position, ip[3].position )
		
		// +x neighbour
		, ComputePatchMidPoint( ip[2].position, ip[3].position, ip[4].position, ip[5].position )
		
		// +z neighbour
		, ComputePatchMidPoint( ip[1].position, ip[3].position, ip[6].position, ip[7].position )
		
		// -x neighbour
		, ComputePatchMidPoint( ip[0].position, ip[1].position, ip[8].position, ip[9].position )
		
		// -z neighbour
		, ComputePatchMidPoint( ip[0].position, ip[2].position, ip[10].position, ip[11].position )
    };
        
    // Determine the appropriate LOD for this patch
    float dist[] = 
    {
		// Main quad
		ComputePatchLOD( midPoints[0] )
		
		// +x neighbour
		, ComputePatchLOD( midPoints[1] )
		
		// +z neighbour
		, ComputePatchLOD( midPoints[2] )
		
		// -x neighbour
		, ComputePatchLOD( midPoints[3] )
		
		// -z neighbour
		, ComputePatchLOD( midPoints[4] )
	};
    
    // Set it up so that this patch always has an interior matching
    // the patch LOD.
    o.insideTesselation[0] = 
        o.insideTesselation[1] = dist[0];
        
    // For the edges its more complex as we have to match
    // the neighbouring patches. The rule in this case is:
    //
    // - If the neighbour patch is of a lower LOD we
    //   pick that LOD as the edge for this patch.
    //
    // - If the neighbour patch is a higher LOD then 
    //   we stick with our LOD and expect them to blend down
    //   towards us
    
    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:
  1. 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.
  2. 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

<hr align="left" width="25%" />
Jack Hoxley <small>[</small><small> Forum FAQ | Revised FAQ | MVP Profile | Developer Journal ]</small>

Advertisement
Looks pretty cool as a demo - but the popping needs to be morphed if possible to hide the transitions. Can that be done in the HS or DS - calculate a time transition value and modify the vertex output accordingly?

Also, any physics simulation that is using the far side of the terrain doesn't need to be too accurate - after all it is pretty far away from the camera. Even so, is there a way to use the stream output to collect the final mesh and feed it back into the physics API?

Great work - especially with the reference rasterizer!
Thanks for the comments Jason [smile]

Quote:Original post by Jason Z
the popping needs to be morphed if possible to hide the transitions. Can that be done in the HS or DS
Yup, it should be possible.

You specify the SV_TessFactor's as floating point, but obviously the actual tessellation level is integer (can't have half a triangle!) so you can grab both values and morph accordingly. For example, if you tried to set a tessellation of 2.75 which gets rounded up to 3 then you can interpolate between the 2nd and 3rd levels by a ratio of 0.75.

Or something like that.

Quote:Original post by Jason Z
is there a way to use the stream output to collect the final mesh and feed it back into the physics API?
Stream Out can capture all the triangles and you can tag patch-related metadata to each vertex. Basically the same as the SO debugging article I put together.

The problem is that in hardware the SO data may not be available for a couple of frames after rendering, so it could still give you artifacts...

Quote:Original post by Jason Z
Great work - especially with the reference rasterizer!
Cheers! It only took 15mins to render each of those movies - and almost an hour to upload them to YouTube [headshake]


Jack

<hr align="left" width="25%" />
Jack Hoxley <small>[</small><small> Forum FAQ | Revised FAQ | MVP Profile | Developer Journal ]</small>

Looks very interesting - good job! Even if the stream out should have unacceptable latency for interactivity, there's still a niche for efficient visualization of terrain. If you can solve the issues with level transitions, or the hardware proves to easily handle a very high tessellation factor, then it seems like a slightly more flexible approach than current techniques! Guess we'll only know for sure once we have DX11 hardware in our hands. However, the current techniques are already quite efficient, and so I can't help but think that the new tessellation stages will only show its true strength outside of height-field-rendering where you can exploit the 2.5d structure with existing techniques. For arbitrary meshes these efficient techniques doesn't really exist on the GPU yet!?

Well I'm glad you've been playing around with this stuff, because I certainly haven't been. [grin]

Looks very nice btw, good job on the shader documentation. Almost makes this stuff comprehensible!
Quote:Original post by ndhb
Looks very interesting - good job!
Thanks!

Quote:Original post by ndhb
Guess we'll only know for sure once we have DX11 hardware in our hands.
Yup, this is the real kicker for now. Plenty of people had cool ideas for the GS when it first came around, but that didn't exactly pan out when we eventually got the hardware....

Quote:Original post by ndhb
the new tessellation stages will only show its true strength outside of height-field-rendering
Yup, quite likely - as you say, people have been creating terrain rendering algorithms for decades and they are quite good!

Quote:Original post by ndhb
For arbitrary meshes these efficient techniques doesn't really exist on the GPU yet!?
I understand there are quite a few algorithms that are popular in the 'offline' space for character and model rendering. Art is increasingly expensive, so adapting the technology to maximize the artists efficiency is a definite plus point for D3D11 tessellation. If the pipeline can now pretty much take and render the mesh in the same way that the artist creates it then that should make things a bit smoother/simpler as well as the scaleability of a tessellated mesh across all hardware....

Quote:Well I'm glad you've been playing around with this stuff, because I certainly haven't been.
[lol] I've only really looked at hull and domain shaders so there are plenty of bits that I haven't checked out yet!

Quote:good job on the shader documentation. Almost makes this stuff comprehensible!
Well most of what I know can be attributed to cornering Amar & co in that meeting room in Redmond [grin]


Cheers,
Jack

<hr align="left" width="25%" />
Jack Hoxley <small>[</small><small> Forum FAQ | Revised FAQ | MVP Profile | Developer Journal ]</small>

This topic is closed to new replies.

Advertisement