2017年4月18日

Unity 動的にメッシュを作成する ~地形を作るぞ編~


スクリプトからメッシュを作成する方法について、理解を深めるための解説その2。今回は当たり判定のある地形っぽいものを…


※このページの内容の動作確認にはUnity5.3を使用しています。
前回は頂点数が4つの超単純なメッシュを作成しました。今回はそこに少しだけ手を加えて、頂点数や大きさを自由に設定できる四角形を作っていきたいと思います。てなわけで、とりあえずどんな感じのコードになるかを先に見てみましょう。

using UnityEngine;
using System.Collections;

public class PlainMesh : MonoBehaviour {

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

 void Start()
 {
  Vector3[] vertices = new Vector3[size * size];
  for (int z = 0; z < size; z++) {
   for (int x = 0; x < size; x++) {
    vertices[z * size + x] = new Vector3(x * vertexDistance, 0, -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;
 }
}

なげえ!だらだらと書いていますが、頂点の位置情報(vertices)とどの頂点で三角形を作るかという情報(triangles)を配列として用意しそれをメッシュに渡す、という全体的な流れは前回と全く同じです。

変数の追加

[Range(1,255)]
public int size;
public float vertexDistance = 1f;

まずは、size変数を宣言し1辺の頂点数を指定できるようにしました。たとえばsize=4にすれば、全体の頂点数は4×4で16個になります。また、Range属性(アトリビュート)をつけることで、インスペクタ上で1から255までの範囲しか入力できないようにしています。というのは、0以下にすると配列のサイズがおかしくなってしまい、さらに256以上にすると1つのメッシュが持てる頂点数の限界(65534個)を超えてしまうためです。ほんとはきちんと内部で例外を弾く処理をしたほうがいい気がするけど、とりあえず今はこれで…。あとは、頂点間の距離を指定できるようにvertexDistanceも用意しました。

さて、次は頂点の位置です。

頂点の位置情報


図のように、原点を0として順に配列に入れていくことにします。地形を想定しているため、XY平面ではなくXZ平面上に配置していくというのがポイントです。この図の場合、頂点を入れる配列の長さは4×4で16必要です。頂点どうしの間隔(vertexDistance)が1のとき、vertices[1]の座標は(1,0,0)、vertices[2]の座標は(2,0,0)になりますね。最後のvertices[15]の座標は(3,0,-3)になるはずです。これを式にしたのが次の部分です。

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

ループ処理でvertices[0], vertices[1], vertices[2]…と順に位置情報を計算して配列に格納しています。自分はvertices[z * size + x]とか見ると一瞬「うっ」となります。すらすら読めるようになりたい!

三角形情報

int[] triangles = new int[(size-1) * (size-1) * 6];

22行目で配列を作成する際、すべての三角形の頂点数の合計を求めています(size*sizeではありません!たとえばvertices[1]だけをみても、三角形0-1-4、三角形4-1-5、三角形1-2-5で3回使われています)。さっきの図のようにsize=4のとき、四角形の数は3×3の9個です。四角形の中に三角形は2つ入っているので三角形の数は9×2で18個。三角形1つにつき頂点は3つなので全体の頂点数は18×3で54個。つまり頂点数を求めるには(size-1)*(size-1)*2*3を計算すればいいわけです。

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;

25-28行目のa,b,c,dについては次の図を見てください。


ある頂点aの右下に四角形がある場合を考えます(たとえば0とか1とか4とか10の場合です)。頂点aが何番目かはz * size + xで求められます。このとき、bはaの右の頂点なのでa+1、cはaの次の行なのでa+size、dはcの右なのでc+1というように表現できます。そして、この四角形には三角形a-b-cと三角形c-b-dという2つの三角形があるので、それぞれの頂点番号を順に配列に格納している、というわけです。右端と下端の頂点(たとえば3とか11とか14とか…)は右下に四角形がないので無視します。forループの条件式がz<size-1やx<size-1になっているのはこのためです。

すんごくややこしいですが、ともかくこれでメッシュは作成できます。あとは前回同様、法線を計算して、MeshFilterとMeshRendererを追加して、マテリアルを適用するだけです。

衝突判定の追加

また、今回はさらに衝突判定(コリジョン)をつけるため、MeshColliderコンポーネントも追加しています。これでメッシュの形に合わせて衝突判定が発生するはずです。

MeshCollider meshCollider = gameObject.GetComponent<MeshCollider>();
if (!meshCollider) meshCollider = gameObject.AddComponent<MeshCollider>();
public PhysicMaterial physicMaterial;
meshCollider.sharedMesh = mesh;
meshCollider.sharedMaterial = physicMaterial;

meshCollider.sharedMeshにはさっき作成したメッシュを設定し、meshCollider.sharedMaterialにはインスペクタ上で指定した物理用のマテリアルphysicMaterialを渡しています。

というわけでついに完成!ドン!


シーンを実行すると何もないところにメッシュが作成されて、ちゃんとボールを受け止めています。

高さの追加

一応これで目的は果たしたのですが、これだと前回のシンプル四角形でもいいじゃん、ってことになりそうなので最初のコードにさらに追加の処理をしてみます。

public float heightMultiplier = 1f;
float y = Random.value * heightMultiplier;
vertices[z * size + x] = new Vector3(x * vertexDistance, y, -z * vertexDistance);

これまですべての頂点のY座標は0でしたが、そこに高さを加えます。まずRandom.valueで0~1の間の数をランダムに取得し、次にheightMultiplierでその高さを変更。これでどうなるかというと…


メッシュがでこぼこになりました。頂点の高さはランダムなので、実行するたびに地形が変わります。なんだかゲームっぽい感じがしてきたぞぉ~!というところで今回はおしまい。

<次回> パーリンノイズ編

0 件のコメント:

コメントを投稿