パーリンノイズを用いてスクリプトでフラクタル地形メッシュを作成してみた

はじめに

今回技術研究を色々行っていくにあたり適当に背景になる使い勝手のいい地形が欲しいなと思いました。
ただデザイナーでもないのでポリゴンをこねこねするのはめんどくさいしUnity以外のツールも手元にない・・
なのでワンボタンで地形メッシュを出せるツールを作ってみました。
今後これを色々改造して様々な表現を作ってみたいと思ってます!

要件

とりあえず今回は地形ぽいメッシュが作れてたらいいと思うので
1.縦横の頂点数が指定できる
2.頂点間隔が指定できる
3.目安の高さが指定できる
4.ランタイムでなくてもいつでも作成できる

この四つを満たす機能を作ってみようと思います!

パーリンノイズを用いて地形を生成する

パーリンノイズは波や雲などゲームでよく使われる乱数生成アルゴリズムの一種です。
今回はこれを用いてなだらかな地形を生成します。
Unityには

Mathf.PerlinNoise

というパーリンノイズを生成する関数が用意されているのでこちらを使いましょう。
Unityでメッシュを生成するにはMeshクラスに頂点情報、インデックス情報を入力して作成します。
まずは頂点を作ってみましょう!

//頂点を設定
for (int i = 0; i < _xVertexNum; i++)
{
   for (int j = 0; j < _zVertexNum; j++)
   {
       var noiseValue = Mathf.PerlinNoise(i * _noiseScale, j * _noiseScale);
       float y = noiseValue * _hegiht;
       vertices.Add(new Vector3(i * _xVertexLength, y, j * _zVertexLength));
        uv.Add(new Vector2(i ,j ));
    }
}

このように縦横の頂点数や頂点間隔の情報を用いて作ります。
またこのUVの箇所に関しては

uv.Add(new Vector2(i ,j ));

一つのメッシュになるので、頂点ごとにテクスチャが繰り返し貼られるように設定します。
(詳しくはリピートテクスチャなどで調べてください)
次はインデックス配列の設定です。

for (int i = 0; i < _xVertexNum - 1; i++)
{
   for (int j = 0; j < _zVertexNum - 1; j++)
   {
       triangles.Add(i * _zVertexNum + j);
       triangles.Add(i * _zVertexNum + j + 1);
       triangles.Add((i + 1) * _zVertexNum + j);
                
       triangles.Add((i + 1) * _zVertexNum + j);
       triangles.Add(i * _zVertexNum + j + 1);
       triangles.Add((i + 1) * _zVertexNum + j + 1);
   }
}

このように設定します。
Unityは左手座標系です。
なので時計回りでインデックスは配置します。
これでメッシュ作成に必要な情報は揃いました。
最終的にはこんな感じになります。

using System.Collections.Generic;
using UnityEngine;

public class TerrainMesh : MonoBehaviour
{
    [SerializeField] private MeshFilter _meshFilter;

    [SerializeField] private float _hegiht = 10.0f;

    [SerializeField] private float _noiseScale = 0.05f;

    [SerializeField] private int _xVertexNum = 100;

    [SerializeField] private int _zVertexNum = 100;

    [SerializeField] private float _xVertexLength = 1.0f;

    [SerializeField] private float _zVertexLength = 1.0f;

    private Mesh _myMesh;

    private void Start()
    {
        CreateMesh();
    }

    private void CreateMesh()
    {
        if (_myMesh != null)
        {
            DestroyImmediate(_myMesh);
        }

        _myMesh = new Mesh();
        _myMesh.name = "TerrainMesh";


        List<Vector3> vertices = new List<Vector3>();
        List<Vector2> uv = new List<Vector2>();
        List<int> triangles = new List<int>();

        //頂点を設定
        for (int i = 0; i < _xVertexNum; i++)
        {
            for (int j = 0; j < _zVertexNum; j++)
            {
                var noiseValue = Mathf.PerlinNoise(i * _noiseScale, j * _noiseScale);
                float y = noiseValue * _hegiht;
                vertices.Add(new Vector3(i * _xVertexLength, y, j * _zVertexLength));

                uv.Add(new Vector2(i, j));
            }
        }

        _myMesh.SetVertices(vertices);
        _myMesh.SetUVs(0, uv);

        //インデックスを設定
        for (int i = 0; i < _xVertexNum - 1; i++)
        {
            for (int j = 0; j < _zVertexNum - 1; j++)
            {
                triangles.Add(i * _zVertexNum + j);
                triangles.Add(i * _zVertexNum + j + 1);
                triangles.Add((i + 1) * _zVertexNum + j);

                triangles.Add((i + 1) * _zVertexNum + j);
                triangles.Add(i * _zVertexNum + j + 1);
                triangles.Add((i + 1) * _zVertexNum + j + 1);
            }
        }

        _myMesh.SetTriangles(triangles.ToArray(), 0);

        _myMesh.RecalculateNormals();

        _meshFilter.mesh = _myMesh;
    }
}

これをこんな感じにアタッチします。
メッシュを表示する場合MeshFilterとMeshRenderer及びMaterialが必要なので忘れずアタッチしましょう(RequireComponentなど使用するのもあり)
実行するとこのように表示されると思います。
ちゃんと作成できました。
しかし実行しないと表示されなかったり、パラメーターの調整がめんどくさかったりするのでエディタ拡張で1ボタンでメッシュを作成できるようにしようと思います。
そのエディタ拡張コードはこんな感じです。

#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(TerrainMesh))]
public class TerrainMeshEditor : Editor
{
    public override void OnInspectorGUI()
    {
        serializedObject.Update();
        
        var meshFilterProperty = serializedObject.FindProperty("_meshFilter");
        EditorGUILayout.PropertyField(meshFilterProperty);
        
        var hegihtProperty = serializedObject.FindProperty("_hegiht");
        EditorGUILayout.PropertyField(hegihtProperty);
        
        var noiseScaleProperty = serializedObject.FindProperty("_noiseScale");
        EditorGUILayout.PropertyField(noiseScaleProperty);
        
        var xVertexNumProperty = serializedObject.FindProperty("_xVertexNum");
        EditorGUILayout.PropertyField(xVertexNumProperty);
        
        var zVertexNumProperty = serializedObject.FindProperty("_zVertexNum");
        EditorGUILayout.PropertyField(zVertexNumProperty);
        
        var xVertexLengthProperty = serializedObject.FindProperty("_xVertexLength");
        EditorGUILayout.PropertyField(xVertexLengthProperty);
        
        var zVertexLengthProperty = serializedObject.FindProperty("_zVertexLength");
        EditorGUILayout.PropertyField(zVertexLengthProperty);
        
        if (GUILayout.Button("メッシュ作成"))
        {
            var terrainMesh = target as TerrainMesh;
            terrainMesh.CreateMesh();
        }

        serializedObject.ApplyModifiedProperties();
    }
}
#endif

またTerrainMeshのCreateMeshをエディタ拡張でもアクセスできるようにパブリックメソッドに変更しておきます。

public void CreateMesh()

*注意:エディタ拡張のコードはEditorフォルダ配下に置くようにしましょう。
これでこのようなボタンを押すと実行中でなくとも生成できるようになりました。
TerrainMeshのStartメソッドも必要なくなったので消しちゃいましょう。
次はこれを使って色々な表現をしていこうと思います。
ここまで見て頂きありがとうございました。