Search Unity

  1. Unity 2020.1 has been released.
    Dismiss Notice
  2. Good news ✨ We have more Unite Now videos available for you to watch on-demand! Come check them out and ask our experts any questions!
    Dismiss Notice

Terrain Adjust Terrain along a Spline

Discussion in 'World Building' started by Rowlan, Jul 11, 2020.

  1. Rowlan

    Rowlan

    Joined:
    Aug 4, 2016
    Posts:
    1,400
    Unity seems to take their time with the release of the Splines. I've had this code for alignment of the terrain along Sebastian Lague's Path Creator for quite some time and thought I'd share in case anyone else has use for it until the Unity Splines surface which is hopefully soon-ish. Here's how it looks like:

    splines.gif

    The Code:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class TerrainAdjusterRuntime : MonoBehaviour
    6. {
    7.     public Terrain terrain;
    8.     public PathCreation.PathCreator pathCreator;
    9.     public float brushFallOff = 0.3f;
    10.     float[,] originalTerrainHeights;
    11.  
    12.     void Start()
    13.     {
    14.         SaveOriginalTerrainHeights();
    15.     }
    16.  
    17.     void SaveOriginalTerrainHeights()
    18.     {
    19.         TerrainData terrainData = terrain.terrainData;
    20.  
    21.         int w = terrainData.heightmapResolution;
    22.         int h = terrainData.heightmapResolution;
    23.  
    24.         originalTerrainHeights = terrainData.GetHeights(0, 0, w, h);
    25.  
    26.     }
    27.  
    28.     void Update()
    29.     {
    30.         if (terrain && pathCreator)
    31.         {
    32.             ShapeTerrain(terrain, pathCreator);
    33.         }
    34.     }
    35.  
    36.     void ShapeTerrain(Terrain currentTerrain, PathCreation.PathCreator currentPathCreator)
    37.     {
    38.  
    39.         Vector3 terrainPosition = currentTerrain.gameObject.transform.position;
    40.         TerrainData terrainData = currentTerrain.terrainData;
    41.  
    42.         // both GetHeights and SetHeights use normalized height values, where 0.0 equals to terrain.transform.position.y in the world space and 1.0 equals to terrain.transform.position.y + terrain.terrainData.size.y in the world space
    43.         // so when using GetHeight you have to manually divide the value by the Terrain.activeTerrain.terrainData.size.y which is the configured height ("Terrain Height") of the terrain.
    44.         float terrainMin = currentTerrain.transform.position.y + 0f;
    45.         float terrainMax = currentTerrain.transform.position.y + currentTerrain.terrainData.size.y;
    46.         float totalHeight = terrainMax - terrainMin;
    47.  
    48.         int w = terrainData.heightmapResolution;
    49.         int h = terrainData.heightmapResolution;
    50.  
    51.         // clone the original data, the modifications along the path are based on them
    52.         float[,] allHeights = originalTerrainHeights.Clone() as float[,];
    53.  
    54.         // the blur radius values being used for the various passes
    55.         int[] initialPassRadii = { 15, 7, 2 };
    56.  
    57.         for (int pass = 0; pass < initialPassRadii.Length; pass++)
    58.         {
    59.             int radius = initialPassRadii[pass];
    60.  
    61.             // points as vertices, not equi-distant
    62.             Vector3[] vertexPoints = currentPathCreator.path.vertices;
    63.  
    64.             // equi-distant points
    65.             List<Vector3> distancePoints = new List<Vector3>();
    66.  
    67.             // spacing along the array, can speed up the loops
    68.             float arrayIterationSpacing = 1;
    69.  
    70.             for (float t = 0; t <= currentPathCreator.path.length; t += arrayIterationSpacing)
    71.             {
    72.                 Vector3 point = currentPathCreator.path.GetPointAtDistance(t, PathCreation.EndOfPathInstruction.Stop);
    73.  
    74.                 distancePoints.Add(point);
    75.             }
    76.  
    77.             // sort by height reverse
    78.             // sequential height raising would just lead to irregularities, ie when a higher point follows a lower point
    79.             // we need to proceed from top to bottom height
    80.             distancePoints.Sort((a, b) => -a.y.CompareTo(b.y));
    81.  
    82.             Vector3[] points = distancePoints.ToArray();
    83.  
    84.             foreach (var point in points)
    85.             {
    86.  
    87.                 float targetHeight = (point.y - terrainPosition.y) / totalHeight;
    88.  
    89.                 int centerX = (int)(currentPathCreator.transform.position.z + point.z);
    90.                 int centerY = (int)(currentPathCreator.transform.position.x + point.x);
    91.  
    92.                 AdjustTerrain(allHeights, radius, centerX, centerY, targetHeight);
    93.  
    94.             }
    95.         }
    96.  
    97.         currentTerrain.terrainData.SetHeights(0, 0, allHeights);
    98.     }
    99.  
    100.     private void AdjustTerrain(float[,] heightMap, int radius, int centerX, int centerY, float targetHeight)
    101.     {
    102.         float deltaHeight = targetHeight - heightMap[centerX, centerY];
    103.         int sqrRadius = radius * radius;
    104.  
    105.         int width = heightMap.GetLength(0);
    106.         int height = heightMap.GetLength(1);
    107.  
    108.         for (int offsetY = -radius; offsetY <= radius; offsetY++)
    109.         {
    110.             for (int offsetX = -radius; offsetX <= radius; offsetX++)
    111.             {
    112.                 int sqrDstFromCenter = offsetX * offsetX + offsetY * offsetY;
    113.  
    114.                 // check if point is inside brush radius
    115.                 if (sqrDstFromCenter <= sqrRadius)
    116.                 {
    117.                     // calculate brush weight with exponential falloff from center
    118.                     float dstFromCenter = Mathf.Sqrt(sqrDstFromCenter);
    119.                     float t = dstFromCenter / radius;
    120.                     float brushWeight = Mathf.Exp(-t * t / brushFallOff);
    121.  
    122.                     // raise terrain
    123.                     int brushX = centerX + offsetX;
    124.                     int brushY = centerY + offsetY;
    125.  
    126.                     if (brushX >= 0 && brushY >= 0 && brushX < width && brushY < height)
    127.                     {
    128.                         heightMap[brushX, brushY] += deltaHeight * brushWeight;
    129.  
    130.                         // clamp the height
    131.                         if (heightMap[brushX, brushY] > targetHeight)
    132.                         {
    133.                             heightMap[brushX, brushY] = targetHeight;
    134.                         }
    135.                     }
    136.                 }
    137.             }
    138.         }
    139.     }
    140. }
    Basically the code iterates along the spline, adjusts the terrain to the height of the spline segment positions and blurs the surrounding terrain.

    It might be interesting to create a DOTS version of it. I hope I find the time soon. In case anyone else beats me to it, please do share.

    Credits and thanks to Sebastian Lague.

    ps: That's of course only for toying around in play mode (executed in update) and has to be converted to an edit mode feature. But I rather wait for the official Unity Splines for that and other optimizations.
     
    Last edited: Jul 11, 2020
    wyattt_ likes this.
  2. Rowlan

    Rowlan

    Joined:
    Aug 4, 2016
    Posts:
    1,400
    Here's an updated version that uses the Unity Editor

    Code (CSharp):
    1. using System.Collections.Generic;
    2. using UnityEngine;
    3.  
    4. [ExecuteInEditMode]
    5. public class TerrainAdjusterRuntime : MonoBehaviour
    6. {
    7.     public Terrain terrain;
    8.  
    9.     [Range(0f,1f)]
    10.     public float brushFallOff = 0.3f;
    11.  
    12.     [Range(1f,10f)]
    13.     public float brushSpacing = 1f;
    14.  
    15.     [HideInInspector]
    16.     public PathCreation.PathCreator pathCreator;
    17.  
    18.     private float[,] originalTerrainHeights;
    19.  
    20.     // the blur radius values being used for the various passes
    21.     public int[] initialPassRadii = { 15, 7, 2 };
    22.  
    23.     void Start()
    24.     {
    25.         pathCreator = GetComponent<PathCreation.PathCreator>();
    26.  
    27.         if( pathCreator == null)
    28.             Debug.LogError("Script must be attached to a PathCreator GameObject");
    29.  
    30.     }
    31.  
    32.     public void SaveOriginalTerrainHeights()
    33.     {
    34.         if (terrain == null || pathCreator == null)
    35.             return;
    36.  
    37.         Debug.Log("Saving original terrain data");
    38.  
    39.         TerrainData terrainData = terrain.terrainData;
    40.  
    41.         int w = terrainData.heightmapResolution;
    42.         int h = terrainData.heightmapResolution;
    43.  
    44.         originalTerrainHeights = terrainData.GetHeights(0, 0, w, h);
    45.  
    46.     }
    47.  
    48.     public void CleanUp()
    49.     {
    50.         originalTerrainHeights = null;
    51.         Debug.Log("Deleting original terrain data");
    52.     }
    53.  
    54.     public void ShapeTerrain()
    55.     {
    56.         if (terrain == null || pathCreator == null)
    57.             return;
    58.  
    59.         // save original terrain in case the terrain got added later
    60.         if (originalTerrainHeights == null)
    61.             SaveOriginalTerrainHeights();
    62.  
    63.         Vector3 terrainPosition = terrain.gameObject.transform.position;
    64.         TerrainData terrainData = terrain.terrainData;
    65.  
    66.         // both GetHeights and SetHeights use normalized height values, where 0.0 equals to terrain.transform.position.y in the world space and 1.0 equals to terrain.transform.position.y + terrain.terrainData.size.y in the world space
    67.         // so when using GetHeight you have to manually divide the value by the Terrain.activeTerrain.terrainData.size.y which is the configured height ("Terrain Height") of the terrain.
    68.         float terrainMin = terrain.transform.position.y + 0f;
    69.         float terrainMax = terrain.transform.position.y + terrain.terrainData.size.y;
    70.         float totalHeight = terrainMax - terrainMin;
    71.  
    72.         int w = terrainData.heightmapResolution;
    73.         int h = terrainData.heightmapResolution;
    74.  
    75.         // clone the original data, the modifications along the path are based on them
    76.         float[,] allHeights = originalTerrainHeights.Clone() as float[,];
    77.  
    78.         // the blur radius values being used for the various passes
    79.         for (int pass = 0; pass < initialPassRadii.Length; pass++)
    80.         {
    81.             int radius = initialPassRadii[pass];
    82.  
    83.             // points as vertices, not equi-distant
    84.             Vector3[] vertexPoints = pathCreator.path.vertices;
    85.  
    86.             // equi-distant points
    87.             List<Vector3> distancePoints = new List<Vector3>();
    88.  
    89.             for (float t = 0; t <= pathCreator.path.length; t += brushSpacing)
    90.             {
    91.                 Vector3 point = pathCreator.path.GetPointAtDistance(t, PathCreation.EndOfPathInstruction.Stop);
    92.  
    93.                 distancePoints.Add(point);
    94.             }
    95.  
    96.             // sort by height reverse
    97.             // sequential height raising would just lead to irregularities, ie when a higher point follows a lower point
    98.             // we need to proceed from top to bottom height
    99.             distancePoints.Sort((a, b) => -a.y.CompareTo(b.y));
    100.  
    101.             Vector3[] points = distancePoints.ToArray();
    102.  
    103.             foreach (var point in points)
    104.             {
    105.  
    106.                 float targetHeight = (point.y - terrainPosition.y) / totalHeight;
    107.  
    108.                 int centerX = (int)(pathCreator.transform.position.z + point.z);
    109.                 int centerY = (int)(pathCreator.transform.position.x + point.x);
    110.  
    111.                 AdjustTerrain(allHeights, radius, centerX, centerY, targetHeight);
    112.  
    113.             }
    114.         }
    115.  
    116.         terrain.terrainData.SetHeights(0, 0, allHeights);
    117.     }
    118.  
    119.     private void AdjustTerrain(float[,] heightMap, int radius, int centerX, int centerY, float targetHeight)
    120.     {
    121.         float deltaHeight = targetHeight - heightMap[centerX, centerY];
    122.         int sqrRadius = radius * radius;
    123.  
    124.         int width = heightMap.GetLength(0);
    125.         int height = heightMap.GetLength(1);
    126.  
    127.         for (int offsetY = -radius; offsetY <= radius; offsetY++)
    128.         {
    129.             for (int offsetX = -radius; offsetX <= radius; offsetX++)
    130.             {
    131.                 int sqrDstFromCenter = offsetX * offsetX + offsetY * offsetY;
    132.  
    133.                 // check if point is inside brush radius
    134.                 if (sqrDstFromCenter <= sqrRadius)
    135.                 {
    136.                     // calculate brush weight with exponential falloff from center
    137.                     float dstFromCenter = Mathf.Sqrt(sqrDstFromCenter);
    138.                     float t = dstFromCenter / radius;
    139.                     float brushWeight = Mathf.Exp(-t * t / brushFallOff);
    140.  
    141.                     // raise terrain
    142.                     int brushX = centerX + offsetX;
    143.                     int brushY = centerY + offsetY;
    144.  
    145.                     if (brushX >= 0 && brushY >= 0 && brushX < width && brushY < height)
    146.                     {
    147.                         heightMap[brushX, brushY] += deltaHeight * brushWeight;
    148.  
    149.                         // clamp the height
    150.                         if (heightMap[brushX, brushY] > targetHeight)
    151.                         {
    152.                             heightMap[brushX, brushY] = targetHeight;
    153.                         }
    154.                     }
    155.                 }
    156.             }
    157.         }
    158.     }
    159.  
    160. }
    and put this into the Editor folder

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3.  
    4. [CustomEditor(typeof(TerrainAdjusterRuntime))]
    5. public class TerrainAdjusterRuntimeEditor : Editor
    6. {
    7.     TerrainAdjusterRuntimeEditor editor;
    8.  
    9.     public void OnEnable()
    10.     {
    11.         this.editor = this;
    12.  
    13.         TerrainAdjusterRuntime targetGameObject = (TerrainAdjusterRuntime)target;
    14.  
    15.         if (targetGameObject.pathCreator != null)
    16.         {
    17.             targetGameObject.pathCreator.pathUpdated -= OnPathChanged;
    18.             targetGameObject.pathCreator.pathUpdated += OnPathChanged;
    19.         }
    20.  
    21.         targetGameObject.SaveOriginalTerrainHeights();
    22.     }
    23.  
    24.  
    25.     void OnDisable()
    26.     {
    27.         TerrainAdjusterRuntime targetGameObject = (TerrainAdjusterRuntime)target;
    28.  
    29.         // remove original terrain data
    30.         targetGameObject.CleanUp();
    31.  
    32.         // remove existing listeners
    33.         if (targetGameObject.pathCreator != null)
    34.         {
    35.             targetGameObject.pathCreator.pathUpdated -= OnPathChanged;
    36.         }
    37.  
    38.     }
    39.  
    40.  
    41.     void OnPathChanged()
    42.     {
    43.         TerrainAdjusterRuntime targetGameObject = (TerrainAdjusterRuntime)target;
    44.  
    45.         targetGameObject.ShapeTerrain();
    46.     }
    47.  
    48.     public override void OnInspectorGUI()
    49.     {
    50.         TerrainAdjusterRuntime targetGameObject = (TerrainAdjusterRuntime)target;
    51.  
    52.         EditorGUI.BeginChangeCheck();
    53.  
    54.         DrawDefaultInspector();
    55.  
    56.         if (EditorGUI.EndChangeCheck())
    57.         {
    58.             // if anything (eg falloff) changed recreate the terrain under the path
    59.             OnPathChanged();
    60.         }
    61.  
    62.         EditorGUILayout.BeginVertical();
    63.  
    64.         if (GUILayout.Button("Flatten entire terrain"))
    65.         {
    66.             SetTerrainHeight(targetGameObject.terrain, 0f);
    67.         }
    68.  
    69.         EditorGUILayout.EndVertical();
    70.     }
    71.  
    72.     void SetTerrainHeight(Terrain terrain, float height)
    73.     {
    74.         TerrainData terrainData = terrain.terrainData;
    75.  
    76.         int w = terrainData.heightmapResolution;
    77.         int h = terrainData.heightmapResolution;
    78.         float[,] allHeights = terrainData.GetHeights(0, 0, w, h);
    79.  
    80.         float terrainMin = terrain.transform.position.y + 0f;
    81.         float terrainMax = terrain.transform.position.y + terrain.terrainData.size.y;
    82.         float totalHeight = terrainMax - terrainMin;
    83.  
    84.         for (int x = 0; x < w; x++)
    85.         {
    86.             for (int y = 0; y < h; y++)
    87.             {
    88.                 allHeights[y, x] = 0f;
    89.             }
    90.         }
    91.  
    92.         terrain.terrainData.SetHeights(0, 0, allHeights);
    93.     }
    94.  
    95. }
    96.  
    Then assign the TerrainAdjusterRuntime script to the PathCreator gameobject of your scene.
     
  3. wyattt_

    wyattt_

    Unity Technologies

    Joined:
    May 9, 2018
    Posts:
    314
    This is awesome!
     
    Rowlan likes this.
  4. jRocket

    jRocket

    Joined:
    Jul 12, 2012
    Posts:
    538
    Where did you hear about Unity officially having splines? I don't see anything on the roadmap.
     
  5. Rowlan

    Rowlan

    Joined:
    Aug 4, 2016
    Posts:
    1,400
    Splines will be tightly integrated in 2021. Unity didn't show a roadmap yet, but some of the Unity devs mentioned it on this forum.
     
  6. Reanimate_L

    Reanimate_L

    Joined:
    Oct 10, 2009
    Posts:
    2,422
  7. Rowlan

    Rowlan

    Joined:
    Aug 4, 2016
    Posts:
    1,400
    I tried that when I started working on YAPP. That spline tool even came with a blog post of the author. However it never got finished afaik and was rather not working, has slowdowns. So I used a free community solution. At least at the time years ago. Sebastian Lague's is an excellent solution now. There are others out there as well. Still an in-built one is always preferred.
     
  8. Reanimate_L

    Reanimate_L

    Joined:
    Oct 10, 2009
    Posts:
    2,422
    oh i totally forgot about Yapp
     
unityunity