Posted by: Mohamed | 11/11/2013

Terrain deformation script.

Hello everyone,

Again, I’m not regularly blogging, but it’s really difficult to keep up with everything…
Now, since I promised sharing the script which deforms the terrain in real time, let’s dive into the subject 😉

Unfortunately the documentation of Unity3D is very poor when it comes to terrain deformation, and modifying the information of terrain’s height map.
I mean… Look at this: http://docs.unity3d.com/Documentation/ScriptReference/TerrainData.GetHeights.html
No examples, no details whatsoever, and what is more unfortunate also, even if you get the terrain deformation working, it’s still not fast, and if you use it as a core mechanic in your game, I think you have to adjust the resolution of your terrain to something manageable.

As you may already know, terrains in Unity3D are based on 2D height maps. These height maps are just gray-scale images with most black is the lowest point and most white is the highest peak in your terrain.
Unity is constantly monitoring the data from the height map file, and in order to modify the terrain in any way, you have to modify the height map, then Unity will reload the terrain automatically. You have to understand that any modification you do to the height map will be saved, and if you terminate your game and reload it again, whatever you’ve done to the terrain will be reloaded. In case you want your game to have untouched terrain every time the player starts a new game, then you have to save the height map info somewhere.

Let’s identify our problems:
1- We need to modify the terrain at a certain location
2- We need to map that location to our height map
3- We need to define the shape of the crater (or brush if you would like to call it that way 🙂 ) and use it to modify the terrain.

The following script is adjusted in a way that it can work with multiple terrains in your scene.
The code is fully commented. Further explanation will be provided after the code snippet if I saw that it’s needed… if something wasn’t clear for you, feel free to post your questions in the comments section, and I’ll gladly help ;).


using UnityEngine;
using System.Collections;

public class TerrainDeformer : MonoBehaviour
{

#region Public members
 //This class member is used for Crater texture shape, you have to make sure that the access of the texture is Read/Write. You can find it in the advanced section of the import option when you select the texture.
 public Texture2D CraterTexture; 
 // This class member modifies the intensity of the brush, if you don't introduce this factor, you will get harsh results, or you will not have a control over how hard your terrain is gonna be deformed
 public float AlphaIntensity;
 //Here you pass the terrain which you want to deform.
 public Terrain targetTerrain;
 #endregion


 #region Private members
 private float[,] SavedTerrainState { get; set; } // This class member is used to hold the original data of the terrain
 private float[,] craterShape { get; set; } //Since the height map of the terrain is 2D array, and since we are modifying square area of the terrain, we are extracting the gray-scale information from our crater texture, and keeping it to modify the terrain.
 private TextureInfo textureInfo { get; set; } // Texture info is a class I made in order to easily cache and manage the crater texture (brushes).
 #endregion

void Start () {
 // Remember, we are addressing a square map here, so you can imagine that 0,0 is the top left corner of the height map, and the terrain height map width and height are the maximum end points of the square
 SavedTerrainState = targetTerrain.terrainData.GetHeights(0, 0, targetTerrain.terrainData.heightmapWidth, targetTerrain.terrainData.heightmapHeight);
// Look at the constructor of the TextureInfo
textureInfo = new TextureInfo(CraterTexture, AlphaIntensity);
 }

void OnApplicationQuit()
 {
// Here I'm restoring the terrain to it's original state. SetHeights takes the starting point which 0 for x and 0 for y, and 2D array of gray-scale data defining the height map.
 targetTerrain.terrainData.SetHeights(0, 0, SavedTerrainState);
 }

void OnCollisionEnter(Collision _colObject)
 {
 // Here I'm deforming the terrain based on the position of the contact point. It could be a good idea to put a condition to make a certain object with a certain tag can modify the terrain, otherwise, anything which is colliding with the terrain will deform it.
  Utils.DeformTerrainOptimized(_colObject.contacts[0].point, targetTerrain, textureInfo);
 }
}

The script above is the main script which you gonna use/apply with any terrain you have in your scene. Now let’s move to the part where the magic is happening ;).


using UnityEngine;
using System.Collections;

public class Utils {
// This function/method, explains itself =D... basically it maps the point of collision on the height map of the terrain. Once we have the point on the height map, we can carry on from there.
public static Vector3 MapPositionOnHeightMap(Vector3 _v3Position, Terrain _terTerrain)
 {
 return new Vector3(((_v3Position.x - _terTerrain.transform.position.x) / _terTerrain.terrainData.size.x) * _terTerrain.terrainData.heightmapWidth, 0,
 ((_v3Position.z - _terTerrain.transform.position.z) / _terTerrain.terrainData.size.z) * _terTerrain.terrainData.heightmapHeight);
 }

<span style="line-height: 1.5;">/// <summary></span>

 /// Deform Terrain the optimized version
 /// </summary>
 /// <param name="_v3Position">Collision point</param>
 /// <param name="_terTerrain">Current terrain</param>
 /// <param name="TextureInfo">Deformation texture shape</param>
 public static void DeformTerrainOptimized(Vector3 _v3Position, Terrain _terTerrain, TextureInfo _texDeformation)
 {
 _v3Position = Utils.MapPositionOnHeightMap(_v3Position, _terTerrain); // Here we get the point on the height map.

// Right here we are getting the area which we need to modify. I'm dividing the texture's width and height by 2 just to get the center of the texture.. so basically the deformation will not start at the top left corner of the brush, but it will start right at the center of it.
var heightMap = _terTerrain.terrainData.GetHeights((int)_v3Position.x - (_texDeformation.Texture.width / 2), (int)_v3Position.z - (_texDeformation.Texture.height / 2), _texDeformation.Texture.width, _texDeformation.Texture.height); 
// This nested for loop is used to modify that area which we have've got using the information we got from our texture.
for (int i = 0; i < _texDeformation.Texture.height; i++)
 for (int j = 0; j < _texDeformation.Texture.width; j++)
 heightMap[i, j] -= _texDeformation.Alpha[i, j];
// Here we are resetting the height map of the terrain for that specific position. Note that we are not starting from 0,0 any more, but we are starting from the point of collision.
_terTerrain.terrainData.SetHeights((int)_v3Position.x - (_texDeformation.Texture.width / 2), (int)_v3Position.z - (_texDeformation.Texture.height / 2), heightMap);
 }
}

Moving on to the final class which is TextureInfo

using UnityEngine;
using System.Collections;

public class TextureInfo
{

#region Public members
 public Texture2D Texture { get; set; }
// I don't know why I just didn't say {private set;}, but I think it was quite late, and I'm quite lazy to change it now
public float[,] Alpha
 {
 get
 {
 return _Alpha;
 }
 }
 #endregion

#region Private members
 private float[,] _Alpha;
 #endregion
// Constructor with default alpha intensity, it could've been a smarter constructor with a default parameter, but again, it was late I think =D...
public TextureInfo(Texture2D _texTexture2D)
 {
 Texture = _texTexture2D;
 //unflatning 1D array into 2D array to feed it into the set heights. Get pixels returns all pixels in 1 dimensional array, and we need 2D array to modify our height map.
 var pixels = Texture.GetPixels();
 var craterShape = new float[Texture.width, Texture.height];

for (int i = 0; i < Texture.height; i++)
 for (int j = 0; j < Texture.width; j++)
 craterShape[i, j] = pixels[j * Texture.width + i].a * 0.03f;
 _Alpha = craterShape;
 }

public TextureInfo(Texture2D _texTexture2D, float _fAlphaIntensity)
 {
 Texture = _texTexture2D;
 //unflatning 1D array into 2D array to feed it into the set heights.
 var pixels = Texture.GetPixels();
 var craterShape = new float[Texture.width, Texture.height];

for (int i = 0; i < Texture.height; i++)
 for (int j = 0; j < Texture.width; j++)
 craterShape[i, j] = pixels[j * Texture.width + i].a * _fAlphaIntensity;
 _Alpha = craterShape;
 }

}

Remember that your texture has to have an Alpha channel. You could use a PSD file (which is the file format I’ve used in my case) or a PNG file ;).

Feel free to post  your comments or question in the comments, and I will gladly help if you needed any ;).
 

Advertisements

Responses

  1. Hi Mohammad, Thank you very much for sharing your great experience. after hours of headaches and confusion i found your blog post very helpful.
    Could you provide an example scene or make unity package if possible?
    I getting error such out-of-bounds terrain height information, or read access error from texture file, etc.

    • Hi Enz,
      I’m gonna post the Unity3D project as soon as I find it =). And regarding that error, I was getting as well, and I would solve this problem by surrounding that part of code were we edit the terrain by a try/catch block =).


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Categories

%d bloggers like this: