Converting Displacement Maps into Normal maps

This is a repost from the old blog, since this is one of those interesting stuff, which weren’t big enough to warrant a sample.

Working on my VTF tutorial, I came across the need to apply lighting to a terrain. For lighting, we need normals, of course, but I only had heightmaps available. When the terrain is static, this poses no problems at all. All we have to de is generate a normal map from the heightmaps with whatever tools we want. For example, NVIDIA has a Photoshop plugin for this here.

However, when we have dynamic terrain, with real time deformation, this is no longer an option. This is especially the case when we make the deformations on the displacement map. After lots of reading and searching, I’ve come across the Sobel Filter, which can be used to compute normal maps.

As a result, I made a pixel shader that takes as input the displacement map and outputs the normal map. At runtime, after modifying the displacement map, I apply this shader and put the result into a render target. This result is then used when drawing the terrain, when computing lighting in the pixel shader.

So here’s the HLSL code:

float textureSize = 256.0f;
float texelSize =  1.0f / textureSize ; //size of one texel;
float normalStrength = 8;
 
float4 ComputeNormalsPS(in float2 uv:TEXCOORD0) : COLOR
    {
    float tl = abs(tex2D (displacementSampler, uv + texelSize * float2(-1, -1)).x);   // top left
    float  l = abs(tex2D (displacementSampler, uv + texelSize * float2(-1,  0)).x);   // left
    float bl = abs(tex2D (displacementSampler, uv + texelSize * float2(-1,  1)).x);   // bottom left
    float  t = abs(tex2D (displacementSampler, uv + texelSize * float2( 0, -1)).x);   // top
    float  b = abs(tex2D (displacementSampler, uv + texelSize * float2( 0,  1)).x);   // bottom
    float tr = abs(tex2D (displacementSampler, uv + texelSize * float2( 1, -1)).x);   // top right
    float  r = abs(tex2D (displacementSampler, uv + texelSize * float2( 1,  0)).x);   // right
    float br = abs(tex2D (displacementSampler, uv + texelSize * float2( 1,  1)).x);   // bottom right
 
    // Compute dx using Sobel:
    //           -1 0 1 
    //           -2 0 2
    //           -1 0 1
    float dX = tr + 2*r + br -tl - 2*l - bl;
 
    // Compute dy using Sobel:
    //           -1 -2 -1 
    //            0  0  0
    //            1  2  1
    float dY = bl + 2*b + br -tl - 2*t - tr;
 
    // Build the normalized normal
    float4 N = float4(normalize(float3(dX, 1.0f / normalStrength, dY)), 1.0f);
 
    //convert (-1.0 , 1.0) to (0.0 , 1.0), if needed
    return N * 0.5f + 0.5f;
}

You should tweak normalStrength until the result you get is satisfactory.

And here is the result of applying that code to two heightmaps, and some terrain lit using a normal map generated by this shader.

 

displacement normals

Categorized: HLSL
  • http://machinesdontcare.wordpress.com a|x

    Hi,

    this looks cool. Do you happen to know if this technique could be applied to a spherical heightfield (ie one where a plane is wrapped around into a sphere, and the radius of the sphere is modulated by the heightfield)?

    I’m guessing you’d need to calculate the normals for the undistorted sphere algorithmically, then use the heightmap to offset those normals, but I’m hazy (at best) on the maths that would be involved. Any pointers much appreciated!

    Cheers!

    PS
    I posted the same question on your other blog at
    http://catalinzima.spaces.live.com/blog/cns!3D9ECAE1F2DC56EF!223.entry
    but I have a suspicion that you look at comments on this one more regularly. Sorry for the duplication though.

  • http://www.catalinzima.com Catalin Zima

    Indeed, I don’t watch that blog anymore. It doesn’t even notify me of new comments….

    Theoretically, you could do that. The only problem is, as you said, the distorsion caused by applying a rectangular texture over a sphere.
    There’s some heavy math involved to solve the distorsion.

    To adjust the normal to the spere’s surface normal, you’d probably need to take into account the tangents and bitangent of the surface at any points.

    Basically, this becomes a sort of normal mapping, for which there is a sample on the Creator’s Club website ( http://creators.xna.com/en-us/sample/normalmapping ). You could take that sample and shader, and try it with the normals supplied by the method presented here, and see the results.

  • http://machinesdontcare.wordpress.com a|x

    Hi,

    thanks very much for getting back to me!
    Damn- it’s a lot more complicated than I’d thought…..
    Also, I think, if in requires tangent and bitangent vectors, I can’t do it in in the application I use (Quartz Composer on the Mac), since it doesn’t provide these as vertex attributes, sadly :(

    I’d naively assumed you could just work out the normal for the sphere, then just offset that normal using the normal from the normalmap.

    Back to the drawing board then….

    Thanks again,

    a|x

  • stc

    Very nice shader

    I am confused why you are using abs(tex2d().x)

    isn’t the return value from tex2D already clamped 0 to 1 ?

  • http://www.catalinzima.com Catalin Zima

    in this example, abs() is indeed not necessary.

    but tex2D does not always return in [0..1]. When you have floating point textures, it could return both positive and negative numbers in the 32-bit floating point range.

  • http://allzoom.ru.ru Zashkaser

    I love her blog too!!! She has amazing clothes!! And I want her hair !!!

  • Pingback: Confluence: LiveSailing

  • default_ex

    I would like to point out for anyone that looks at this and things that lighting the terrain looks a bit off. The dx and dy are flipped resulting in a normal that has the x and z axes reversed.

    A simple change corrects it:
    float3 normal = float3(-dx, 1 / NormalScale, -dy)

  • Pingback: yahoo account bot