From Unify Community Wiki
Revision as of 01:38, 30 June 2011 by Jessy (Talk | contribs)

Jump to: navigation, search

Author: Eric Haines (Eric5h5); C# conversion by Jessy



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. (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.

Click on an object in the scene view or hierarchy, then select Object to Terrain from the Terrain menu. The object is then converted to the heightmap in the active terrain. The object you're clicking on must contain the mesh itself--for example, selecting a root transform object, where the actual mesh is contained in the child object, won't work. If in doubt, simply open the mesh asset in the project view, then drag the bare mesh into the scene or hierarchy:


This function 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.

You will probably need to adjust the height of the terrain to match the object mesh (using Set Resolution… in the Terrain menu).

JavaScript - Object2Terrain.js

<javascript> @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"); return; } if (obj.GetComponent(MeshFilter) == null) { EditorUtility.DisplayDialog("No mesh selected", "Please select an object with a mesh.", "Cancel"); return; } else if ((obj.GetComponent(MeshFilter) as MeshFilter).sharedMesh == null) { EditorUtility.DisplayDialog("No mesh selected", "Please select an object with a valid mesh.", "Cancel"); return; } if (Terrain.activeTerrain == null) { EditorUtility.DisplayDialog("No terrain found", "Please make sure a terrain exists.", "Cancel"); return; } 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) { DestroyImmediate(objCollider); } } </javascript>

Additional Usage Notes for C# version

The main difference with this version of the script involves the Size Adjustment parameter. After you choose the Object to Terrain menu item, a utility window will pop up with the field for adjustment of this parameter automatically selected. Try leaving it at zero at first – just hit return/enter to close the window and create the terrain. If necessary, try again with positive vales to grow the terrain horizontally, or negative values to shrink it (around +/- .01 is probably a good starting point). If you click outside the window, it will close, and the operation will be canceled.

C# - Object2Terrain.cs

<csharp>using UnityEngine; using UnityEditor;

public class Object2Terrain : EditorWindow {

[MenuItem ("Terrain/Object to Terrain", true)] static bool Validate () { try {return Selection.activeTransform.GetComponent<MeshFilter>().sharedMesh && Terrain.activeTerrain;} catch {return false;} }

[MenuItem("Terrain/Object to Terrain", false, 2000)] static void OpenWindow () { EditorWindow.GetWindow<Object2Terrain>(true); }

float sizeAdjustment; // Easily custom-tailor edge behavior

void OnGUI () { GUI.SetNextControlName("Size Adjustment"); sizeAdjustment = EditorGUILayout.FloatField ("Size Adjustment", sizeAdjustment); GUI.FocusControl("Size Adjustment"); if (Event.current.type == EventType.KeyUp && (Event.current.keyCode == KeyCode.Return) || Event.current.keyCode == KeyCode.KeypadEnter) { this.Close(); CreateTerrain(); } }

void OnLostFocus () {this.Close();} // Otherwise the object selection could change and that would screw CreateTerrain() up.

delegate void CleanUp (); void CreateTerrain () { TerrainData terrain = Terrain.activeTerrain.terrainData; Undo.RegisterUndo(terrain, "Object to Terrain");

MeshCollider collider = Selection.activeGameObject.GetComponent<MeshCollider>(); CleanUp cleanUp = null; if (!collider) { collider = Selection.activeGameObject.AddComponent<MeshCollider>(); cleanUp = () => DestroyImmediate(collider); }

Bounds bounds = collider.bounds; bounds.Expand(new Vector3(-sizeAdjustment * bounds.size.x, 0, -sizeAdjustment * bounds.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 * 2, bounds.min.z), -Vector3.up); RaycastHit hit = new RaycastHit(); float meshHeightInverse = 1 / bounds.size.y; Vector3 rayOrigin = ray.origin; Vector2 stepXZ = new Vector2(bounds.size.x / heights.GetLength(1), bounds.size.z / heights.GetLength(0)); for (int zCount = 0; zCount < heights.GetLength(0); zCount++) { for (int xCount = 0; xCount < heights.GetLength(1); xCount++) {

           heights[zCount, xCount] = collider.Raycast(ray, out hit, bounds.size.y * 2) ?

1 - (bounds.max.y - hit.point.y) * meshHeightInverse : 0;

          	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) cleanUp(); }


Personal tools