From Unify Community Wiki
Jump to: navigation, search

Eric Haines (Eric5h5): original.
Jessy: C# conversion.
Bit Barrel Media: added functionality.



Converts an object mesh to a heightmap. This way you can create terrain meshes in a standard 3D app such as Blender or Maya and convert it to a Unity terrain. It uses raycasting instead of file conversion, so it works with any object which has a mesh.

See also TerrainObjExporter, which saves a Unity terrain as an .obj file.


You must place the script in a folder named Editor in your project's Assets folder for it to work properly. The source object must contain a mesh. Selecting a parent object will not work.

If in doubt, simply open the mesh asset in the project view, then drag the bare mesh into the scene or hierarchy: MeshIllustration.png

Note: The script uses the axis-aligned bounding box of the mesh, so object rotations on the x and z axis other than 0, or rotations on the y axis other than multiples of 90 degrees, may give somewhat odd results.

C# (updated):
-Click on an object in the scene view or hierarchy.
-Select from the top menu: Terrain->Object to Terrain.
-A new terrain GameObject is automatically created.

Java (original):
-Add a default terrain GameObject to the scene.
-Click on an object in the scene view or hierarchy.
-Select from the top menu: Terrain->Object to Terrain.
-The object is then converted to the heightmap in the active terrain.

Settings (C# only)

-Resolution: the resolution of the generated terrain.
-Add terrain: add blank terrain to the front/back, left/right and above. Handy if you want to create additional content. Increase this if the edge of the terrain is cut off.
-Shift height: move the terrain up or down. The terrain GameObject will stay in the same position. Range limited to the vertical terrain size.
-Bottom up: terrain is generated from the bottom up. This will ensure a 1 to 1 resemblance of the source object.
-Top down: terrain is generated from top to bottom. This will stretch the terrain if a larger y value of "Add terrain" is used. This is the original mode but gives somewhat odd results.
-Create terrain: start generating the terrain. Depending on the resolution, this might take a while.

C# - Object2Terrain.cs

using UnityEngine;
using UnityEditor;
public class Object2Terrain : EditorWindow {
	[MenuItem("Terrain/Object to Terrain", false, 2000)] static void OpenWindow () {
	private int resolution = 512;
	private Vector3 addTerrain;
	int bottomTopRadioSelected = 0;
	static string[] bottomTopRadio = new string[] { "Bottom Up", "Top Down"};
	private float shiftHeight = 0f;
	void OnGUI () {
		resolution = EditorGUILayout.IntField("Resolution", resolution);
		addTerrain = EditorGUILayout.Vector3Field("Add terrain", addTerrain);
		shiftHeight = EditorGUILayout.Slider("Shift height", shiftHeight, -1f, 1f);
		bottomTopRadioSelected = GUILayout.SelectionGrid(bottomTopRadioSelected, bottomTopRadio, bottomTopRadio.Length, EditorStyles.radioButton);
		if(GUILayout.Button("Create Terrain")){
			if(Selection.activeGameObject == null){
				EditorUtility.DisplayDialog("No object selected", "Please select an object.", "Ok");
	delegate void CleanUp();
	void CreateTerrain(){	
		//fire up the progress bar
		ShowProgressBar(1, 100);
		TerrainData terrain = new TerrainData();
		terrain.heightmapResolution = resolution;
		GameObject terrainObject = Terrain.CreateTerrainGameObject(terrain);
		Undo.RegisterCreatedObjectUndo(terrainObject, "Object to Terrain");
		MeshCollider collider = Selection.activeGameObject.GetComponent<MeshCollider>();
		CleanUp cleanUp = null;
		//Add a collider to our source object if it does not exist.
		//Otherwise raycasting doesn't work.
			collider = Selection.activeGameObject.AddComponent<MeshCollider>();
			cleanUp = () => DestroyImmediate(collider);
		Bounds bounds = collider.bounds;	
		float sizeFactor = collider.bounds.size.y / (collider.bounds.size.y + addTerrain.y);
		terrain.size = collider.bounds.size + addTerrain;
		bounds.size = new Vector3(terrain.size.x, collider.bounds.size.y, terrain.size.z);
		// Do raycasting samples over the object to see what terrain heights should be
		float[,] heights = new float[terrain.heightmapWidth, terrain.heightmapHeight];	
		Ray ray = new Ray(new Vector3(bounds.min.x, bounds.max.y + bounds.size.y, bounds.min.z), -Vector3.up);
		RaycastHit hit = new RaycastHit();
		float meshHeightInverse = 1 / bounds.size.y;
		Vector3 rayOrigin = ray.origin;
		int maxHeight = heights.GetLength(0);
		int maxLength = heights.GetLength(1);
		Vector2 stepXZ = new Vector2(bounds.size.x / maxLength, bounds.size.z / maxHeight);
		for(int zCount = 0; zCount < maxHeight; zCount++){
			ShowProgressBar(zCount, maxHeight);
			for(int xCount = 0; xCount < maxLength; xCount++){
				float height = 0.0f;
				if(collider.Raycast(ray, out hit, bounds.size.y * 3)){
					height = (hit.point.y - bounds.min.y) * meshHeightInverse;
					height += shiftHeight;
					//bottom up
					if(bottomTopRadioSelected == 0){
						height *= sizeFactor;
					if(height < 0){
						height = 0;
				heights[zCount, xCount] = height;
           		rayOrigin.x += stepXZ[0];
           		ray.origin = rayOrigin;
			rayOrigin.z += stepXZ[1];
      		rayOrigin.x = bounds.min.x;
      		ray.origin = rayOrigin;
		terrain.SetHeights(0, 0, heights);
		if(cleanUp != null){
    void ShowProgressBar(float progress, float maxProgress){
		float p = progress / maxProgress;
		EditorUtility.DisplayProgressBar("Creating Terrain...", Mathf.RoundToInt(p * 100f)+ " %", p);

JavaScript - Object2Terrain.js

@MenuItem ("Terrain/Object to Terrain")
static function Object2Terrain () {
	// See if a valid object is selected
	var obj = Selection.activeObject as GameObject;
	if (obj == null) { 
		EditorUtility.DisplayDialog("No object selected", "Please select an object.", "Cancel");
	if (obj.GetComponent(MeshFilter) == null) {
		EditorUtility.DisplayDialog("No mesh selected", "Please select an object with a mesh.", "Cancel");
	else if ((obj.GetComponent(MeshFilter) as MeshFilter).sharedMesh == null) {
		EditorUtility.DisplayDialog("No mesh selected", "Please select an object with a valid mesh.", "Cancel");
	if (Terrain.activeTerrain == null) {
		EditorUtility.DisplayDialog("No terrain found", "Please make sure a terrain exists.", "Cancel");
	var terrain = Terrain.activeTerrain.terrainData;
	// If there's no mesh collider, add one (and then remove it later when done)
	var addedCollider = false;
	var addedMesh = false;
	var objCollider = obj.collider as MeshCollider;
	if (objCollider == null) {
		objCollider = obj.AddComponent(MeshCollider);
		addedCollider = true;
	else if (objCollider.sharedMesh == null) {
		objCollider.sharedMesh = (obj.GetComponent(MeshFilter) as MeshFilter).sharedMesh;
		addedMesh = true;
	Undo.RegisterUndo (terrain, "Object to Terrain");
	var resolutionX = terrain.heightmapWidth;
	var resolutionZ = terrain.heightmapHeight;
	var heights = terrain.GetHeights(0, 0, resolutionX, resolutionZ);
	// Use bounds a bit smaller than the actual object; otherwise raycasting tends to miss at the edges
	var objectBounds = objCollider.bounds;
	var leftEdge = objectBounds.center.x - objectBounds.extents.x + .01;
	var bottomEdge = objectBounds.center.z - objectBounds.extents.z + .01;
	var stepX = (objectBounds.size.x - .019) / resolutionX;
	var stepZ = (objectBounds.size.z - .019) / resolutionZ;
	// Set up raycast vars
	var y = objectBounds.center.y + objectBounds.extents.y + .01;
	var hit : RaycastHit;
	var ray = new Ray(Vector3.zero, -Vector3.up);
	var rayDistance = objectBounds.size.y + .02;
	var heightFactor = 1.0 / rayDistance;
	// Do raycasting samples over the object to see what terrain heights should be
	var z = bottomEdge;
	for (zCount = 0; zCount < resolutionZ; zCount++) {
		var x = leftEdge;
		for (xCount = 0; xCount < resolutionX; xCount++) {
			ray.origin = Vector3(x, y, z);
			if (objCollider.Raycast(ray, hit, rayDistance)) {
				heights[zCount, xCount] = 1.0 - (y - hit.point.y)*heightFactor;
			else {
				heights[zCount, xCount] = 0.0;
			x += stepX;
		z += stepZ;
	terrain.SetHeights(0, 0, heights);
	if (addedMesh) {
		objCollider.sharedMesh = null;
	if (addedCollider) {
Personal tools