Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

How would you export terrain normal map via script

Discussion in 'World Building' started by o1o101, Sep 25, 2019.

  1. o1o101

    o1o101

    Joined:
    Jan 19, 2014
    Posts:
    639
    Interested in exporting my Unity terrain height data into a normal map via script within Unity, I not a programmer, so this is pretty much black magic to me, but I am not 100% useless.

    This is a free script I have laying around which does exactly what I want, however it is intended for mesh terrain.

    Code (CSharp):
    1.  protected override void CreateMap(float[] heights, int width, int height)
    2.         {
    3.  
    4.             float ux = 1.0f / (width - 1.0f);
    5.             float uy = 1.0f / (height - 1.0f);
    6.  
    7.             float scaleX = m_terrainHeight / m_terrainWidth;
    8.             float scaleY = m_terrainHeight / m_terrainLength;
    9.  
    10.             Texture2D normalMap = new Texture2D(width, height, TextureFormat.ARGB32, false, true);
    11.  
    12.             for (int y = 0; y < height; y++)
    13.             {
    14.                 for (int x = 0; x < width; x++)
    15.                 {
    16.  
    17.                     int xp1 = (x == width - 1) ? x : x + 1;
    18.                     int xn1 = (x == 0) ? x : x - 1;
    19.  
    20.                     int yp1 = (y == height - 1) ? y : y + 1;
    21.                     int yn1 = (y == 0) ? y : y - 1;
    22.  
    23.                     float l = heights[xn1 + y * width] * scaleX;
    24.                     float r = heights[xp1 + y * width] * scaleX;
    25.  
    26.                     float b = heights[x + yn1 * width] * scaleY;
    27.                     float t = heights[x + yp1 * width] * scaleY;
    28.  
    29.                     float dx = (r - l) / (2.0f * ux);
    30.                     float dy = (t - b) / (2.0f * uy);
    31.  
    32.                     Vector3 normal;
    33.                     normal.x = -dx;
    34.                     normal.y = -dy; //you might need to flip y
    35.                     normal.z = 1;
    36.                     normal.Normalize();
    37.  
    38.                     Color pixel;
    39.                     pixel.r = normal.x * 0.5f + 0.5f;
    40.                     pixel.g = normal.y * 0.5f + 0.5f;
    41.                     pixel.b = normal.z;
    42.                     pixel.a = 1.0f;
    43.  
    44.                     normalMap.SetPixel(x, y, pixel);
    45.                 }
    46.  
    47.             }
    48.  
    49.             normalMap.Apply();
    50.             m_material.mainTexture = normalMap;
    51.         }
    52.  
    53.     }
    54.  
    55. }
    I believe the key height data here is "heights" which is a float array from the heightmap on the mesh terrain, here is what the above script derives from.

    Code (CSharp):
    1. void Start()
    2.         {
    3.  
    4.             if (m_material == null) return;
    5.  
    6.             string fileName = Application.dataPath + "/TerrainTopology/Heights.raw";
    7.             float[] heights = Load16Bit(fileName);
    8.  
    9.             int width = 1024;
    10.             int height = 1024;
    11.  
    12.             CreateMap(heights, width, height);
    13.         }
    14.  
    15.         void OnDestroy()
    16.         {
    17.             if (m_material == null) return;
    18.             m_material.mainTexture = null;
    19.         }
    20.  
    21.         protected abstract void CreateMap(float[] heights, int width, int height);
    22.  
    23.         protected float[] Load16Bit(string fileName, bool bigendian = false)
    24.         {
    25.             byte[] bytes = System.IO.File.ReadAllBytes(fileName);
    26.  
    27.             int size = bytes.Length / 2;
    28.             float[] data = new float[size];
    29.  
    30.             for (int x = 0, i = 0; x < size; x++)
    31.             {
    32.                 data[x] = (bigendian) ? (bytes[i++] * 256.0f + bytes[i++]) : (bytes[i++] + bytes[i++] * 256.0f);
    33.                 data[x] /= ushort.MaxValue;
    34.             }
    35.  
    36.             return data;
    37.         }
    So, I am wondering if anyone knows how this could be converted to take the heightmap from a Unity Terrain instead of a mesh terrain.
    I assumed I would be able to do something like, heights = myTerrain.terrainData.GetHeights; However that is a nested [,] float, which I have no clue how to make work with the above set up.

    Any tips from you programming experts would be amazingly helpful!
     
  2. wyattt_

    wyattt_

    Unity Technologies

    Joined:
    May 9, 2018
    Posts:
    424
    Hey! You can use the PaintContext and TerrainPaintUtility APIs to do this. I think this is how it would go:


    Code (CSharp):
    1.  
    2.  
    3. RenderTexture prev = RenderTexture.active; // get ref to current RenderTexture so it can be restored later
    4.  
    5. Rect boundsInTerrainSpace = new Rect( 0, 0, terrain.terrainData.scale.x, terrain.terrainData.scale.z );
    6. PaintContext ctx = TerrainPaintUtility.CollectNormals( terrain, boundsInTerrainSpace, 1 );
    7.  
    8. int textureWidth = ctx.sourceRenderTexture.width;
    9. int textureHeight = ctx.sourceRenderTexture.height;
    10. Texture2D normalMap = new Texture2D( textureWidth, textureHeight, TextureFormat.ARGB32, false, false );
    11. RenderTexture.active = ctx.sourceRenderTexture; // assign active RenderTexture to be the Terrain normalMap
    12. normalMap.ReadPixels( new Rect( 0, 0, textureWidth, textureHeight ), 0, 0, false ); // read pixels from the active RenderTexture to the normalMap Texture2D
    13. normalMap.Apply();
    14.  
    15. RenderTexture.active = prev; // restore previous RenderTexture
    16.  
    17. // do with what you will of your new normalMap Texture2D
    18.  
    19.