GPU Noise and Normal generation

For the last several weeks I’ve been experimenting with GPU generation of height and normal maps in order to increase real-time performance of the patch generation. This is a bit of a progress update covering the different kinds of maps that I’m now generating and using to render the basic planet terrain.

Height Map

The height map GPU generation didn’t take long to sort out, starting with the sample at Ziggyware; which was recently hacked and is currently down so I cant provide a link :(  I initially encountered precision issues with generating the GPU generated values to apply to the vertex heights, with the edges of patches at the same LOD being quite different resulting in very large gaps. I eventually worked out that I needed to set the render target surface format to SurfaceFormat.Single and then read the texture into memory as float[] after the GPU has finished rendering. I was then able to go back to using the GPU height map as the height values for the vertices and got much better precision. Not perfect, but very, very close. I can resolve the small cracks between equal LOD patches when I do seam-fixing, which is next on my todo list.


The basic concept is that I pass in the normalised spherical vertices of the patch, which the GPU Noise shader then interprets and creates noise values for on a per-pixel basis. I played with many, many, many different noise generation patterns, you could literally spend 6 months doing different combinations of noise, combiners, feedback etc without finding something that looks really good. I settled on a not-so-bad combination (at least from orbit) at the moment:

image (In HLSL)

It basically applies 3 ridged multifractal noise values of increasing radius’ and averages them. Then does the same for a fractional Brownian motion noise, using the value of c1 as a modifier to further increase the radius of the source positions. I also divide the fBm result by c1 which I’ve found gives interesting results. I also mixed in a 3 octave version of ridgedmf and fBm to give it a bit of a softer edge. I finally c3^0.4 to scale down the results a bit, then subtract 0.5f to push the terrain lower, creating more sea area.

You can see the results of the above function in the screenshots at the end of this post, but here’s the height map result of some mix-mashing of high-octave noise which proved to be too noisy at ground level (massive noise spikes at the highest LOD = bad for games):

patches-good

I really like the pattern though, it has a good distribution of complex features and flat planes. Maybe I’ll end up using it as the basis for a cloud layer or some other arbitrary noise source.

Normal Map

The normal map generation took a very long time to sort out. In the CPU method I was computing real positions for each texel required and calculating the true normal. The results were very accurate, but incredibly, mind numbingly, slow due to the massive amount of noise calls that had to be made.  My screenshots and YouTube Video in earlier posts use the CPU based method. GPU generation was the obvious answer to get some decently interactive frame rates going.

There are so many articles about various methods floating around the internet, and very few relating to spherical terrain that it makes things very difficult. The most common approach seems to be to compute slope from the height map and derive a normal using a fixed Z value. This works.. to a point, but different LOD patches give different results, so may look fine at orbit, but by the time you reach the surface the normals contribute so little to the lighting the effect is barely visible. A few experiments in the way of increasing/decreasing the static Z constant based on LOD level yielded little improvement. Also, because the generation assumed that the height map was planar, the normals at the edges of the faces didn’t match and there were obnoxious seams visible.

After experimenting with various other methods I gave up for about a week. Then over the course of about 2 days I started to formulate the idea of generating positions per-texel on the GPU in the same way that I do on the CPU for patch vertices. This was made easier by the fact that the positions I pass into the height map shader lie on a unit sphere, so I would not be running into any precision issues since the normal map shader and the height map shader are operating on the same values.

All I needed to do was to pass in the normalised starting position (x,y) of the patch on the face of the unit-cube and the rotation matrix to transform the patch to the correct position (there are 6 total rotation matrices, up/down/left/right/forward/back). I also passed in the normalised width of the patch. The concept is that each texel will lie within the patch bounds on the unit cube, at which point I transform the position into the unit sphere equivalent and pass into the noise function to get the height of terrain at that point.

That probably sounds confusing. So how about some code.

Vertex shader: 
image

This calculates the location of the vertex on the unit cube using the texture coordinates already provided. Bounds(X|Y|Width|Height) are the normalised extents of the patch’s location on the planet’s cube face.

Pixel Shader:
image

Because the original positions are planar, I can easily add 1 texel unit to the x/y positions of the FacePosition and get the correct locations; the above shows this. It just gets the current position, and the positions one texel to the right, and one texel below. It also transforms the unit cube position to the correct face, then converts to the spherical equivalent.

image

Next, the 3 positions are scaled by the noise (terrain height). Scale is used to reduce the strength of the normals. Ideally the proportion between scale and the unit sphere radius would be the same as the proportion of the maximum terrain height and your planet’s radius, so that you get proportionally equivalent results. My normals are still coming out a bit strong, but that’ll be fixed later.

image

The last bit of the pixel shader computes the normal from the 3 scales positions and packs it into the output pixel.

As a step of convenience, I decided to calculate the slope for this particular location using the dot product of the unit sphere normal and the calculated normal packed into the W channel. This is convenient because I don’t have to generate a slope map in a separate step and pass into the terrain shader as a separate texture. It wasn’t something I planned for, it just happened :)

The normal map is still not 100% accurate and there are some minor bugs. Mainly, positions which lie on the x=1 or y=1 of the unit cube face and add one texel to FacePosition x and y are generating potentially invalid positions. It’s my intention to pass the correct FaceMatrix in for the adjoining faces so that when the positions are x>1 or y>0, they are correctly placed on the adjoining unit cube face. Also, the normals only take into account the positions x+texel and y+texel, whereas to be most accurate it should properly take into account x-texel and y-texel as well, which at the moment would double the error on the edge cases where x-texel < 0 or y-texel < 0. Something to look in to later.

Slope Map

I’ve already mentioned how the Slope map is being generated (W channel of the normal map). So I thought I’d give a quick rundown on what it’s used for. Basically, the Slope map indicates how steep a certain location on the terrain is. This is useful for adjusting the texture blending selection so that steep surfaces that would otherwise be showing green grass, could show brown dirt or rock instead. Just eye-candy really.

Diffuse Map

The diffuse map is currently generated each frame in the terrain shader. It takes the other maps (Normal, Height, Slope, Blend) and the result is a half-decent looking, per-pixel lit planet. It uses very basic 4-channel texture blending utilising a pre-made Blend Map:

image

So as the altitude increases, the texture blending shifts from water to sand, to grass, to rock. With the creation of the Slope Map just recently, I’ve made the slope value affect the height at which it thinks the terrain is. It is completely inaccurate but it adds a little flair for now. I’ll be doing a proper slope-based blend map later when I extend the system to the popular 16-texture blending method.

Once I get the diffuse map shader to a point that I’m happy with, I’ll be generating it into a texture to reduce the pixel shader workload, and I’ll also be able to free up the height map memory which is currently hanging around.

Screenshots

So I took two sets of screenshots. One of a far-orbit view, and one much, much closer. The various maps are shown, as well as the result of combining them all. Also I’ve included a wireframe view showing just how much work the pixel shader and noise functions do. The patch size is 16x16 vertices using 512x512 textures (height & normal). Planet radius is 6378100 game units, with 1unit = 1m, so it’s the radius of Earth.

Note: The images are 1600x1024 PNG’s, averaging 600kb each and are set to open in a new window since a popup wouldn’t cut it ;)

Jump 2009-10-12 22-53-11-52 Jump 2009-10-12 22-53-15-54 Jump 2009-10-12 22-53-18-55 Jump 2009-10-12 22-53-27-81 Jump 2009-10-12 22-53-29-84 Jump 2009-10-12 22-53-09-73

Nearer to the planet. Note how bad the noise looks at this altitude. Overly strong normals are contributing to making it look bad. Tweaking to come.

Jump 2009-10-12 22-55-26-32 Jump 2009-10-12 22-55-18-22 Jump 2009-10-12 22-55-19-81 Jump 2009-10-12 22-55-22-00 Jump 2009-10-12 22-55-23-13 Jump 2009-10-12 22-55-16-42

Next on the list for me is to fix up the terrain stitching which shouldn’t be too hard. I already have a function to find the visible neighbouring patch, and edge vertex LOD adjustment is just a LOD level comparison with a quick calculation to determine how many verts need to be adjusted using midpoint displacement. After that I imagine I’ll start on some LOD patch blending, so that the ugly popping is replaced by a gradual morph from a lower LOD level into the higher one. And the 16-texture slope blending method at some point as well.


Tags:
Categories: General

150 Comments
Actions: E-mail | Permalink | Comment RSSRSS comment feed

Comments