
【Unity】オブジェクトからTerrainの地形データを自動生成する

IT技術

Unity(ユニティ)でのマップ生成は大変!
ロールプレイングゲームやアクションゲームを作成する中で、最も大変な作業といっても過言ではないのが「マップ生成」です。
Unity では、マップ・地形を生成する際に「Terrain」という機能を使用します。
GUI(グラフィカル・ユーザ・インターフェース)で非常に使いやすい機能です。
しかし、直感的に使える反面、広大なマップを作ろうとすると、膨大な労力がかかってしまいます。
だからと言って、他のソフトで作成した地形を取り込んでも、Unity 上で草木や花などのアセットを敷くことは出来ません。
その対処法として、今回は、3D オブジェクトとして取り込んだデータから、Terrain の地形データを自動生成する方法を紹介したいと思います!
UnityEditor に Terrain 生成機能を付与する!
今回は、ゲームビューを起動すると Terrain が完成するのではなく、Editor 機能として、オブジェクトから Terrain を作成できるようにします。
ソースコードを入れるフォルダを用意する
新しく「Editor」という名前のフォルダを、「Assets」フォルダの直下に用意します。

ここで作成した「Editor」フォルダにスクリプトを入れないと、ビルドや書き出しの際に、エラーが出てしまう可能性があります。
※注意※
「Editor」フォルダ以外の場所でも問題ありませんが、ビルドするときは、Unity のプロジェクトの外にバックアップを取り、削除するようにしてください。
ソースコードをフォルダに配置する
先ほど用意した「Editor」フォルダに、ソースコードを配置します。
そして、右クリックから「Create」→「C#Script」を選択し、名前は「Object2Terrain」にしてみましょう。
ソースコード
ソースコードは以下の通りです。
1using UnityEngine;
2using UnityEditor;
3
4public class Object2Terrain : EditorWindow
5{
6
7 [MenuItem("Terrain/Object to Terrain", false, 2000)]
8 static void OpenWindow()
9 {
10
11 EditorWindow.GetWindow<Object2Terrain>(true);
12 }
13
14 private int resolution = 512;
15 private Vector3 addTerrain;
16 int bottomTopRadioSelected = 0;
17 static string[] bottomTopRadio = new string[] { "Bottom Up", "Top Down" };
18 private float shiftHeight = 0f;
19
20 void OnGUI()
21 {
22
23 resolution = EditorGUILayout.IntField("Resolution", resolution);
24 addTerrain = EditorGUILayout.Vector3Field("Add terrain", addTerrain);
25 shiftHeight = EditorGUILayout.Slider("Shift height", shiftHeight, -1f, 1f);
26 bottomTopRadioSelected = GUILayout.SelectionGrid(bottomTopRadioSelected, bottomTopRadio, bottomTopRadio.Length, EditorStyles.radioButton);
27
28 if (GUILayout.Button("Create Terrain"))
29 {
30
31 if (Selection.activeGameObject == null)
32 {
33
34 EditorUtility.DisplayDialog("No object selected", "Please select an object.", "Ok");
35 return;
36 }
37
38 else
39 {
40
41 CreateTerrain();
42 }
43 }
44 }
45
46 delegate void CleanUp();
47
48 void CreateTerrain()
49 {
50
51 //fire up the progress bar
52 ShowProgressBar(1, 100);
53
54 TerrainData terrain = new TerrainData();
55 terrain.heightmapResolution = resolution;
56 GameObject terrainObject = Terrain.CreateTerrainGameObject(terrain);
57
58 Undo.RegisterCreatedObjectUndo(terrainObject, "Object to Terrain");
59
60 MeshCollider collider = Selection.activeGameObject.GetComponent<MeshCollider>();
61 CleanUp cleanUp = null;
62
63 //Add a collider to our source object if it does not exist.
64 //Otherwise raycasting doesn't work.
65 if (!collider)
66 {
67
68 collider = Selection.activeGameObject.AddComponent<MeshCollider>();
69 cleanUp = () => DestroyImmediate(collider);
70 }
71
72 Bounds bounds = collider.bounds;
73 float sizeFactor = collider.bounds.size.y / (collider.bounds.size.y + addTerrain.y);
74 terrain.size = collider.bounds.size + addTerrain;
75 bounds.size = new Vector3(terrain.size.x, collider.bounds.size.y, terrain.size.z);
76
77 // Do raycasting samples over the object to see what terrain heights should be
78 float[,] heights = new float[terrain.heightmapWidth, terrain.heightmapHeight];
79 Ray ray = new Ray(new Vector3(bounds.min.x, bounds.max.y + bounds.size.y, bounds.min.z), -Vector3.up);
80 RaycastHit hit = new RaycastHit();
81 float meshHeightInverse = 1 / bounds.size.y;
82 Vector3 rayOrigin = ray.origin;
83
84 int maxHeight = heights.GetLength(0);
85 int maxLength = heights.GetLength(1);
86
87 Vector2 stepXZ = new Vector2(bounds.size.x / maxLength, bounds.size.z / maxHeight);
88
89 for (int zCount = 0; zCount < maxHeight; zCount++)
90 {
91
92 ShowProgressBar(zCount, maxHeight);
93
94 for (int xCount = 0; xCount < maxLength; xCount++)
95 {
96
97 float height = 0.0f;
98
99 if (collider.Raycast(ray, out hit, bounds.size.y * 3))
100 {
101
102 height = (hit.point.y - bounds.min.y) * meshHeightInverse;
103 height += shiftHeight;
104
105 //bottom up
106 if (bottomTopRadioSelected == 0)
107 {
108
109 height *= sizeFactor;
110 }
111
112 //clamp
113 if (height < 0)
114 {
115
116 height = 0;
117 }
118 }
119
120 heights[zCount, xCount] = height;
121 rayOrigin.x += stepXZ[0];
122 ray.origin = rayOrigin;
123 }
124
125 rayOrigin.z += stepXZ[1];
126 rayOrigin.x = bounds.min.x;
127 ray.origin = rayOrigin;
128 }
129
130 terrain.SetHeights(0, 0, heights);
131
132 EditorUtility.ClearProgressBar();
133
134 if (cleanUp != null)
135 {
136
137 cleanUp();
138 }
139 }
140
141 void ShowProgressBar(float progress, float maxProgress)
142 {
143
144 float p = progress / maxProgress;
145 EditorUtility.DisplayProgressBar("Creating Terrain...", Mathf.RoundToInt(p * 100f) + " %", p);
146 }
147}
動作を確認する
下の画像のように、Editor の機能部分に、新しく「Terrain」というボタンが出来ていれば、成功です!

Terrain ウィンドウから、「Objetct to Terrain」を選択してください。
すると、以下のようなウィンドウが表示されます。

このウィンドウが出ている状態で、Terrain のもととなるオブジェクト選択します。
オブジェクトを選択したら、「Create Terrain」を押してください。
生成した結果
すると…

できました!!
今回は、タンスと同じ形の Terrain を生成しています。
これで、草木をはやしたり、花を咲かせることが可能となりました。
Terrain を生成する上での注意点
この機能を使う上で、いくつか注意点があります。
1つのメッシュを持つオブジェクトに適用させる!
2つ以上のオブジェクトが親子構造になっていて、それぞれにメッシュがついている場合は使用できません。
親子構造であっても、アセットに よくある形の一つのメッシュが親についていて、パーツごとに子がついている場合は問題ありません。
完成した Terrain はプレハブ化できない!
完成した Terrain をプロジェクトビューにドラッグ&ドロップしても、正しくプレハブが生成されません。
これは、「.meta」ファイルに対応した「.assets」ファイルが生成されないことが原因です。
また、元となったオブジェクトのメッシュファイルは削除しないようにしてください。
使いすぎるとプロジェクトの容量が大きくなる!
何度も試行錯誤を繰り返すと、どんどんプロジェクトの容量が大きくなっていきます。
これは、毎回「Resources」フォルダに一時ファイルが生成されることが原因です。
最終的に使うもの以外は、削除してください。
頂点の数が大きすぎると Terrain が生成されない!
パソコンの性能にもよりますが、2000がボーダーラインです。
あらかじめ、blender などで分割しておきましょう。
さいごに
今回は、オブジェクトから Terrain を自動生成する方法を紹介しました。
最も大変な作業といっても過言ではない「マップ生成」。
今回紹介した方法で、少しでも負担が軽くなればと思います。
ぜひ、今回紹介した「オブジェクトから Terrain の地形データを自動生成する」方法を試してみてください!
こちらの記事もオススメ!
2020.07.28Unity 特集知識編おすすめのゲームエンジン5選実装編※最新記事順に並べています。VR環境でテキストを表示する方法非同期式の入れ子処...
2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ

「好きを仕事にするエンジニア集団」の(株)ライトコードです! ライトコードは、福岡、東京、大阪、名古屋の4拠点で事業展開するIT企業です。 現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。 いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。 システム開発依頼・お見積もり大歓迎! また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」を積極採用中です! インターンや新卒採用も行っております。 以下よりご応募をお待ちしております! https://rightcode.co.jp/recruit