ゲーム制作の過程でパスに沿ったオブジェクトを作る必要があったため記事にします。
少し掘り下げてメッシュ生成の基本となる記事も書いたのでよかった見てください。
パスに沿った頂点の位置
パスに沿ってメッシュを生成する場合、最初に課題となるのが 「頂点をどのようにとるか」 になります。
図のように白い球がパスの頂点だとして、灰色の球の位置に頂点を置くことが理想です。
一番簡単な考え方としてはパスの頂点を中心に左右にオフセットした位置を頂点に置くことです。
Vector3 lowerLeft = position + Vector3.left;
Vector3 lowerRight = position + Vector3.right;
ただし、この場合は真っすぐ進む分には問題ありませんが、90度以上曲がった場合破綻してしまいます。
つまり、回転を考慮してあげなくてはいけません。
2点間の向きはQuaternion.LookRotation
を使って求めることができます。
Vector3 direction = (secondPos - firstPos).normalized;
Quaternion rot = Quaternion.LookRotation(direction);
Vector3 lowerLeft = positions[i] + rot * Vector3.left;
Vector3 lowerRight = positions[i] + rot * Vector3.right;
こうすることで白い球がどのような位置にあっても図の灰色の球のように頂点をとることができました。
GeneratePathCube.cs
using System.Collections.Generic;
using UnityEngine;
public class GeneratePathCube : MonoBehaviour
{
class Square
{
public Vector3 lowerLeft;
public Vector3 upperLeft;
public Vector3 lowerRight;
public Vector3 upperRight;
}
[SerializeField] Transform[] PathPosition;
[SerializeField] Material Material;
[SerializeField] float Width = 1f;
[SerializeField] float Height = 2f;
[SerializeField] bool IsDrawGizmo = false;
private Square[] PathSquares()
{
int length = PathPosition.Length;
var positions = new Vector3[length];
var squares = new Square[length];
for( int i = 0; i < length; i++)
{
positions[i] = PathPosition[i].position;
}
for (int i = 0; i < length; i++)
{
Vector3 firstPos, secondPos;
if (i == 0)
{
firstPos = positions[0];
secondPos = positions[1];
}
else
{
firstPos = positions[i - 1];
secondPos = positions[i];
}
Vector3 direction = (secondPos - firstPos).normalized;
Quaternion rot = Quaternion.LookRotation(direction);
Vector3 lowerLeft = positions[i] + rot * Vector3.left * Width;
Vector3 lowerRight = positions[i] + rot * Vector3.right * Width;
var square = new Square()
{
lowerLeft = lowerLeft,
lowerRight = lowerRight,
upperLeft = lowerLeft + Vector3.up * Height,
upperRight = lowerRight + Vector3.up * Height,
};
squares[i] = square;
}
return squares;
}
private void OnDrawGizmos()
{
if((PathPosition == null) || (PathPosition.Length < 2) || !IsDrawGizmo)
{
return;
}
var squares = PathSquares();
for (int i = 0; i < squares.Length; i++)
{
Gizmos.DrawSphere(squares[i].lowerLeft, 0.2f);
Gizmos.DrawSphere(squares[i].lowerRight, 0.2f);
Gizmos.DrawSphere(squares[i].upperLeft, 0.2f);
Gizmos.DrawSphere(squares[i].upperRight, 0.2f);
}
}
パスに沿った頂点
キューブを作る考え方とあまり変わりませんが、正面と背面以外は今の頂点とひとつ前の頂点を結んだ面を作るだけです。
for (int i = 0; i < squares.Length; i++)
{
int currrentLowerLeft = (i * 4) + 0;
int currrentLowerRight = (i * 4) + 1;
int currrentUpperLeft = (i * 4) + 2;
int currrentUpperRight = (i * 4) + 3;
if (i == 0)
{
// 正面
triangles.AddRange(SquareToTriangles(currrentLowerLeft, currrentLowerRight, currrentUpperLeft, currrentUpperRight));
}
else
{
int lastLowerLeft = ((i - 1) * 4) + 0;
int lastLowerRight = ((i - 1) * 4) + 1;
int lastUpperLeft = ((i - 1) * 4) + 2;
int lastUpperRight = ((i - 1) * 4) + 3;
// 左側面
triangles.AddRange(SquareToTriangles(currrentLowerLeft, lastLowerLeft, currrentUpperLeft, lastUpperLeft));
// 右側面
triangles.AddRange(SquareToTriangles(lastLowerRight, currrentLowerRight, lastUpperRight, currrentUpperRight));
// 上側面
triangles.AddRange(SquareToTriangles(lastUpperLeft, lastUpperRight, currrentUpperLeft, currrentUpperRight));
// 下側面
triangles.AddRange(SquareToTriangles(lastLowerRight, lastLowerLeft, currrentLowerRight, currrentLowerLeft));
if (i == squares.Length - 1)
{
// 背面
triangles.AddRange(SquareToTriangles(currrentLowerRight, currrentLowerLeft, currrentUpperRight, currrentUpperLeft));
}
}
}
トライアングルとメッシュ生成の全体像は以下になります。
GeneratePathCube.cs
void Start()
{
GenerateMesh();
}
public void GenerateMesh()
{
var squares = PathSquares();
var mesh = new Mesh();
var vertices = new List<Vector3>();
var triangles = new List<int>();
var uvs = new List<Vector2>();
for (int i = 0; i < squares.Length; i++)
{
vertices.Add(squares[i].lowerLeft);
vertices.Add(squares[i].lowerRight);
vertices.Add(squares[i].upperLeft);
vertices.Add(squares[i].upperRight);
uvs.Add(new Vector2(0, 0));
uvs.Add(new Vector2(1, 0));
uvs.Add(new Vector2(0, 1));
uvs.Add(new Vector2(1, 1));
int currrentLowerLeft = (i * 4) + 0;
int currrentLowerRight = (i * 4) + 1;
int currrentUpperLeft = (i * 4) + 2;
int currrentUpperRight = (i * 4) + 3;
if (i == 0)
{
// 正面
triangles.AddRange(SquareToTriangles(currrentLowerLeft, currrentLowerRight, currrentUpperLeft, currrentUpperRight));
}
else
{
int lastLowerLeft = ((i - 1) * 4) + 0;
int lastLowerRight = ((i - 1) * 4) + 1;
int lastUpperLeft = ((i - 1) * 4) + 2;
int lastUpperRight = ((i - 1) * 4) + 3;
// 左側面
triangles.AddRange(SquareToTriangles(currrentLowerLeft, lastLowerLeft, currrentUpperLeft, lastUpperLeft));
// 右側面
triangles.AddRange(SquareToTriangles(lastLowerRight, currrentLowerRight, lastUpperRight, currrentUpperRight));
// 上側面
triangles.AddRange(SquareToTriangles(lastUpperLeft, lastUpperRight, currrentUpperLeft, currrentUpperRight));
// 下側面
triangles.AddRange(SquareToTriangles(lastLowerRight, lastLowerLeft, currrentLowerRight, currrentLowerLeft));
if (i == squares.Length - 1)
{
// 背面
triangles.AddRange(SquareToTriangles(currrentLowerRight, currrentLowerLeft, currrentUpperRight, currrentUpperLeft));
}
}
}
mesh.SetVertices(vertices);
mesh.SetTriangles(triangles, 0);
mesh.SetUVs(0, uvs);
mesh.RecalculateNormals();
var obj = new GameObject();
obj.name = "GenerateMesh";
obj.transform.parent = transform;
var meshFilter = obj.AddComponent<MeshFilter>();
var meshRenderer = obj.AddComponent<MeshRenderer>();
var meshCollider = obj.AddComponent<MeshCollider>();
meshFilter.mesh = mesh;
meshCollider.sharedMesh = mesh;
meshRenderer.material = Material;
}
private List<int> SquareToTriangles(int lowerLeft, int lowerRight, int upperLeft, int upperRight)
{
var triangles = new List<int>();
triangles.Add(lowerLeft); // 0
triangles.Add(upperLeft); // 2
triangles.Add(lowerRight); // 1
triangles.Add(upperLeft); // 2
triangles.Add(upperRight); // 3
triangles.Add(lowerRight); // 1
return triangles;
}