Search Unity

Normal map from height map on runtime

Discussion in 'Shaders' started by m9_psy, Jul 27, 2014.

  1. m9_psy

    m9_psy

    Joined:
    Jul 27, 2014
    Posts:
    10
    I have grayscale image (noise) and want to extract normal map from it. For this purpose i am using Sobel filter. Like http://stackoverflow.com/questions/2368728/can-normal-maps-be-generated-from-a-texture?rq=1.
    But instead of normal map i get something like this:
    .
    When i shuffle dx, dy and dz in normal.SetPixel(x, y, new Color(dx, dy, dz)); my normal map turns red, green, blue, black and so on. How should i get a real normal map? What i should pick for alpha channel?
    Code (CSharp):
    1. public Texture2D getNormalMap(Texture2D texture, float str = 2.0f)
    2.     {
    3.         Color[] colors = texture.GetPixels();
    4.         Texture2D normal = new Texture2D(texture.width, texture.height, TextureFormat.ARGB32, false);
    5.         for (int x = 1; x < texture.width - 1; x++ )
    6.             for (int y = 1; y < texture.height - 1; y++)
    7.             {
    8.                 //using Sobel operator
    9.                 float tl, t, tr, l, right, bl, bot, br;
    10.                 tl = intensity(texture.GetPixel(x - 1, y - 1).r, texture.GetPixel(x- 1, y-1).g, texture.GetPixel(x-1, y-1).b);
    11.                 t = intensity(texture.GetPixel(x - 1, y).r, texture.GetPixel(x - 1, y).g, texture.GetPixel(x - 1, y).b);
    12.                 tr = intensity(texture.GetPixel(x - 1, y + 1).r, texture.GetPixel(x - 1, y + 1).g, texture.GetPixel(x - 1, y + 1).b);
    13.                 right = intensity(texture.GetPixel(x, y + 1).r, texture.GetPixel(x, y + 1).g, texture.GetPixel(x, y + 1).b);
    14.                 br = intensity(texture.GetPixel(x + 1, y + 1).r, texture.GetPixel(x + 1, y + 1).g, texture.GetPixel(x + 1, y + 1).b);
    15.                 bot = intensity(texture.GetPixel(x + 1, y).r, texture.GetPixel(x + 1, y).g, texture.GetPixel(x + 1, y).b);
    16.                 bl = intensity(texture.GetPixel(x + 1, y - 1).r, texture.GetPixel(x + 1, y - 1).g, texture.GetPixel(x + 1, y - 1).b);
    17.                 l = intensity(texture.GetPixel(x, y - 1).r, texture.GetPixel(x, y - 1).g, texture.GetPixel(x, y - 1).b);
    18.  
    19.                 //Sobel filter
    20.                 float dX = (tr + 2.0f * right + br) - (tl + 2.0f * l + bl);
    21.                 float dY = (bl + 2.0f * bot + br) - (tl + 2.0f * t + tr);
    22.                 float dZ = 1.0f / str;
    23.  
    24.                 Vector3 vc = new Vector3(dX, dY, dZ);
    25.                 vc.Normalize();
    26.  
    27.                 normal.SetPixel(x, y, new Color(vc.x, 0.5f, vc.y, vc.z));
    28.             }
    29.         normal.Apply();
    30.         return normal;
    31.     }
    32.  
    33.     public float intensity(float r, float g, float b)
    34.     {
    35.         return (r + g + b) / 3.0f;
    36.     }
     
  2. m9_psy

    m9_psy

    Joined:
    Jul 27, 2014
    Posts:
    10
    Some reading about Dxtmn format seems solve the problem. My normal map looks like:


    Not really a normal map, but it works in runtime. And you can adjust strength ("Bumpiness") much bigger than in editor;
    Here full code. Method take any texture (diffuse, gray) and strength. Return normal map texture that can be used like:
    Code (CSharp):
    1. Material m = new Material(Shader.Find("Bumped Diffuse"));
    2.         m.SetTexture("_MainTex", texture);
    3.         m.SetTexture("_BumpMap", normalMap);
    4.         renderer.material = m;
    Code (CSharp):
    1. public Texture2D getNormalMap(Texture2D texture, float str = 2.0f)
    2.     {
    3.         Color[] colors = texture.GetPixels();
    4.         Texture2D normal = new Texture2D(texture.width, texture.height, TextureFormat.ARGB32, false);
    5.         for (int x = 1; x < texture.width - 1; x++ )
    6.             for (int y = 1; y < texture.height - 1; y++)
    7.             {
    8.                 //using Sobel operator
    9.                 float tl, t, tr, l, right, bl, bot, br;
    10.                 tl = intensity(texture.GetPixel(x - 1, y - 1).r, texture.GetPixel(x- 1, y-1).g, texture.GetPixel(x-1, y-1).b);
    11.                 t = intensity(texture.GetPixel(x - 1, y).r, texture.GetPixel(x - 1, y).g, texture.GetPixel(x - 1, y).b);
    12.                 tr = intensity(texture.GetPixel(x - 1, y + 1).r, texture.GetPixel(x - 1, y + 1).g, texture.GetPixel(x - 1, y + 1).b);
    13.                 right = intensity(texture.GetPixel(x, y + 1).r, texture.GetPixel(x, y + 1).g, texture.GetPixel(x, y + 1).b);
    14.                 br = intensity(texture.GetPixel(x + 1, y + 1).r, texture.GetPixel(x + 1, y + 1).g, texture.GetPixel(x + 1, y + 1).b);
    15.                 bot = intensity(texture.GetPixel(x + 1, y).r, texture.GetPixel(x + 1, y).g, texture.GetPixel(x + 1, y).b);
    16.                 bl = intensity(texture.GetPixel(x + 1, y - 1).r, texture.GetPixel(x + 1, y - 1).g, texture.GetPixel(x + 1, y - 1).b);
    17.                 l = intensity(texture.GetPixel(x, y - 1).r, texture.GetPixel(x, y - 1).g, texture.GetPixel(x, y - 1).b);
    18.  
    19.                 //Sobel filter
    20.                 float dX = (tr + 2.0f * right + br) - (tl + 2.0f * l + bl);
    21.                 float dY = (bl + 2.0f * bot + br) - (tl + 2.0f * t + tr);
    22.                 float dZ = 1.0f / str;
    23.  
    24.                 Vector3 vc = new Vector3(dX, dY, dZ);
    25.                 vc.Normalize();
    26.                 //Debug.Log(vc.x + " " + vc.y + " " + vc.z);
    27.                 normal.SetPixel(x, y, new Color((vc.y + 1f) / 2f, (vc.y + 1f) / 2f, (vc.y + 1f) / 2f, (vc.x + 1f) / 2f));
    28.             }
    29.         normal.Apply();
    30.         return normal;
    31.     }
    32.  
    33.     public float intensity(float r, float g, float b)
    34.     {
    35.         return (r + g + b) / 3.0f;
    36.     }
     
  3. jvo3dc

    jvo3dc

    Joined:
    Oct 11, 2013
    Posts:
    1,520
    Neither is very correct. The code should be in the lines of:

    Code (csharp):
    1.  
    2. //Sobel filter
    3. float dX = (tr + 2.0f * right + br) - (tl + 2.0f * l + bl);
    4. float dY = (bl + 2.0f * bot + br) - (tl + 2.0f * t + tr);
    5. float dZ = 1.0f;
    6.  
    7. Vector3 vc = new Vector3(str * dX, str * dY, dZ);
    8. vc.Normalize();
    9.  
    10. normal.SetPixel(x, y, new Color(0.5f + 0.5f * vc.x, 0.5f + 0.5f * vc.y, 0.5f + 0.5f * vc.z, 0.0f));
    11.  
    The normalized result needs to be fitted in the 0 to 1 range instead of the -1 to 1 range.

    I am assuming a standard RGB normal map, where the tangent space X, Y and Z coordinates are stored in R, G and B respectively.

    Oh, and another little tip on rewriting your intensity function:
    Code (csharp):
    1.  
    2. public float Intensity(Color color) {
    3.         return (0.229f * color.r + 0.587f * color.g + 0.114f * color.b);
    4. }
    5.  
    Saves getting the pixel color three times:
    Code (csharp):
    1.  
    2. tl = Intensity(texture.GetPixel(x - 1, y - 1));
    3.  
     
    Last edited: Jul 28, 2014
    m9_psy likes this.
  4. m9_psy

    m9_psy

    Joined:
    Jul 27, 2014
    Posts:
    10
    Unity (or may be operating system?) use DXT compression for normal maps. It use only two colors from our pixel. And compute z part as z = sqrt(1 - (x*x + y*y)). Because vector is normalised. Unity use DXTnm compression that store values in A channel and G channel. So we should map X to A channel and Y to G channel. And have two free channels: R and B. If they free, we can even mark them as 0 and dont use.
    So this code does not work:
    Code (CSharp):
    1. normal.SetPixel(x, y, new Color(0.5f + 0.5f * vc.x, 0.5f + 0.5f * vc.y, 0.5f + 0.5f * vc.z, 0.0f));
    And this works as expected (we have correct normal map):
    Code (CSharp):
    1. normal.SetPixel(x, y, new Color((vc.x + 1f) / 2f, (vc.y + 1f) / 2f, (vc.z + 1f) / 2f, (vc.x + 1f) / 2f));
    And this code will work as a previous:
    Code (CSharp):
    1. normal.SetPixel(x, y, new Color(0.0f, (vc.y + 1f) / 2f, 0.0f, (vc.x + 1f) / 2f));
    But we'll have ugly green normal map, but it works in unity.

    Correct me if i wrong.

    And what means that magic numbers?
    Code (CSharp):
    1. public float Intensity(Color color) {
    2.         return (0.229f * color.r + 0.587f * color.g + 0.114f * color.b);
    3. }
     
    Last edited: Jul 28, 2014
  5. jvo3dc

    jvo3dc

    Joined:
    Oct 11, 2013
    Posts:
    1,520
    Yes, it is a common strategy to store Y in the green channel and X in the alpha channel and then use DXT5 or a similar type of compression. As far as I know, Unity does that for you automatically depending on your target platform. So you should deliver your normal map as standard RGB map and let Unity do the compression and select the appropriate shader. (The change is located in the shader, it's not an operating system thing.) That's also why you should mark the texture as being a normal map.
    The main change here is that the input is a Color instead of three floats, but I also adjusted the multipliers to the standard values for a RGB to YUV color space conversion. (I do see I made a typo there. The first value should be 0.299.)

    What these values finally mean is the human eye's response to the standard red, green and blue colors. So, 100% blue is perceived as a brightness of 11.4%. Compared to 100% white being 100% bright. Another common approach is to only use the green channel.

    http://en.wikipedia.org/wiki/YUV