動的に球体メッシュを作成したいというケースはそこそこあると思いますが、キューブに比べて構造がかなり複雑です。なにげなくプリミティブとして作成できてしまう形状ですが、実際にはどのようなアルゴリズムで生成されているのかを見ていきます。
Contents
今回作る球体のサンプル
今回作る球体のサンプルは、左の画像のようなものになります。Unityのプリミティブとは、作り方が違うので注意してください。とはいっても、頂点の置き方とポリゴンの貼り方さえ理解できれば、基本的には再現できると思います。
頂点を生成
まずは頂点を並べていくとわかりやすいので、頂点の並べかたを考えます。今回の並べ方は、y軸方向の階層を並べていくイメージです。頂点を円状に配置し終えたら、次段に移ってまた円状に配置する…..という工程をn段分繰り返します。
以下のサンプルは細分化を前提とした書き方になっているので、具体的な数字を入れてみて確認してみてください。各頂点の間隔はすべて三角関数によって導出しています。
[SerializeField] [Range(3, 360)] private int dividedInVertical = 6;
[SerializeField] [Range(3, 360)] private int dividedInHorizontal = 6;
[SerializeField] private float radius = 1;
List<Vector3> vertices = new List<Vector3>();
List<int> triangles = new List<int>();
float x;
float y;
float z;
for (int p = 1; p < dividedInVertical; p++)
{
y = Mathf.Cos(Mathf.Deg2Rad * p * 180f / dividedInVertical) * radius; //y軸を計算。単純な分割ではないので注意
var t = Mathf.Sin(Mathf.Deg2Rad * p * 180f / dividedInVertical) * radius; //0 < t < 1。
for (int q = 0; q < dividedInHorizontal; q++)
{
//tは半径に対してどれだけ縮小されているかのパラメータです
x = Mathf.Cos(Mathf.Deg2Rad * q * 360f / dividedInHorizontal) * t;
z = Mathf.Sin(Mathf.Deg2Rad * q * 360f / dividedInHorizontal) * t;
vertices.Add(new Vector3(x, y, z));
}
}
なお、天井と底の一点は例外として追加したほうが簡単になります。メッシュの構成においては、頂点の順番も重要になってきますので、わかりやすいようにリストの最初と最後に追加します。
///球のてっぺんは例外処理で追加
vertices.Add(new Vector3(0 , radius, 0));
------------略------------
///球の底は例外処理で追加
vertices.Add(new Vector3(0 , -radius, 0));
ポリゴンを貼る
ポリゴンの貼り方は以下のようなイメージになります。画像からもわかるように、天井のみポリゴンの貼り方が異なっているので、例外処理を挟むことになります。
天井
天井に貼られたポリゴンのみ、最初の1頂点をすべてのポリゴンが共有します。Indexの数え方はそれほど難しくはなく、
- [1, 3, 2]
- [1, 4, 3]
- [1, 5, 4]
という具合に規則的に加算していくので、for文でうまく回すことができます。なお、表面として描画されるには、頂点の組み合わせが時計回りになっている必要があるので注意してください。頂点リストの作り方によっては、当然[1, 2, 3]や[1, 3, 4]となることはありえます。
さて、最後の組みだけは例外となります。画像でいうと頂点②に戻ってくる組みがあるので、それだけは例外処理として[1, 2, n]という感じにセットしてあげる必要があります。
以上の点を踏まえた上で、この天井部分の書き方はこんな感じになります。
///てっぺんを含む三角形のみ例外。円形にポリゴンを敷き詰めていく
for (int i = 0; i < dividedInHorizontal; i++)
{
///最後の頂点の組み合わせのみ、例外
if (i == dividedInHorizontal - 1)
{
triangles.Add(0);
triangles.Add(1);
triangles.Add(i + 1);
break;
}
triangles.Add(0);
triangles.Add(i + 2);
triangles.Add(i + 1);
}
真ん中
球の真ん中部分は、二枚づつ貼っていくイメージがいいと思います。そして、一段貼っては次の一段を貼っていく….という感じです。
こちらも、一周してきた最後の組に例外が発生するので注意してください。
具体的なコードは、以下の通りです。縦の分割数×横の分割数でfor文を回す必要があるので、多少ややこしい感じになっています。
for (int p = 0; p < dividedInVertical - 2; p++) //縦に何層あるかによってforループの回数が変わります
{
//各段の最初となる頂点インデックスを計算
var firstIndexInLayer = p * dividedInHorizontal + 1;
for (int q = 0; q < dividedInHorizontal; q++) //段が何分割されているかによってforループの回数が変わります。
{
///一周してきた最後の組のみ、例外
if (q == dividedInHorizontal - 1)
{
triangles.Add(firstIndexInLayer + q);
triangles.Add(firstIndexInLayer);
triangles.Add(firstIndexInLayer + dividedInHorizontal);
triangles.Add(firstIndexInLayer + q);
triangles.Add(firstIndexInLayer + dividedInHorizontal);
triangles.Add(firstIndexInLayer + q + dividedInHorizontal);
break;
}
triangles.Add(firstIndexInLayer + q);
triangles.Add(firstIndexInLayer + q + 1);
triangles.Add(firstIndexInLayer + q + 1 + dividedInHorizontal);
triangles.Add(firstIndexInLayer + q);
triangles.Add(firstIndexInLayer + q + dividedInHorizontal + 1);
triangles.Add(firstIndexInLayer + q + dividedInHorizontal);
}
}
底
天井と底はぼぼ同じです。しかし、同じ貼り方をすると裏表が逆転してしまうので注意が必要です。
for (int i = 0; i < dividedInHorizontal; i++)
{
///一周した最後の組は例外
if (i == dividedInHorizontal - 1)
{
triangles.Add(vertices.Count - 1);
triangles.Add(vertices.Count - 1 - dividedInHorizontal + i);
triangles.Add(vertices.Count - 1 - dividedInHorizontal);
break;
}
triangles.Add(vertices.Count - 1);
triangles.Add(vertices.Count - 1 - dividedInHorizontal + i);
triangles.Add(vertices.Count - dividedInHorizontal + i);
}
サンプルコード全体
というわけで、コードをすべてまとめたものが以下になります。コピペしてそのまま使える形になっていますので、Unity上でぜひ確認してみてください。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
[ExecuteInEditMode]
public class SphereMeshGenerator : MonoBehaviour
{
[SerializeField] [Range(3, 360)] private int dividedInVertical = 6;
[SerializeField] [Range(3, 360)] private int dividedInHorizontal = 6;
[SerializeField] private float radius = 1;
private Mesh _mesh;
private MeshFilter _meshFilter;
private void Start()
{
_mesh = GetComponent<Mesh>();
_meshFilter = GetComponent<MeshFilter>();
CreateSphereMesh();
}
private void OnValidate()
{
CreateSphereMesh();
}
public void CreateSphereMesh()
{
List<Vector3> vertices = new List<Vector3>();
List<int> triangles = new List<int>();
float x;
float y;
float z;
#region 頂点を格納
///球のてっぺんは例外処理で追加
vertices.Add(new Vector3(0 , radius, 0));
for (int p = 1; p < dividedInVertical; p++)
{
y = Mathf.Cos(Mathf.Deg2Rad * p * 180f / dividedInVertical) * radius;
var t = Mathf.Sin(Mathf.Deg2Rad * p * 180f / dividedInVertical) * radius;
for (int q = 0; q < dividedInHorizontal; q++)
{
x = Mathf.Cos(Mathf.Deg2Rad * q * 360f / dividedInHorizontal) * t;
z = Mathf.Sin(Mathf.Deg2Rad * q * 360f / dividedInHorizontal) * t;
vertices.Add(new Vector3(x, y, z));
}
}
///球の底は例外処理で追加
vertices.Add(new Vector3(0 , -radius, 0));
#endregion
#region 頂点順序を格納
///てっぺんを含む三角形のみ例外。円環上にポリゴンを敷き詰めていく
for (int i = 0; i < dividedInHorizontal; i++)
{
///円環の最後のポリゴンのみ最初にもどるので例外
if (i == dividedInHorizontal - 1)
{
triangles.Add(0);
triangles.Add(1);
triangles.Add(i + 1);
break;
}
triangles.Add(0);
triangles.Add(i + 2);
triangles.Add(i + 1);
}
for (int p = 0; p < dividedInVertical - 2; p++)
{
var firstIndexInLayer = p * dividedInHorizontal + 1;
for (int q = 0; q < dividedInHorizontal; q++)
{
///円環の最後のみ例外
if(q == dividedInHorizontal - 1)
{
triangles.Add(firstIndexInLayer + q);
triangles.Add(firstIndexInLayer);
triangles.Add(firstIndexInLayer + dividedInHorizontal);
triangles.Add(firstIndexInLayer + q);
triangles.Add(firstIndexInLayer + dividedInHorizontal);
triangles.Add(firstIndexInLayer + q + dividedInHorizontal);
break;
}
triangles.Add(firstIndexInLayer + q);
triangles.Add(firstIndexInLayer + q + 1);
triangles.Add(firstIndexInLayer + q + 1 + dividedInHorizontal);
triangles.Add(firstIndexInLayer + q);
triangles.Add(firstIndexInLayer + q + dividedInHorizontal + 1);
triangles.Add(firstIndexInLayer + q + dividedInHorizontal);
}
}
///底を含む三角形のみ例外処理
for (int i = 0; i < dividedInHorizontal; i++)
{
///円環の最後のポリゴンのみ最初にもどるので例外
if (i == dividedInHorizontal - 1)
{
triangles.Add(vertices.Count - 1);
triangles.Add(vertices.Count - 1 - dividedInHorizontal + i);
triangles.Add(vertices.Count - 1 - dividedInHorizontal);
break;
}
triangles.Add(vertices.Count - 1);
triangles.Add(vertices.Count - 1 - dividedInHorizontal + i);
triangles.Add(vertices.Count - dividedInHorizontal + i);
}
#endregion
Mesh mesh = new Mesh(); // メッシュを作成
mesh.Clear(); // メッシュ初期化
mesh.SetVertices(vertices); // メッシュに頂点を登録する
mesh.SetTriangles(triangles, 0); // メッシュにインデックスリストを登録する
mesh.SetIndices(triangles, MeshTopology.Triangles, 0); //MeshTopologyを変更すればラインや点群といった表示もできる
mesh.RecalculateNormals(); // 法線の再計算
mesh.name = "SmoothedSphere";
_meshFilter.mesh = mesh;
}
}
まとめ
球体の動的な作成方法を解説してきましたが、頂点の作り方やポリゴンの貼り方はこれだけではありません。私は縦方向に段々になるように貼っていきましたが、これは横方向に貼っていっても問題ありません。自分なりのしっくりくる順番でコーディングしていくといいと思います。