Search Unity

  1. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Simple Runtime Terrain Editor

Discussion in 'Scripting' started by WinterboltGames, Nov 1, 2017.

  1. WinterboltGames

    WinterboltGames

    Joined:
    Jul 27, 2016
    Posts:
    258
    I have created a simple runtime terrain editor for unity terrain I have done all the tools that I need for my game and decided to share the script here. for people who want to use it, go ahead it's free with no restrictions

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. using System.Collections;
    4.  
    5. public class TerrainTool : MonoBehaviour
    6. {
    7.     public enum TerrainModificationAction
    8.     {
    9.         Raise,
    10.         Lower,
    11.         Flatten,
    12.     }
    13.  
    14.     public float range;
    15.  
    16.     public int areaOfEffectSize;
    17.  
    18.     [Range(0.001f, 0.1f)]
    19.     public float effectIncrement;
    20.  
    21.     public float sampledHeight;
    22.  
    23.     public TerrainModificationAction modificationAction;
    24.  
    25.     public LayerMask modificationLayerMask;
    26.  
    27.     private Terrain targetTerrain;
    28.  
    29.     private TerrainData targetTerrainData;
    30.  
    31.     private float[,] terrainHeightMap;
    32.  
    33.     private int terrainHeightMapWidth;
    34.     private int terrainHeightMapHeight;
    35.  
    36.     private void Update()
    37.     {
    38.         if (Camera.main)
    39.         {
    40.                 if (Input.GetButton("Primary Action"))
    41.                 {
    42.                     Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    43.                     RaycastHit hit;
    44.  
    45.                     if (Physics.Raycast(ray, out hit, range, modificationLayerMask))
    46.                     {
    47.                         if (GetTerrainAtObject(hit.transform.gameObject))
    48.                         {
    49.                             targetTerrain = GetTerrainAtObject(hit.transform.gameObject);
    50.                             SetEditValues(targetTerrain);
    51.                         }
    52.  
    53.                         switch (modificationAction)
    54.                         {
    55.                             case TerrainModificationAction.Raise:
    56.                                 RaiseTerrain(targetTerrain, hit.point, effectIncrement);
    57.                                 break;
    58.  
    59.                             case TerrainModificationAction.Lower:
    60.                                 LowerTerrain(targetTerrain, hit.point, effectIncrement);
    61.                                 break;
    62.  
    63.                             case TerrainModificationAction.Flatten:
    64.                                 FlattenTerrain(targetTerrain, hit.point, sampledHeight);
    65.                                 break;
    66.                         }
    67.                     }
    68.                 }
    69.  
    70.                 if (Input.GetButton("Secondary Action"))
    71.                 {
    72.                     Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    73.                     RaycastHit hit;
    74.  
    75.                     if (Physics.Raycast(ray, out hit, range, modificationLayerMask))
    76.                     {
    77.                         if (GetTerrainAtObject(hit.transform.gameObject))
    78.                         {
    79.                             targetTerrain = GetTerrainAtObject(hit.transform.gameObject);
    80.                             SetEditValues(targetTerrain);
    81.                         }
    82.  
    83.                         switch (modificationAction)
    84.                         {
    85.                             case TerrainModificationAction.Flatten:
    86.                                 sampledHeight = SampleHeight(targetTerrain, hit.point);
    87.                                 break;
    88.                         }
    89.                     }
    90.                 }
    91.             }
    92.         }
    93.  
    94.     public void SetEditValues(Terrain terrain)
    95.     {
    96.         targetTerrainData = GetCurrentTerrainData();
    97.         terrainHeightMap = GetCurrentTerrainHeightMap();
    98.         terrainHeightMapWidth = GetCurrentTerrainWidth();
    99.         terrainHeightMapHeight = GetCurrentTerrainHeight();
    100.     }
    101.  
    102.     public void RaiseTerrain(Terrain terrain, Vector3 location, float effectIncrement)
    103.     {
    104.         int offset = areaOfEffectSize / 2;
    105.  
    106.         Vector3 tempCoord = (location - terrain.GetPosition());
    107.         Vector3 coord;
    108.  
    109.         coord = new Vector3
    110.             (
    111.             (tempCoord.x / GetTerrainSize().x),
    112.             (tempCoord.y / GetTerrainSize().y),
    113.             (tempCoord.z / GetTerrainSize().z)
    114.             );
    115.  
    116.         Vector3 locationInTerrain = new Vector3(coord.x * terrainHeightMapWidth, 0, coord.z * terrainHeightMapHeight);
    117.  
    118.         int terX = (int)locationInTerrain.x - offset;
    119.  
    120.         int terZ = (int)locationInTerrain.z - offset;
    121.  
    122.         float[,] heights = targetTerrainData.GetHeights(terX, terZ, areaOfEffectSize, areaOfEffectSize);
    123.  
    124.         for (int xx = 0; xx < areaOfEffectSize; xx++)
    125.         {
    126.             for (int yy = 0; yy < areaOfEffectSize; yy++)
    127.             {
    128.                 heights[xx, yy] += (effectIncrement * Time.smoothDeltaTime);
    129.             }
    130.         }
    131.  
    132.         targetTerrainData.SetHeights(terX, terZ, heights);
    133.     }
    134.  
    135.     public void LowerTerrain(Terrain terrain, Vector3 location, float effectIncrement)
    136.     {
    137.         int offset = areaOfEffectSize / 2;
    138.  
    139.         Vector3 tempCoord = (location - terrain.GetPosition());
    140.         Vector3 coord;
    141.  
    142.         coord = new Vector3
    143.             (
    144.             (tempCoord.x / GetTerrainSize().x),
    145.             (tempCoord.y / GetTerrainSize().y),
    146.             (tempCoord.z / GetTerrainSize().z)
    147.             );
    148.  
    149.         Vector3 locationInTerrain = new Vector3(coord.x * terrainHeightMapWidth, 0, coord.z * terrainHeightMapHeight);
    150.  
    151.         int terX = (int)locationInTerrain.x - offset;
    152.  
    153.         int terZ = (int)locationInTerrain.z - offset;
    154.  
    155.         float[,] heights = targetTerrainData.GetHeights(terX, terZ, areaOfEffectSize, areaOfEffectSize);
    156.  
    157.         for (int xx = 0; xx < areaOfEffectSize; xx++)
    158.         {
    159.             for (int yy = 0; yy < areaOfEffectSize; yy++)
    160.             {
    161.                 heights[xx, yy] -= (effectIncrement * Time.smoothDeltaTime);
    162.             }
    163.         }
    164.  
    165.         targetTerrainData.SetHeights(terX, terZ, heights);
    166.     }
    167.  
    168.     public void FlattenTerrain(Terrain terrain, Vector3 location, float sampledHeight)
    169.     {
    170.         int offset = areaOfEffectSize / 2;
    171.  
    172.         Vector3 tempCoord = (location - terrain.GetPosition());
    173.         Vector3 coord;
    174.  
    175.         coord = new Vector3
    176.             (
    177.             (tempCoord.x / GetTerrainSize().x),
    178.             (tempCoord.y / GetTerrainSize().y),
    179.             (tempCoord.z / GetTerrainSize().z)
    180.             );
    181.  
    182.         Vector3 locationInTerrain = new Vector3(coord.x * terrainHeightMapWidth, 0, coord.z * terrainHeightMapHeight);
    183.  
    184.         int terX = (int)locationInTerrain.x - offset;
    185.  
    186.         int terZ = (int)locationInTerrain.z - offset;
    187.  
    188.         float[,] heights = targetTerrainData.GetHeights(terX, terZ, areaOfEffectSize, areaOfEffectSize);
    189.  
    190.         for (int xx = 0; xx < areaOfEffectSize; xx++)
    191.         {
    192.             for (int yy = 0; yy < areaOfEffectSize; yy++)
    193.             {
    194.                 if (heights[xx, yy] != sampledHeight)
    195.                 {
    196.                     heights[xx, yy] = sampledHeight;
    197.                 }
    198.             }
    199.         }
    200.  
    201.         targetTerrainData.SetHeights(terX, terZ, heights);
    202.     }
    203.  
    204.     public float SampleHeight(Terrain terrain, Vector3 location)
    205.     {
    206.         Vector3 tempCoord = (location - terrain.GetPosition());
    207.         Vector3 coord;
    208.  
    209.         coord = new Vector3
    210.             (
    211.             (tempCoord.x / GetTerrainSize().x),
    212.             (tempCoord.y / GetTerrainSize().y),
    213.             (tempCoord.z / GetTerrainSize().z)
    214.             );
    215.  
    216.         Vector3 locationInTerrain = new Vector3(coord.x * terrainHeightMapWidth, 0, coord.z * terrainHeightMapHeight);
    217.  
    218.         int terX = (int)locationInTerrain.x;
    219.  
    220.         int terZ = (int)locationInTerrain.z;
    221.  
    222.         return Mathf.LerpUnclamped(0f, 1f, (terrain.terrainData.GetHeight(terX, terZ) / terrain.terrainData.size.y));
    223.     }
    224.  
    225.     public Terrain GetTerrainAtObject(GameObject gameObject)
    226.     {
    227.         if (gameObject.GetComponent<Terrain>())
    228.         {
    229.             return gameObject.GetComponent<Terrain>();
    230.         }
    231.  
    232.         return default(Terrain);
    233.     }
    234.  
    235.     public Terrain GetCurrentTerrain()
    236.     {
    237.         if (targetTerrain)
    238.         {
    239.             return targetTerrain;
    240.         }
    241.  
    242.         return default(Terrain);
    243.     }
    244.  
    245.     public TerrainData GetCurrentTerrainData()
    246.     {
    247.         if (targetTerrain)
    248.         {
    249.             return targetTerrain.terrainData;
    250.         }
    251.  
    252.         return default(TerrainData);
    253.     }
    254.  
    255.     public Vector3 GetTerrainSize()
    256.     {
    257.         if (targetTerrain)
    258.         {
    259.             return targetTerrain.terrainData.size;
    260.         }
    261.  
    262.         return Vector3.zero;
    263.     }
    264.  
    265.     public float[,] GetCurrentTerrainHeightMap()
    266.     {
    267.         if (targetTerrain)
    268.         {
    269.             return targetTerrain.terrainData.GetHeights(0, 0, targetTerrain.terrainData.heightmapWidth, targetTerrain.terrainData.heightmapHeight);
    270.         }
    271.  
    272.         return default(float[,]);
    273.     }
    274.  
    275.     public int GetCurrentTerrainWidth()
    276.     {
    277.         if (targetTerrain)
    278.         {
    279.             return targetTerrain.terrainData.heightmapWidth;
    280.         }
    281.  
    282.         return 0;
    283.     }
    284.  
    285.     public int GetCurrentTerrainHeight()
    286.     {
    287.         if (targetTerrain)
    288.         {
    289.             return targetTerrain.terrainData.heightmapHeight;
    290.         }
    291.  
    292.         return 0;
    293.     }
    294. }
    295.  
    If you hate copying and pasting (like me) you can download the file here: https://sabercathost.com/ds1u/TerrainTool.cs

    Note: This tool is still being optimized as soon as I get more into unity terrains I will update it more with features (ex, painting, smooth raising and lowering, etc) so please, if you have any ideas or suggestions share them here with me and thanks!

    Update (September 28th, 2021)

    It has been almost 4 years since I've posted this code and I think it's time to update it :)

    Code (CSharp):
    1. using System.Linq;
    2.  
    3. using UnityEngine;
    4.  
    5. public sealed class TerrainTool : MonoBehaviour
    6. {
    7.     public enum TerrainModificationAction
    8.     {
    9.         Raise,
    10.         Lower,
    11.         Flatten,
    12.         Sample,
    13.         SampleAverage,
    14.     }
    15.  
    16.     public int brushWidth;
    17.     public int brushHeight;
    18.  
    19.     [Range(0.001f, 0.1f)]
    20.     public float strength;
    21.  
    22.     public TerrainModificationAction modificationAction;
    23.  
    24.     private Terrain _targetTerrain;
    25.  
    26.     private float _sampledHeight;
    27.  
    28.     private void Update()
    29.     {
    30.         if (Input.GetMouseButton(0))
    31.         {
    32.             if (Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition), out var hit))
    33.             {
    34.                 if (hit.transform.TryGetComponent(out Terrain terrain)) _targetTerrain = terrain;
    35.  
    36.                 switch (modificationAction)
    37.                 {
    38.                     case TerrainModificationAction.Raise:
    39.  
    40.                         RaiseTerrain(hit.point, strength, brushWidth, brushHeight);
    41.  
    42.                         break;
    43.  
    44.                     case TerrainModificationAction.Lower:
    45.  
    46.                         LowerTerrain(hit.point, strength, brushWidth, brushHeight);
    47.  
    48.                         break;
    49.  
    50.                     case TerrainModificationAction.Flatten:
    51.  
    52.                         FlattenTerrain(hit.point, _sampledHeight, brushWidth, brushHeight);
    53.  
    54.                         break;
    55.  
    56.                     case TerrainModificationAction.Sample:
    57.  
    58.                         _sampledHeight = SampleHeight(hit.point);
    59.  
    60.                         break;
    61.  
    62.                     case TerrainModificationAction.SampleAverage:
    63.  
    64.                         _sampledHeight = SampleAverageHeight(hit.point, brushWidth, brushHeight);
    65.  
    66.                         break;
    67.                 }
    68.             }
    69.         }
    70.     }
    71.  
    72.     private TerrainData GetTerrainData() => _targetTerrain.terrainData;
    73.  
    74.     private int GetHeightmapResolution() => GetTerrainData().heightmapResolution;
    75.  
    76.     private Vector3 GetTerrainSize() => GetTerrainData().size;
    77.  
    78.     public Vector3 WorldToTerrainPosition(Vector3 worldPosition)
    79.     {
    80.         var terrainPosition = worldPosition - _targetTerrain.GetPosition();
    81.  
    82.         var terrainSize = GetTerrainSize();
    83.  
    84.         var heightmapResolution = GetHeightmapResolution();
    85.  
    86.         terrainPosition = new Vector3(terrainPosition.x / terrainSize.x, terrainPosition.y / terrainSize.y, terrainPosition.z / terrainSize.z);
    87.  
    88.         return new Vector3(terrainPosition.x * heightmapResolution, 0, terrainPosition.z * heightmapResolution);
    89.     }
    90.  
    91.     public Vector2Int GetBrushPosition(Vector3 worldPosition, int brushWidth, int brushHeight)
    92.     {
    93.         var terrainPosition = WorldToTerrainPosition(worldPosition);
    94.  
    95.         var heightmapResolution = GetHeightmapResolution();
    96.  
    97.         return new Vector2Int((int)Mathf.Clamp(terrainPosition.x - brushWidth / 2.0f, 0.0f, heightmapResolution), (int)Mathf.Clamp(terrainPosition.z - brushHeight / 2.0f, 0.0f, heightmapResolution));
    98.     }
    99.  
    100.     public Vector2Int GetSafeBrushSize(int brushX, int brushY, int brushWidth, int brushHeight)
    101.     {
    102.         var heightmapResolution = GetHeightmapResolution();
    103.  
    104.         while (heightmapResolution - (brushX + brushWidth) < 0) brushWidth--;
    105.  
    106.         while (heightmapResolution - (brushY + brushHeight) < 0) brushHeight--;
    107.  
    108.         return new Vector2Int(brushWidth, brushHeight);
    109.     }
    110.  
    111.     public void RaiseTerrain(Vector3 worldPosition, float strength, int brushWidth, int brushHeight)
    112.     {
    113.         var brushPosition = GetBrushPosition(worldPosition, brushWidth, brushHeight);
    114.  
    115.         var brushSize = GetSafeBrushSize(brushPosition.x, brushPosition.y, brushWidth, brushHeight);
    116.  
    117.         var terrainData = GetTerrainData();
    118.  
    119.         var heights = terrainData.GetHeights(brushPosition.x, brushPosition.y, brushSize.x, brushSize.y);
    120.  
    121.         for (var y = 0; y < brushSize.y; y++)
    122.         {
    123.             for (var x = 0; x < brushSize.x; x++)
    124.             {
    125.                 heights[y, x] += strength * Time.deltaTime;
    126.             }
    127.         }
    128.  
    129.         terrainData.SetHeights(brushPosition.x, brushPosition.y, heights);
    130.     }
    131.  
    132.     public void LowerTerrain(Vector3 worldPosition, float strength, int brushWidth, int brushHeight)
    133.     {
    134.         var brushPosition = GetBrushPosition(worldPosition, brushWidth, brushHeight);
    135.  
    136.         var brushSize = GetSafeBrushSize(brushPosition.x, brushPosition.y, brushWidth, brushHeight);
    137.  
    138.         var terrainData = GetTerrainData();
    139.  
    140.         var heights = terrainData.GetHeights(brushPosition.x, brushPosition.y, brushSize.x, brushSize.y);
    141.  
    142.         for (var y = 0; y < brushSize.y; y++)
    143.         {
    144.             for (var x = 0; x < brushSize.x; x++)
    145.             {
    146.                 heights[y, x] -= strength * Time.deltaTime;
    147.             }
    148.         }
    149.  
    150.         terrainData.SetHeights(brushPosition.x, brushPosition.y, heights);
    151.     }
    152.  
    153.     public void FlattenTerrain(Vector3 worldPosition, float height, int brushWidth, int brushHeight)
    154.     {
    155.         var brushPosition = GetBrushPosition(worldPosition, brushWidth, brushHeight);
    156.  
    157.         var brushSize = GetSafeBrushSize(brushPosition.x, brushPosition.y, brushWidth, brushHeight);
    158.  
    159.         var terrainData = GetTerrainData();
    160.  
    161.         var heights = terrainData.GetHeights(brushPosition.x, brushPosition.y, brushSize.x, brushSize.y);
    162.  
    163.         for (var y = 0; y < brushSize.y; y++)
    164.         {
    165.             for (var x = 0; x < brushSize.x; x++)
    166.             {
    167.                 heights[y, x] = height;
    168.             }
    169.         }
    170.  
    171.         terrainData.SetHeights(brushPosition.x, brushPosition.y, heights);
    172.     }
    173.  
    174.     public float SampleHeight(Vector3 worldPosition)
    175.     {
    176.         var terrainPosition = WorldToTerrainPosition(worldPosition);
    177.  
    178.         return GetTerrainData().GetInterpolatedHeight((int)terrainPosition.x, (int)terrainPosition.z);
    179.     }
    180.  
    181.     public float SampleAverageHeight(Vector3 worldPosition, int brushWidth, int brushHeight)
    182.     {
    183.         var brushPosition = GetBrushPosition(worldPosition, brushWidth, brushHeight);
    184.  
    185.         var brushSize = GetSafeBrushSize(brushPosition.x, brushPosition.y, brushWidth, brushHeight);
    186.  
    187.         var heights2D = GetTerrainData().GetHeights(brushPosition.x, brushPosition.y, brushSize.x, brushSize.y);
    188.  
    189.         var heights = new float[heights2D.Length];
    190.  
    191.         var i = 0;
    192.  
    193.         for (int y = 0; y <= heights2D.GetUpperBound(0); y++)
    194.         {
    195.             for (int x = 0; x <= heights2D.GetUpperBound(1); x++)
    196.             {
    197.                 heights[i++] = heights2D[y, x];
    198.             }
    199.         }
    200.  
    201.         return heights.Average();
    202.     }
    203. }
    The updated code above works with the most recent Unity version and is much better and simpler compared to the previous one. It also includes a fix for the annoying out-of-bounds errors that used to occur in the old one.
     
    Last edited: Sep 28, 2021
  2. Ben_Iyan

    Ben_Iyan

    Joined:
    May 13, 2016
    Posts:
    204
    @KillerOfSteam7 It was very kind of you to share your work. It's exactly what I was looking for. Thanks.
     
    WinterboltGames likes this.
  3. WinterboltGames

    WinterboltGames

    Joined:
    Jul 27, 2016
    Posts:
    258
    You're welcome.
     
    Last edited: Sep 28, 2021
  4. LodestoneNetwork

    LodestoneNetwork

    Joined:
    Jul 12, 2018
    Posts:
    42
    I know im kinda late, but how could i lower the terrain a small bit on the mouse click? I would use this with a shovel, kinda like you are digging. (im using raycasting) Also, would you put this script on the terrain itself, or anywhere?
     
    WinterboltGames likes this.
  5. SheZii

    SheZii

    Joined:
    May 26, 2016
    Posts:
    4
    Can't thank you more. Will update you in a couple of days.
     
    WinterboltGames likes this.
  6. sofschmi

    sofschmi

    Joined:
    Sep 7, 2018
    Posts:
    2
    Hello! I know this post is a couple of years old. But could somebody please explain how this script should be used? I have tried several things, but still not sure how to get around it.

    Nevertheless, thank you for sharing it!
     
  7. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,243
    SeanBotha likes this.
  8. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,243
    Here's a quick video of the above in action:

     
    heartingNinja, Wothanar and pjbaron like this.
  9. Wothanar

    Wothanar

    Joined:
    Sep 2, 2016
    Posts:
    122
    nice , its looks great. concrats and thanks.
     
  10. none3d

    none3d

    Joined:
    Mar 4, 2021
    Posts:
    167
    Hello
    this code not working in unity 2019
    please help me
    thanks
     
  11. none3d

    none3d

    Joined:
    Mar 4, 2021
    Posts:
    167
    Do you have any comments?
     
  12. WinterboltGames

    WinterboltGames

    Joined:
    Jul 27, 2016
    Posts:
    258
    I might update the script later this month.
     
    none3d likes this.
  13. none3d

    none3d

    Joined:
    Mar 4, 2021
    Posts:
    167
    thanks for your responding
    please also add the "attachment of the texture to the terrain" by code
     
  14. WinterboltGames

    WinterboltGames

    Joined:
    Jul 27, 2016
    Posts:
    258
    The original post has been updated if anyone's interested.
     
    yotmev, t-m-jelinek and milox777 like this.
  15. yotmev

    yotmev

    Joined:
    Feb 8, 2021
    Posts:
    2
    Thank you so much for this. I think this will be super helpful for my next project.

    Do you know how to go about editing the texture of a terrain in realtime (ideally using splatmaps)?

    Edit:
    Also, it seems as though that the modifications made to the terrain are not centered to the mouse position. It seems like the center-point is a bit to the bottom-left of the mouse cursor.

    Let me know if that makes sense. If not, I can try to further explain and/or add pictures.
     
    Last edited: Nov 1, 2021
  16. WinterboltGames

    WinterboltGames

    Joined:
    Jul 27, 2016
    Posts:
    258
    I'll look into both of those issues later.
     
  17. yotmev

    yotmev

    Joined:
    Feb 8, 2021
    Posts:
    2
    I think I may have found the issue.

    It occurs within the GetBrushPosition() method.

    Code (CSharp):
    1. public Vector2Int GetBrushPosition(Vector3 worldPosition, int brushWidth, int brushHeight)
    2.     {
    3.         var terrainPosition = WorldToTerrainPosition(worldPosition);
    4.         var heightmapResolution = GetHeightmapResolution();
    5.         return new Vector2Int((int)Mathf.Clamp(terrainPosition.x - brushWidth / 2.0f, 0.0f, heightmapResolution), (int)Mathf.Clamp(terrainPosition.z - brushHeight / 2.0f, 0.0f, heightmapResolution));
    6.     }
    The last line there is subtracting (brushHeight / 2.0f) from the terrainPosition. I think that's the part that's causing the brush's position to be offset from center. I went ahead and updated the line to:

    Code (CSharp):
    1. return new Vector2Int((int)Mathf.Clamp(terrainPosition.x, 0.0f, heightmapResolution), (int)Mathf.Clamp(terrainPosition.z, 0.0f, heightmapResolution));
    This seems to make it more centered. There are times where it is still offset, but I believe that's due to the fact that GetBrushPosition() (along with GetSafeBrushSize()) are returning ints and then being applied to a non-int space (the terrain map). In other words, rounding issues caused by going from ints to floats.

    Please let me know if this makes sense to you. I am pretty new to Unity and programming in general, so I may be way off with all this.

    Again, thank you for all of your help!

    -----

    EDIT:

    Okay, so it looks like I'm wrong here. I was originally testing out the brushes at these settings:
    Brush Width: 3
    Brush Height: 3

    At these relatively small values, I was experiencing that offset issue. When I changed the code as per above, the problem seemed to have been solved.

    However, when I test the Brush Width and Brush Height settings at higher values (like at 15 each), then my solution no longer works. The offset issue comes back. Though when I revert back to your original code, then it works well again.

    It seems that with your original code, the "critical point" of when the offset issue occurs is when both Brush Width and Brush Height dips below ~4 (note, I've only tested with perfect squared sizes, not rectangular).

    My current code looks like this:
    Code (CSharp):
    1. ...
    2. public int brushWidth;
    3. public int brushHeight;
    4. public int criticalPointOfOffset = 4;
    5. ...
    6. public Vector2Int GetBrushPosition(Vector3 worldPosition, int brushWidth, int brushHeight)
    7.     {
    8.         var terrainPosition = WorldToTerrainPosition(worldPosition);
    9.         var heightmapResolution = GetHeightmapResolution();
    10.         if (brushWidth >= criticalPointOfOffset)
    11.         {
    12.             return new Vector2Int((int)Mathf.Clamp(terrainPosition.x - brushWidth / 2.0f, 0.0f, heightmapResolution), (int)Mathf.Clamp(terrainPosition.z - brushHeight / 2.0f, 0.0f, heightmapResolution));
    13.         }
    14.         return new Vector2Int((int)Mathf.Clamp(terrainPosition.x, 0.0f, heightmapResolution), (int)Mathf.Clamp(terrainPosition.z, 0.0f, heightmapResolution));
    15.     }
    -----

    Also, semi-related, would it be possible/how would you go about designing a circular brush instead of a square brush?
     
    Last edited: Nov 2, 2021
  18. maszto

    maszto

    Joined:
    Mar 14, 2020
    Posts:
    1
    Hi,
    are you still working on this script? It doesn't work for me. I try to use it as a shovel, with object detection, it works fine but the terrain doesn't change when I enter. There are no bugs. I have no idea why that happened.
     
  19. rabin-tech

    rabin-tech

    Joined:
    Dec 29, 2021
    Posts:
    2
    I used. It was so good.
    Because I had several cameras, I used it in a different way. (Of course, I set the projection mode to orthographic mode)
    You only need to connect the camera after placing the code on the ground (Terrain )
    Thank you.



    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using System.Linq;
    5.  
    6.  
    7. public sealed class TerrainTool : MonoBehaviour
    8. {
    9.     public enum TerrainModificationAction
    10.     {
    11.         Raise,
    12.         Lower,
    13.         Flatten,
    14.         Sample,
    15.         SampleAverage,
    16.     }
    17.  
    18.     public int brushWidth;
    19.     public int brushHeight;
    20.  
    21.     public   Camera camera_;
    22.     [Range(0.001f, 0.1f)]
    23.     public float strength;
    24.  
    25.     public TerrainModificationAction modificationAction;
    26.  
    27.     private Terrain _targetTerrain;
    28.  
    29.     private float _sampledHeight;
    30.  
    31.     private void Update()
    32.     {
    33.         if (Input.GetMouseButton(0))
    34.         {
    35.             if (Physics.Raycast(camera_.ScreenPointToRay(Input.mousePosition), out var hit))
    36.             {
    37.                 if (hit.transform.TryGetComponent(out Terrain terrain)) _targetTerrain = terrain;
    38.  
    39.                 switch (modificationAction)
    40.                 {
    41.                     case TerrainModificationAction.Raise:
    42.  
    43.                         RaiseTerrain(hit.point, strength, brushWidth, brushHeight);
    44.  
    45.                         break;
    46.  
    47.                     case TerrainModificationAction.Lower:
    48.  
    49.                         LowerTerrain(hit.point, strength, brushWidth, brushHeight);
    50.  
    51.                         break;
    52.  
    53.                     case TerrainModificationAction.Flatten:
    54.  
    55.                         FlattenTerrain(hit.point, _sampledHeight, brushWidth, brushHeight);
    56.  
    57.                         break;
    58.  
    59.                     case TerrainModificationAction.Sample:
    60.  
    61.                         _sampledHeight = SampleHeight(hit.point);
    62.  
    63.                         break;
    64.  
    65.                     case TerrainModificationAction.SampleAverage:
    66.  
    67.                         _sampledHeight = SampleAverageHeight(hit.point, brushWidth, brushHeight);
    68.  
    69.                         break;
    70.                  
    71.  
    72.                 }
    73.             }
    74.         }
    75.     }
    76.  
    77.     private TerrainData GetTerrainData() => _targetTerrain.terrainData;
    78.  
    79.     private int GetHeightmapResolution() => GetTerrainData().heightmapResolution;
    80.  
    81.     private Vector3 GetTerrainSize() => GetTerrainData().size;
    82.  
    83.     public Vector3 WorldToTerrainPosition(Vector3 worldPosition)
    84.     {
    85.         var terrainPosition = worldPosition - _targetTerrain.GetPosition();
    86.  
    87.         var terrainSize = GetTerrainSize();
    88.  
    89.         var heightmapResolution = GetHeightmapResolution();
    90.  
    91.         terrainPosition = new Vector3(terrainPosition.x / terrainSize.x, terrainPosition.y / terrainSize.y, terrainPosition.z / terrainSize.z);
    92.  
    93.         return new Vector3(terrainPosition.x * heightmapResolution, 0, terrainPosition.z * heightmapResolution);
    94.     }
    95.  
    96.     public Vector2Int GetBrushPosition(Vector3 worldPosition, int brushWidth, int brushHeight)
    97.     {
    98.         var terrainPosition = WorldToTerrainPosition(worldPosition);
    99.  
    100.         var heightmapResolution = GetHeightmapResolution();
    101.  
    102.         return new Vector2Int((int)Mathf.Clamp(terrainPosition.x - brushWidth / 2.0f, 0.0f, heightmapResolution), (int)Mathf.Clamp(terrainPosition.z - brushHeight / 2.0f, 0.0f, heightmapResolution));
    103.     }
    104.  
    105.     public Vector2Int GetSafeBrushSize(int brushX, int brushY, int brushWidth, int brushHeight)
    106.     {
    107.         var heightmapResolution = GetHeightmapResolution();
    108.  
    109.         while (heightmapResolution - (brushX + brushWidth) < 0) brushWidth--;
    110.  
    111.         while (heightmapResolution - (brushY + brushHeight) < 0) brushHeight--;
    112.  
    113.         return new Vector2Int(brushWidth, brushHeight);
    114.     }
    115.  
    116.     public void RaiseTerrain(Vector3 worldPosition, float strength, int brushWidth, int brushHeight)
    117.     {
    118.         var brushPosition = GetBrushPosition(worldPosition, brushWidth, brushHeight);
    119.  
    120.         var brushSize = GetSafeBrushSize(brushPosition.x, brushPosition.y, brushWidth, brushHeight);
    121.  
    122.         var terrainData = GetTerrainData();
    123.  
    124.         var heights = terrainData.GetHeights(brushPosition.x, brushPosition.y, brushSize.x, brushSize.y);
    125.  
    126.         for (var y = 0; y < brushSize.y; y++)
    127.         {
    128.             for (var x = 0; x < brushSize.x; x++)
    129.             {
    130.                 heights[y, x] += strength * Time.deltaTime;
    131.             }
    132.         }
    133.  
    134.         terrainData.SetHeights(brushPosition.x, brushPosition.y, heights);
    135.     }
    136.  
    137.     public void LowerTerrain(Vector3 worldPosition, float strength, int brushWidth, int brushHeight)
    138.     {
    139.         var brushPosition = GetBrushPosition(worldPosition, brushWidth, brushHeight);
    140.  
    141.         var brushSize = GetSafeBrushSize(brushPosition.x, brushPosition.y, brushWidth, brushHeight);
    142.  
    143.         var terrainData = GetTerrainData();
    144.  
    145.         var heights = terrainData.GetHeights(brushPosition.x, brushPosition.y, brushSize.x, brushSize.y);
    146.  
    147.         for (var y = 0; y < brushSize.y; y++)
    148.         {
    149.             for (var x = 0; x < brushSize.x; x++)
    150.             {
    151.                 heights[y, x] -= strength * Time.deltaTime;
    152.             }
    153.         }
    154.  
    155.         terrainData.SetHeights(brushPosition.x, brushPosition.y, heights);
    156.     }
    157.  
    158.     public void FlattenTerrain(Vector3 worldPosition, float height, int brushWidth, int brushHeight)
    159.     {
    160.         var brushPosition = GetBrushPosition(worldPosition, brushWidth, brushHeight);
    161.  
    162.         var brushSize = GetSafeBrushSize(brushPosition.x, brushPosition.y, brushWidth, brushHeight);
    163.  
    164.         var terrainData = GetTerrainData();
    165.  
    166.         var heights = terrainData.GetHeights(brushPosition.x, brushPosition.y, brushSize.x, brushSize.y);
    167.  
    168.         for (var y = 0; y < brushSize.y; y++)
    169.         {
    170.             for (var x = 0; x < brushSize.x; x++)
    171.             {
    172.                 heights[y, x] = height;
    173.             }
    174.         }
    175.  
    176.         terrainData.SetHeights(brushPosition.x, brushPosition.y, heights);
    177.     }
    178.  
    179.     public float SampleHeight(Vector3 worldPosition)
    180.     {
    181.         var terrainPosition = WorldToTerrainPosition(worldPosition);
    182.  
    183.         return GetTerrainData().GetInterpolatedHeight((int)terrainPosition.x, (int)terrainPosition.z);
    184.     }
    185.  
    186.     public float SampleAverageHeight(Vector3 worldPosition, int brushWidth, int brushHeight)
    187.     {
    188.         var brushPosition = GetBrushPosition(worldPosition, brushWidth, brushHeight);
    189.  
    190.         var brushSize = GetSafeBrushSize(brushPosition.x, brushPosition.y, brushWidth, brushHeight);
    191.  
    192.         var heights2D = GetTerrainData().GetHeights(brushPosition.x, brushPosition.y, brushSize.x, brushSize.y);
    193.  
    194.         var heights = new float[heights2D.Length];
    195.  
    196.         var i = 0;
    197.  
    198.         for (int y = 0; y <= heights2D.GetUpperBound(0); y++)
    199.         {
    200.             for (int x = 0; x <= heights2D.GetUpperBound(1); x++)
    201.             {
    202.                 heights[i++] = heights2D[y, x];
    203.             }
    204.         }
    205.  
    206.         return heights.Average();
    207.     }
    208. }
     
  20. myazuid

    myazuid

    Joined:
    Jan 29, 2019
    Posts:
    26
  21. runnair

    runnair

    Joined:
    Aug 3, 2018
    Posts:
    1
    Winterbolt Games!

    Thank you very much! It is working perfectly with my hand grenade script.

    At the void I'm spawning the explosion effect and the crater object I also get the terrain (collision.gameObject) then I put a GrassDeleter.cs on every terrain and calculate the impact point then remove the grass at the explosion radius.
     
  22. SteenPetersen

    SteenPetersen

    Joined:
    Mar 13, 2016
    Posts:
    95

    You can do something like this to update the texture on the terrain after using this script:


    Code (CSharp):
    1.  
    2. private void UpdateTerrainTexture(Vector3 worldPosition, int textureIndex, int textureWidth, int textureHeight)
    3.         {
    4.             // Get the position of the texture brush and the safe size of the brush
    5.             Vector2Int brushPosition = GetBrushPosition(worldPosition, textureWidth, textureHeight);
    6.             Vector2Int brush = GetSafeBrushSize(brushPosition.x, brushPosition.y, textureWidth, textureHeight);
    7.             // Get the terrain data at the location
    8.             TerrainData terrainData = GetTerrainData();
    9.             // Get the number of textures on the terrain
    10.             int texturesCount = terrainData.alphamapLayers;
    11.             // Create a 3D array to hold the new alpha values for each texture on the terrain
    12.             float[,,] textureAlphas = new float[brush.y, brush.x, texturesCount];
    13.             // Loop through each pixel in the brush area and set the alpha value for the specified texture index to 1, and 0 for all others
    14.             for (var y = 0; y < brush.y; y++)
    15.             {
    16.                 for (var x = 0; x < brush.x; x++)
    17.                 {
    18.                     for (var i = 0; i < texturesCount; i++)
    19.                     {
    20.                         // If the current texture index matches the specified texture index, set its alpha value to 1
    21.                         // Otherwise, set its alpha value to 0
    22.                         textureAlphas[y, x, i] = (i == textureIndex) ? 1.0f : 0.0f;
    23.                     }
    24.                 }
    25.             }
    26.             // Set the alpha map at the specified position to the updated texture alphas
    27.             terrainData.SetAlphamaps(brushPosition.x, brushPosition.y, textureAlphas);
    28.         }
    29.  
    Remember to Terrain.Flush(); after doing this. [https://docs.unity3d.com/ScriptReference/Terrain.Flush.html]
     
  23. TsundereChan

    TsundereChan

    Joined:
    Oct 13, 2017
    Posts:
    2
    You are a legend!
    I was working on my own solution until I found this.
     
  24. TungNguyen2046

    TungNguyen2046

    Joined:
    Jan 5, 2022
    Posts:
    1
    Thank you so much, I tried to create my own script but had so much problems because SetHeights was very performance costly. Turned out the key is to set the base coordinates on your location instead of 0,0.