Skip to main content

3Dモデルの仕組み(その1)

 今まで、FBXファイルでモデルをダウンロードし、そのまま使ってきましたが、実際にどう作られているのか調べてみましょう。

頂点(Vertex / バーテックス)

 頂点は3D空間の「ある位置」を表している。普通、3次元ベクトルとして表現する。この位置を適切に繋がることにより、3Dの形状を作成できる。例えば、普通の立方体は頂点は8点がある:

image.png

頂点の配列

 

0 (-1, -1, -1)
1 ( 1, -1, -1)
2 ( 1, 1, -1)
3 (-1, 1, -1)
4 (-1, -1, 1)
5 ( 1, -1, 1)
6 ( 1, 1, 1)
7 (-1, 1, 1)

 

 Unityの場合は「Vertex3」型の配列で頂点を作成ができる。それぞれの頂点に「指数」(Index)を割り当てる必要がある。上の図だと、「0〜7」の指数をアサインした。ただし、必要に応じて、同じ頂点を重複する必要な場合もある。このときは、同じ位置に複数の頂点があり、それぞれが別々の指数を持っている。

image.png

頂点の配列

 

0 8 16 (-1, -1, -1)
1 9 17 ( 1, -1, -1)
2 10 18 ( 1, 1, -1)
3 11 19 (-1, 1, -1)
4 12 20 (-1, -1, 1)
5 13 21 ( 1, -1, 1)
6 14 22 ( 1, 1, 1)
7 15 23 (-1, 1, 1)

 ここでは、各面は自分の頂点を持ち、隣の面と共有しない。つまり、この場合は6面 × 4点=24点である。

練習

MyCube スクリプトを作成して…

  • Vector3型の配列「vertices」を宣言
  • CreateVertices() メソッドを作成し、上記の頂点の配列を作成
  • Awake() メソッドの中に CreateVertices() を呼び出す          ※ AwakeStart よりも一歩早く走る
MyCube スクリプト
// 立方体
public class MyCube : MonoBehaviour
{
	// 頂点の配列
	private Vector3[] vertices;
	
	// Awake は Start よりも一歩早く実行する
	// 内部初期化(他のコンポーネントに依存しない)ときにはおすすめ
	private void Awake()
	{
		CreateVertices();
	}
	
	// 頂点を作成する
	private void CreateVertices()
	{
		vertices = new Vector3[24];

		vertices[0] = new Vector3(-1, -1, -1);
		vertices[1] = new Vector3(1, -1, -1);
		vertices[2] = new Vector3(1, 1, -1);
		vertices[3] = new Vector3(-1, 1, -1);
		vertices[4] = new Vector3(-1, -1, 1);
		vertices[5] = new Vector3(1, -1, 1);
		vertices[6] = new Vector3(1, 1, 1);
		vertices[7] = new Vector3(-1, 1, 1);

		vertices[8] = new Vector3(-1, -1, -1);
		vertices[9] = new Vector3(1, -1, -1);
		vertices[10] = new Vector3(1, 1, -1);
		vertices[11] = new Vector3(-1, 1, -1);
		vertices[12] = new Vector3(-1, -1, 1);
		vertices[13] = new Vector3(1, -1, 1);
		vertices[14] = new Vector3(1, 1, 1);
		vertices[15] = new Vector3(-1, 1, 1);

		vertices[16] = new Vector3(-1, -1, -1);
		vertices[17] = new Vector3(1, -1, -1);
		vertices[18] = new Vector3(1, 1, -1);
		vertices[19] = new Vector3(-1, 1, -1);
		vertices[20] = new Vector3(-1, -1, 1);
		vertices[21] = new Vector3(1, -1, 1);
		vertices[22] = new Vector3(1, 1, 1);
		vertices[23] = new Vector3(-1, 1, 1);
	}
}

三角形(Triangle)

 頂点3点を繋がると三角形(Triangle)を作ることができ、三角形の群は「メッシュ」(Mesh)という。

image.png

 三角形を作成するときに、次の点を考慮しないといけない:

① 頂点の順番(三角形は時計回りか?逆時計回りか?)

 3つの頂点の繋がり方は2つある:時計回り(Clockwise - CW)と反時計回り(Counter - Clockwise - CCW)。ゲーム開発では、この順番はとても重要である。これは「Winding Order」(巻き順)という。

 ゲームエンジンの最適化と高速化のため、無駄な面(三角形)を計算処理から抜いた方が良いでしょう。普通、カメラに映らない面(カメラの後ろにあるもの)を計算から抜く。また、カメラに向いていない面(三角形の裏側)も、計算から抜くことが一般である。

 三角形の「表」と「裏」が分かるには、頂点の順番を使う。Unityの場合は「時計回りの三角形は表面である」となっているが、別のエンジンは逆になる可能性があるので、注意してください。

image.png

② 頂点を共有するか?別々にするのか?

 2枚の三角形は1つの辺が接着しているときに、2つの選択しがある:

  • 同じ頂点を使いまわし、共有する
  • 頂点を重複し、面を作成するとき、別々の頂点を使う

 とくにどれにするかは別にルールがなく、ゲームでやりたいことにより、適切な繋がり方を選ぶべきが、一般的には:

  • なめらかなカーブを表現しようとし、同じ面であるとき→頂点を共有する(連続する面)
    • 例:キャラクター、服装、植物、動物
  • 硬い角を持っているオブジェ、別の面であるとき→別々の頂点にする(連続しない面)
    • 例:壁、さいころ、ブロック、かくかくしかじかの形

ならば、普通の立方体の繋がりは、以下のようになる:

image.png
各面が自分の頂点を持っているが、同じ面の三角形は頂点を共有する

練習

MyCube スクリプトを拡張して…

  • triangles の 整数型の配列を宣言
    • 長さは「三角形の数 × 3」= 6 × 2 × 3 = 36
  • CreateTriangles() のメソッドを作成し、図のように3つの指数ごとに三角形を作成
    • 例:
      triangles[0] = 0;
      triangles[1] = 3;
      triangles[2] = 1;
  • Awake()CreateTriangles() を呼び出す
MyCube スクリプト(編集分)

// 三角形の配列
private int[] triangles;
	
// Awake は Start よりも一歩速く実行する
// 内部初期化(他のコンポーネントに依存しない)ときにはおすすめ
private void Awake()
{
	CreateVertices();
	CreateTriangles();
}

// 三角形を作成する
private void CreateTriangles()
{
	triangles = new int[36];
	int i = 0;

	// 前
	triangles[i++] = 0;
	triangles[i++] = 3;
	triangles[i++] = 1;

	triangles[i++] = 1;
	triangles[i++] = 3;
	triangles[i++] = 2;

	// 右
	triangles[i++] = 9;
	triangles[i++] = 10;
	triangles[i++] = 6;

	triangles[i++] = 9;
	triangles[i++] = 6;
	triangles[i++] = 5;

	// 下
	triangles[i++] = 20;
	triangles[i++] = 16;
	triangles[i++] = 17;

	triangles[i++] = 20;
	triangles[i++] = 17;
	triangles[i++] = 21;

	// 左
	triangles[i++] = 7;
	triangles[i++] = 8;
	triangles[i++] = 4;

	triangles[i++] = 7;
	triangles[i++] = 19;
	triangles[i++] = 8;

	// 後ろ
	triangles[i++] = 14;
	triangles[i++] = 12;
	triangles[i++] = 13;

	triangles[i++] = 14;
	triangles[i++] = 15;
	triangles[i++] = 12;

	// 上
	triangles[i++] = 18;
	triangles[i++] = 11;
	triangles[i++] = 23;

	triangles[i++] = 18;
	triangles[i++] = 23;
	triangles[i++] = 22;
}

メッシュ(Mesh)

 メッシュは三角形の群である。メッシュはあくまで「形状」を定義し、色や模様は別途で作成しなければならない(シェーダーのときにまた学ぼう!)

 Unityでは「Mesh」というクラスがある。この「Mesh」クラスは、頂点と三角形を定義するクラスである。また、光の計算のための「法線ベクトル」や模様を貼り付けるための「UV座標」も持っている(これもシェーダーの時に学ぼう)

 メッシュができたら、「MeshFilter」コンポーネントに渡し、「MeshRenderer」で描画する。

image.png

練習

「MyCube」クラスを作成し

  • Meshを作成
    • verticestriangles を利用し、立方体メッシュを作成
  • マテリアルをInspectorで設定
  • Awakeで 実行中に MeshFilter と MeshRenderer を追加し、描画する。
MyCube スクリプト(完成)
using UnityEngine;

// 立方体のメッシュを作成し、描画する
public class MyCube : MonoBehaviour
{
	// 立方体のマテリアル
	[SerializeField] 
	private Material material;

	// 頂点の配列
	private Vector3[] vertices;
	
	// 三角形の配列
	private int[] triangles;
	
	// 作成したメッシュ
	private Mesh mesh;
	
	// Awake は Start よりも一歩速く実行する
	// 内部初期化(他のコンポーネントに依存しない)ときにはおすすめ
	private void Awake()
	{
		CreateVertices();
		CreateTriangles();

		mesh = new Mesh();
		mesh.vertices = vertices;
		mesh.triangles = triangles;
		mesh.RecalculateNormals(); // 法線ベクトルを自動計算(※)
		
		// ゲームオブジェクトにMeshFilterを追加
		MeshFilter mf = gameObject.AddComponent<MeshFilter>();
		mf.mesh = mesh;
		
		// MeshRendererも追加
		MeshRenderer mr = gameObject.AddComponent<MeshRenderer>();
		mr.material = material;
	}

	// 頂点を作成する
	private void CreateVertices()
	{
		vertices = new Vector3[24];

		vertices[0] = new Vector3(-1, -1, -1);
		vertices[1] = new Vector3(1, -1, -1);
		vertices[2] = new Vector3(1, 1, -1);
		vertices[3] = new Vector3(-1, 1, -1);
		vertices[4] = new Vector3(-1, -1, 1);
		vertices[5] = new Vector3(1, -1, 1);
		vertices[6] = new Vector3(1, 1, 1);
		vertices[7] = new Vector3(-1, 1, 1);

		vertices[8] = new Vector3(-1, -1, -1);
		vertices[9] = new Vector3(1, -1, -1);
		vertices[10] = new Vector3(1, 1, -1);
		vertices[11] = new Vector3(-1, 1, -1);
		vertices[12] = new Vector3(-1, -1, 1);
		vertices[13] = new Vector3(1, -1, 1);
		vertices[14] = new Vector3(1, 1, 1);
		vertices[15] = new Vector3(-1, 1, 1);

		vertices[16] = new Vector3(-1, -1, -1);
		vertices[17] = new Vector3(1, -1, -1);
		vertices[18] = new Vector3(1, 1, -1);
		vertices[19] = new Vector3(-1, 1, -1);
		vertices[20] = new Vector3(-1, -1, 1);
		vertices[21] = new Vector3(1, -1, 1);
		vertices[22] = new Vector3(1, 1, 1);
		vertices[23] = new Vector3(-1, 1, 1);
	}

	// 三角形を作成する
	private void CreateTriangles()
	{
		triangles = new int[36];
		int i = 0;

		// 前
		triangles[i++] = 0;
		triangles[i++] = 3;
		triangles[i++] = 1;

		triangles[i++] = 1;
		triangles[i++] = 3;
		triangles[i++] = 2;

		// 右
		triangles[i++] = 9;
		triangles[i++] = 10;
		triangles[i++] = 6;

		triangles[i++] = 9;
		triangles[i++] = 6;
		triangles[i++] = 5;

		// 下
		triangles[i++] = 20;
		triangles[i++] = 16;
		triangles[i++] = 17;

		triangles[i++] = 20;
		triangles[i++] = 17;
		triangles[i++] = 21;

		// 左
		triangles[i++] = 7;
		triangles[i++] = 8;
		triangles[i++] = 4;

		triangles[i++] = 7;
		triangles[i++] = 19;
		triangles[i++] = 8;

		// 後ろ
		triangles[i++] = 14;
		triangles[i++] = 12;
		triangles[i++] = 13;

		triangles[i++] = 14;
		triangles[i++] = 15;
		triangles[i++] = 12;

		// 上
		triangles[i++] = 18;
		triangles[i++] = 11;
		triangles[i++] = 23;

		triangles[i++] = 18;
		triangles[i++] = 23;
		triangles[i++] = 22;
	}
}