Search Unity

Adding terrain textures procedurally

Discussion in 'Scripting' started by abertrand, Dec 16, 2015.

  1. abertrand

    abertrand

    Joined:
    Dec 7, 2015
    Posts:
    29
    There is very little information online on how to assign terrain textures via code on Unity (there are a couple of posts like this one or that one but they’re several years old and they don’t explain fully what’s what). And Unity’s own documentation on the topic is very poor.

    The following code allows you to assign as many textures as you want to your terrain and to define how they appear based on factors such as altitude or steepness (e.g. a snow texture should only appear at high altitude where it’s not too steep). Let me first share the code with you and I will tell you afterwards how to use it.

    Code (CSharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections;
    4. using System.Collections.Generic;
    5. using System;
    6. using System.Linq;
    7.  
    8. public class TextureModifier : MonoBehaviour {
    9.  
    10.     // Using Serializable allows us to embed a class with sub properties in the inspector.
    11.     [Serializable]
    12.     public class TextureAttributes{
    13.  
    14.         public string name;
    15.         public int index;
    16.         public bool defaultTexture = false;
    17.         [Range(0.0f,1.0f)]
    18.         public float minSteepness;
    19.         [Range(0.0f,1.0f)]
    20.         public float maxSteepness;
    21.         [Range(0.0f,1.0f)]
    22.         public float minAltitude;
    23.         [Range(0.0f,1.0f)]
    24.         public float maxAltitude;
    25.     }
    26.  
    27.     public List<TextureAttributes> listTextures = new List<TextureAttributes> ();
    28.     private Terrain terrain;
    29.     private TerrainData terrainData;
    30.     private int indexOfDefaultTexture;
    31.  
    32.     void Start () {
    33.  
    34.         // Get the attached terrain component
    35.         terrain = GetComponent<Terrain>();
    36.  
    37.         // Get a reference to the terrain data
    38.         terrainData = terrain.terrainData;
    39.        
    40.         //This is the # of textures you have added in the terrain editor
    41.         int nbTextures = terrainData.alphamapLayers;
    42.        
    43.         //See below for the definition of GetMaxHeight
    44.         float maxHeight = GetMaxHeight(terrainData, terrainData.heightmapWidth);
    45.  
    46.         // Your texture data (i.e. Splatmap) is stored internally as a 3d array of floats with x and y location as the first 2 dimensions of the array and the index of the texture to be used as the 3rd dimension
    47.         float[, ,] splatmapData = new float[terrainData.alphamapWidth, terrainData.alphamapHeight, terrainData.alphamapLayers];
    48.  
    49.         //This is just in case someone mixed up min and max when completing the inspector for this script
    50.         for (int i = 0; i < listTextures.Count; i++) {
    51.             if (listTextures [i].minAltitude > listTextures [i].maxAltitude) {
    52.                 float temp = listTextures [i].minAltitude;
    53.                 listTextures [i].minAltitude = listTextures [i].maxAltitude;
    54.                 listTextures [i].maxAltitude = temp;
    55.             }
    56.  
    57.             if (listTextures [i].minSteepness > listTextures [i].maxSteepness) {
    58.                 float temp2 = listTextures [i].minSteepness;
    59.                 listTextures [i].minSteepness = listTextures [i].maxSteepness;
    60.                 listTextures [i].maxSteepness = temp2;
    61.             }
    62.         }
    63.            
    64.         //For some reason you need a default texture in Unity
    65.         for (int i = 0; i < listTextures.Count; i++) {
    66.             if (listTextures [i].defaultTexture) {
    67.                 indexOfDefaultTexture = listTextures [i].index;
    68.             }
    69.         }
    70.  
    71.  
    72.         for (int y = 0; y < terrainData.alphamapHeight; y++)
    73.         {
    74.             for (int x = 0; x < terrainData.alphamapWidth; x++)
    75.             {
    76.                 // Normalise x/y coordinates to range 0-1
    77.                 float y_01 = (float)y/(float)terrainData.alphamapHeight;
    78.                 float x_01 = (float)x/(float)terrainData.alphamapWidth;
    79.  
    80.                 // Sample the height at this location (note GetHeight expects int coordinates corresponding to locations in the heightmap array)
    81.                 float height = terrainData.GetHeight(Mathf.RoundToInt(y_01 * terrainData.heightmapHeight),Mathf.RoundToInt(x_01 * terrainData.heightmapWidth) );
    82.  
    83.                 //Normalise the height by dividing it by maxHeight
    84.                 float normHeight = height / maxHeight;
    85.  
    86.                 // Calculate the steepness of the terrain at this location
    87.                 float steepness = terrainData.GetSteepness(y_01,x_01);
    88.  
    89.                 // Normalise the steepness by dividing it by the maximum steepness: 90 degrees
    90.                 float normSteepness = steepness / 90.0f;
    91.  
    92.                 //Erase existing splatmap at this point
    93.                 for(int i = 0; i<terrainData.alphamapLayers; i++){
    94.                     splatmapData [x, y, i] = 0.0f;
    95.                 }
    96.  
    97.                 // Setup an array to record the mix of texture weights at this point
    98.                 float[] splatWeights = new float[terrainData.alphamapLayers];
    99.  
    100.                 for (int i = 0; i < listTextures.Count; i++) {
    101.      
    102.                     //The rules you defined in the inspector are being applied for each texture
    103.                     if (normHeight >= listTextures [i].minAltitude && normHeight <= listTextures [i].maxAltitude && normSteepness >= listTextures [i].minSteepness && normSteepness <= listTextures [i].maxSteepness) {
    104.                         splatWeights [listTextures [i].index] = 1.0f;
    105.                     }
    106.                 }
    107.  
    108.                 // Sum of all textures weights must add to 1, so calculate normalization factor from sum of weights
    109.                 float z = splatWeights.Sum();
    110.  
    111.                 //If z is 0 at this location (i.e. no texture was applied), put default texture
    112.                 if(Mathf.Approximately(z, 0.0f)){
    113.                     splatWeights [indexOfDefaultTexture] = 1.0f;
    114.                 }
    115.  
    116.                 // Loop through each terrain texture
    117.                 for(int i = 0; i<terrainData.alphamapLayers; i++){
    118.  
    119.                     // Normalize so that sum of all texture weights = 1
    120.                     splatWeights[i] /= z;
    121.  
    122.                     // Assign this point to the splatmap array
    123.                     splatmapData[x, y, i] = splatWeights[i];
    124.                 }
    125.             }
    126.         }
    127.  
    128.         // Finally assign the new splatmap to the terrainData:
    129.         terrainData.SetAlphamaps(0, 0, splatmapData);
    130.     }
    131.    
    132.     //This is to get the maximum altitude of your terrain. For some reason TerrainData.
    133.     private float GetMaxHeight(TerrainData tData, int heightmapWidth){
    134.  
    135.         float maxHeight = 0f;
    136.  
    137.         for (int x = 0; x < heightmapWidth; x++) {
    138.             for (int y = 0; y < heightmapWidth; y++) {
    139.                 if (tData.GetHeight (x, y) > maxHeight) {
    140.                     maxHeight = tData.GetHeight (x, y);
    141.                 }
    142.             }
    143.         }
    144.         return maxHeight;
    145.     }
    146. }
    147.  
    When you attach this script to a terrain object, you get something like this:



    Simply do as follows:
    1. Precise the size on top (i.e. how many textures you want on your terrain)
    2. For each texture:
      1. Precise the name (you can put whatever name you wish, there is no logic that derives from this in the code)
      2. Precise if it's your default texture, a must-have on Unity for some reason. In other words if for a specific location no texture should appear based on the steepness/altitude rules you defined, which texture should be there?
      3. Precise the index, i.e. in which order it appears in your terrain inspector. For instance in the image below my texture with index "0" (zero) is a sort of dried grass, the one with index "3" is a sort of greyish snow, the one with index "5" a greener grass, etc.
      4. Define the rules for steepness and altitude. For instance if I want my snow texture to only appear in high altitudes and one not-to-steep areas, I could define Min Altitude as 0.6 and Max Altitude at 1 which means it will only appear in the top 40% highest altitudes. For steepness if I put Min Steepness at 0 and Max Steepness at 0.6 it means it will only appear in places with a steepness degree from 0 degree to 0.6 times 90 (maximum amount of degrees) = 54 degrees.
    There you go, I hope it helps some people!
     
  2. Recon03

    Recon03

    Joined:
    Aug 5, 2013
    Posts:
    845
    Great post, but I use Gaia and Terrain Composer, amazing tools.
     
  3. Proto-G

    Proto-G

    Joined:
    Nov 22, 2014
    Posts:
    213
    Thanks for sharing this with the community, while Gaia may be an amazing tool, it's not free ;)
     
  4. abertrand

    abertrand

    Joined:
    Dec 7, 2015
    Posts:
    29
    May I ask why you use both: aren't they doing roughly the same thing?
     
  5. abertrand

    abertrand

    Joined:
    Dec 7, 2015
    Posts:
    29
    You're very welcome ;-)
     
  6. AlucardJay

    AlucardJay

    Joined:
    May 28, 2012
    Posts:
    328
    Firstly: this is really great of you to submit this script, many thanks :)

    Just some feedback if you don't mind, it's all mostly negligible, just sharing thoughts.

    It's interesting how you use the max vertex height of the terrain rather than the actual max terrain height. however, if the user modified the terrain to have a higher point, this would change the alphamap output, which could be frustrating to a user who spent a long time tweaking values then modifying the max vertex height.
    Personally I prefer using real world values rather than normalized ones ( height in units, slope angle in degrees ), and am modifying the code for these preferences.

    For this line, you could just use the x and y values :
    float height = terrainData.GetHeight( Mathf.RoundToInt( y_01 * terrainData.heightmapHeight), Mathf.RoundToInt( x_01 * terrainData.heightmapWidth ) );
    float height = terrainData.GetHeight( y, x );

    Really petty observations :
    nbTextures is assigned but never used. Perhaps use this in the places where terrainData.alphamapLayers is referenced. Maybe also cache the height/alpha width and height too. I don't know if this would be any faster than referencing the terrainData every time in the loops.
    Check if there are textures assigned to the terrain ( terrainData.alphamapLayers > 0 ). Also check if a texture index is out-of-range where you loop through for checking the min/max slope angles.

    Again, thank you very much for submitting this. I managed to write something similar, but it only worked using a specific fixed number of textures. I still have no idea how you get the blend weights to balance to equal 1. Good Job! All the Best.
     
  7. abertrand

    abertrand

    Joined:
    Dec 7, 2015
    Posts:
    29
    Thanks a lot for the feedback, much appreciated! To reply on your various points:
    • Max vertex height vs max terrain height, I assume you refer to using terrainData.heightmapHeight rather than my custom formula? I did so at first but for some reason for me this outputs the resolution of the heightmap rather than the max height of the terrain. I couldn't find another method to get the max terrain height so wrote the code to calculate it.
    • On real world value vs normalise ones, sure, quite easy to modify the code to achieve this.
    • On "For this line, you could just use the x and y values" --> only if the heightmap resolution is the same as the splatmap's. If they are different, which is often the case, you need that code to translate the coordinates between both maps.
    • On nbTextures, inefficient referencing in the loop and avoiding out of ranges indexes --> all very good points!
     
  8. AlucardJay

    AlucardJay

    Joined:
    May 28, 2012
    Posts:
    328
    * terrainData.size : The total size in world units of the terrain
    * oops, overlooked it was looping through alphamap width/height, then factoring the heightmap width/height. D'oh!

    Thanks again. I also made a custom inspector with a button so I could run it in edit mode.
    Code (csharp):
    1. using UnityEditor;
    2. using UnityEngine;
    3.  
    4.  
    5. [ CustomEditor( typeof( TerrainAutoPaint ) ) ]
    6. public class TerrainAutoPaintEditor : Editor
    7. {
    8.    private GameObject obj;
    9.    private TerrainAutoPaint objScript;
    10.  
    11.    void OnEnable()
    12.    {
    13.      obj = Selection.activeGameObject;
    14.      objScript = obj.GetComponent< TerrainAutoPaint >();
    15.    }
    16.  
    17.    public override void OnInspectorGUI()
    18.    {
    19.      DrawDefaultInspector();
    20.    
    21.    
    22.      //
    23.      EditorGUILayout.BeginHorizontal();
    24.    
    25.      if ( GUILayout.Button( "P : Paint Terrain", GUILayout.MinWidth( 80 ), GUILayout.MaxWidth( 350 ) ) )
    26.      {
    27.        objScript.PaintTerrain();
    28.      }
    29.    
    30.      EditorGUILayout.EndHorizontal();
    31.    }
    32.  
    33.  
    34.    void OnSceneGUI()
    35.    {
    36.      Event e = Event.current;
    37.    
    38.      if ( e.type == EventType.KeyDown )
    39.      {
    40.        switch ( e.keyCode )
    41.        {
    42.          case KeyCode.P :
    43.            objScript.PaintTerrain();
    44.          break;
    45.          
    46.          default:
    47.          
    48.          break;
    49.        }  
    50.      }
    51.    }
    52. }
     
  9. abertrand

    abertrand

    Joined:
    Dec 7, 2015
    Posts:
    29
    Very nice addition this custom inspector!
     
  10. Recon03

    Recon03

    Joined:
    Aug 5, 2013
    Posts:
    845
    no, Gaia can not slice terrains, also placement is different in GAIA vs TC, also I love that RTP, WC are all intergrated into TC.. Plus the runtime is amazing for TC.. So Gaia while its good, it still is new so its lacking what is needed to large terrains, for multiplayer and more.
     
  11. jasonguenther

    jasonguenther

    Joined:
    Feb 6, 2019
    Posts:
    3
    maybe dumb question but how are the textures literally applied exactly? I see they can be set but what function applys them?
     
  12. jasonguenther

    jasonguenther

    Joined:
    Feb 6, 2019
    Posts:
    3

    Hi, I wonder If you could tell me how to use this code with the texture modifier script? I would love to use this asset for splat painting
     
  13. grayando2

    grayando2

    Joined:
    Mar 22, 2020
    Posts:
    2
    Hi... does this still work? For some reason It only ever displays the default texture....
     
  14. grayando2

    grayando2

    Joined:
    Mar 22, 2020
    Posts:
    2
    Forget that comment, it still works, I was calling the index wrong on my second texture
     
  15. PeterCym

    PeterCym

    Joined:
    May 5, 2019
    Posts:
    7
    That looks like an amazing tool! I've already built a similar (but much less feature-complete) version, but I'm running into an issue with the array of terrain textures. I want to make streaming terrain, but new terrain tiles are instantiated with only one texture (index 0).

    How do I make new tiles load all of the needed textures?

    More information here: https://answers.unity.com/questions/1731560/handling-terrain-layers-at-runtime.html
     
  16. FormalSnake

    FormalSnake

    Joined:
    Oct 26, 2019
    Posts:
    12
    doesnt work anymore :(
     
  17. FormalSnake

    FormalSnake

    Joined:
    Oct 26, 2019
    Posts:
    12
    Can you tell me how to do that?

    @grayando2 ?
     
  18. FormalSnake

    FormalSnake

    Joined:
    Oct 26, 2019
    Posts:
    12
    im doing that cliffs have a separate texture procedurally and the rest of the terrain with my other texture (i am making a biiigggg map)