2017年4月20日

Unity 動的にメッシュを作成する ~パーリンノイズ編~


スクリプトからメッシュを作成する方法について、勉強しながらの解説その3。いい感じに起伏のある地形を目指します…

たーのしー!(きもーい!)

※このページの内容の動作確認にはUnity5.3を使用しています。
前回は大きさを自由に設定できるメッシュを作成し、それぞれの頂点の高さをランダムに指定してみました。紙をくしゃっとしたような面白い地形にはなったのですが、これだと隣り合う頂点の高さに何の関連性もなく、自然な見た目にはなりません。

パーリンノイズ

そこで登場するのが「パーリンノイズ」です。パーリンさんが作ったノイズですね。これは、単純にランダムなノイズとは違って、それぞれの地点がどんな勾配になるかを考慮して作られた疑似乱数であり、「不規則だけどなめらかに変化させたい」という場合にもってこいのノイズです。地形生成だけでなく各種エフェクトやCGのテクスチャなど、最近のゲームでは様々なところで使われています。

public float perlinNoiseScale = 1f;
public Vector2 perlinNoiseOffset;
perlinNoiseScale = Mathf.Max(0.0001f, perlinNoiseScale);
float sampleX = (x + perlinNoiseOffset.x) / perlinNoiseScale;
float sampleZ = (z + perlinNoiseOffset.y) / perlinNoiseScale;
float y = Mathf.PerlinNoise(sampleX, sampleZ) * heightMultiplier;
vertices[z * size + x] = new Vector3(x * vertexDistance, y, -z * vertexDistance);

まず変数としてノイズの拡大率(perlinNoiseScale)とノイズのサンプルを取得する位置(perlinNoiseOffset)の2つを用意しました。拡大率を大きくするとなめらかに、小さくするとトゲトゲになります。徒歩で登っているときはなだらかな坂にしか見えなかったものが、上空から離れて見ると山に見えるようなもの…かな?サンプルの取得位置をずらすと、形成される地形が変化します。たとえばここにランダムな値を入れると、毎回違った地形が出来上がります。

次に頂点位置を決める部分にコードを追加します。パーリンノイズはUnityに標準に搭載されていて、Mathf.PerlinNoise()で簡単に取得できます。便利~!パーリンノイズに座標を渡すと、その座標での値(0~1)を返してくれるので、あとはその値を頂点のY座標に適用する、という流れです。さきほど説明したように、perlinNoiseOffsetで座標の位置をずらし、さらにperlinNoiseScaleで割ることで座標が変化するなだらかさを制御しています。あと、perlinNoiseScaleが0に近い数になるとエラーを吐くので、常に0.0001以上になるよう一応対策しておきます。

完成!

パーリンノイズを重ねる

でもこれだとちょっとなめらかすぎます。現実の地形はもう少し凹凸がありますよね。

凹凸をつける方法に「ノイズを重ねる」というものがあります。大きなノイズの波形で山や谷などの広い地形を表し、小さなノイズの波形でさらに山の表面のでこぼこや岩などを追加することができるのです。ノイズを重ねる方法は簡単で、大きい(高さがあるなだらかな)ノイズと小さい(低くトゲトゲした)ノイズを用意し、それを"+"で足し合わせるだけです。

実際どんな感じになるかというと…


こうなります。大まかな起伏はそのままに表面が多少ボコボコっとなっているのがわかるでしょうか。リアルっぽい地形に近づいた気がします。

では、実装です。変更するのは変数宣言の部分と頂点位置(vertices)を決める箇所だけです。

public PerlinNoiseProperty[] perlinNoiseProperty = new PerlinNoiseProperty[1];
[System.Serializable]
public class PerlinNoiseProperty
{
 public float heightMultiplier = 1f;
 public float scale = 1f;
 public Vector2 offset;
}

まず、heightMultiplier、perlinNoiseScale、perlinNoiseOffsetと別々になっていた変数を1つのクラスにまとめました。[System.Serializable]の属性を使ってインスペクタ上で数値の変更ができるようにしてあります。そしてそのクラスの配列を作成しています。この配列の要素が、重ね合わせるノイズに対応しているわけです。たとえば3つノイズを重ねたい場合は、インスペクタ上でこの配列の長さを3にし、それぞれでheightMultiplier、scale、offsetの値を設定します。

Vector3[] vertices = new Vector3[size * size];
for (int z = 0; z < size; z++) {
 for (int x = 0; x < size; x++) {

  float sampleX;
  float sampleZ;
  float y = 0;
  foreach (PerlinNoiseProperty p in perlinNoiseProperty) {
   p.scale = Mathf.Max(0.0001f, p.scale);
   sampleX = (x + p.offset.x) / p.scale;
   sampleZ = (z + p.offset.y) / p.scale;
   y += Mathf.PerlinNoise(sampleX, sampleZ) * p.heightMultiplier;
  }

  vertices[z * size + x] = new Vector3(x * vertexDistance, y, -z * vertexDistance);
 }
}

それから、それぞれの頂点位置を決定するときに、その頂点でのノイズの値を算出します。計算方法自体は全く変わっていませんが、たとえば配列perlinNoisePropertyの長さが3であれば、3回ループを繰り返し、そのつど変数yに値を足し合わせるようになっています。

ちなみにインスペクタ上でこんな感じに設定すれば上の画像の状態になります。2つのノイズを重ねています。



てなわけで今回はここまでです。最後にコード全体を載っけておきます。

using UnityEngine;
using System.Collections;

public class PerlinNoiseMesh : MonoBehaviour
{
 [Range(1, 255)]
 public int size;
 public float vertexDistance = 1f;
 public Material material;
 public PhysicMaterial physicMaterial;

 public PerlinNoiseProperty[] perlinNoiseProperty = new PerlinNoiseProperty[1];
 [System.Serializable]
 public class PerlinNoiseProperty {
  public float heightMultiplier = 1f;
  public float scale = 1f;
  public Vector2 offset;
 }

 void Start()
 {
  CreateMesh();
 }

 void CreateMesh()
 {
  Vector3[] vertices = new Vector3[size * size];
  for (int z = 0; z < size; z++) {
   for (int x = 0; x < size; x++) {

    float sampleX;
    float sampleZ;
    float y = 0;
    foreach (PerlinNoiseProperty p in perlinNoiseProperty) {
     p.scale = Mathf.Max(0.0001f, p.scale);
     sampleX = (x + p.offset.x) / p.scale;
     sampleZ = (z + p.offset.y) / p.scale;
     y += Mathf.PerlinNoise(sampleX, sampleZ) * p.heightMultiplier;
    }

    vertices[z * size + x] = new Vector3(x * vertexDistance, y, -z * vertexDistance);
   }
  }

  int triangleIndex = 0;
  int[] triangles = new int[(size - 1) * (size - 1) * 6];
  for (int z = 0; z < size - 1; z++) {
   for (int x = 0; x < size - 1; x++) {

    int a = z * size + x;
    int b = a + 1;
    int c = a + size;
    int d = c + 1;

    triangles[triangleIndex] = a;
    triangles[triangleIndex + 1] = b;
    triangles[triangleIndex + 2] = c;

    triangles[triangleIndex + 3] = c;
    triangles[triangleIndex + 4] = b;
    triangles[triangleIndex + 5] = d;

    triangleIndex += 6;
   }
  }

  Mesh mesh = new Mesh();
  mesh.vertices = vertices;
  mesh.triangles = triangles;

  mesh.RecalculateNormals();

  MeshFilter meshFilter = gameObject.GetComponent<MeshFilter>();
  if (!meshFilter) meshFilter = gameObject.AddComponent<MeshFilter>();

  MeshRenderer meshRenderer = gameObject.GetComponent<MeshRenderer>();
  if (!meshRenderer) meshRenderer = gameObject.AddComponent<MeshRenderer>();

  MeshCollider meshCollider = gameObject.GetComponent<MeshCollider>();
  if (!meshCollider) meshCollider = gameObject.AddComponent<MeshCollider>();

  meshFilter.mesh = mesh;
  meshRenderer.sharedMaterial = material;
  meshCollider.sharedMesh = mesh;
  meshCollider.sharedMaterial = physicMaterial;
 }

 void OnValidate()
 {
  CreateMesh();
 }
}

わざわざプレイボタンを押して実行しなくても結果が確認できるよう、メッシュ作成処理をひとつのメソッドにまとめてOnValidate()に配置しています。OnValidate()は、インスペクタ上で数値が変更されるごとに自動で呼び出されるメソッドです。size変数をあまり大きくしすぎると処理に時間がかかってしまうので、ほどほどに…

<次回> テクスチャ編

0 件のコメント:

コメントを投稿