• トップ
  • ブログ一覧
  • 【Unity】オブジェクトからTerrainの地形データを自動生成する
  • 【Unity】オブジェクトからTerrainの地形データを自動生成する

    メディアチームメディアチーム
    2020.05.08

    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 の地形データを自動生成する」方法を試してみてください!

    こちらの記事もオススメ!

    featureImg2020.07.28Unity 特集知識編おすすめのゲームエンジン5選実装編※最新記事順に並べています。VR環境でテキストを表示する方法非同期式の入れ子処...

    featureImg2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    メディアチーム
    メディアチーム
    Show more...

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background