パスに沿ったメッシュ生成を応用してパスの部分をドローイングに置き換えました。
記事のスクリプトを応用した場合、不具合があったためその解決方法を含めて解説していきます。
マウスイベントからパスを生成する
Input.GetMouseButton
でマウスのクリックやホールド状態を取得します。
Input.mousePosition
で取得したマウスの座標をCamera.main.ScreenPointToRay
でRay
に変換し、ワールド座標を取得します。
Input.GetMouseButtonDown(0)
を使うときの注意としてInput.GetMouseButtonUp(0)
のイベントが来る前にInput.GetMouseButtonDown(0)
が2度続くことがあったのでbool
型のフラグ管理をしてDownイベントを2重に通さない工夫をしておくと安全かもしれません。
パス用の座標を格納するときはVector3.Distance
を使って現在のマウスポイントと1つ前の位置からの移動距離で新しい座標を格納するよにしました。
[SerializeField] LayerMask Mask;
[SerializeField] float DistanceInterval = 0.3f;
bool isDrawing = false;
List<Vector3> pathPositions = new List<Vector3>();
private void Update()
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit, Mathf.Infinity, Mask))
{
if (Input.GetMouseButtonDown(0) && !isDrawing)
{
isDrawing = true;
CreateMeshObject();
pathPositions.Clear();
pathPositions.Add(hit.point);
}
else if (Input.GetMouseButton(0) && isDrawing)
{
var lastPos = pathPositions[pathPositions.Count - 1];
var currentPos = hit.point;
if (Vector3.Distance(lastPos, currentPos) >= DistanceInterval)
{
pathPositions.Add(hit.point);
UpdateMeshObject();
}
}
}
else
{
DrawEnd();
}
if (Input.GetMouseButtonUp(0))
{
DrawEnd();
}
}
図のようにBox Collider
の配置とLayerMaskを指定してドローイングの範囲とメッシュの生成位置をコントロールすると楽です。
Physics.Raycast
でヒットしたRaycastHit.point
がBox Collider
の表面を取得出来るので位置調整をTransformをいじるだけで調整出来ます。
座標の向きの反転問題
以下のコードのようにQuaternion.LookRotation
を使ってパスに沿った座標を計算していましたが、所々で座標が反転してしまいました。
Vector3 direction = (secondPos - firstPos).normalized;
Quaternion rot = Quaternion.LookRotation(direction);
var square = new Square()
{
lowerLeft = positions[i] + rot * new Vector3(-1f * Width, 0f, 0f),
lowerRight = positions[i] + rot * new Vector3(1f * Width, 0f, 0f),
upperLeft = positions[i] + rot * new Vector3(-1f * Width, 1f * Height, 0f),
upperRight = positions[i] + rot * new Vector3(1f * Width, 1f * Height, 0f),
};
Gizmoで可視化してみるとどうやら一つ前のX座標と動かしているX座標の一致したときにおかしくなっていることが分かりました。
試行錯誤したところQuaternion.LookRotation
を使ったやり方では解決方法が見いだせなかったので別の方法をとります。
(X座標が一致した場合は1つ前と同じ回転のオフセットをかけることで解決したかもしれません)
座標の向きの求め方を変更
Vector3 direction = (secondPos - firstPos).normalized;
Vector3 lowerLeft = positions[i] + new Vector3(-direction.y, direction.x, 0f) * Width;
Vector3 lowerRight = positions[i] + new Vector3(direction.y, -direction.x, 0f) * Width;
var square = new Square()
{
lowerLeft = lowerLeft,
lowerRight = lowerRight,
upperLeft = lowerLeft + Vector3.back * Height,
upperRight = lowerRight + Vector3.back * Height,
};
Vector3.normalized
で求めた向きを使って、direction.x
をY座標にdirection.y
をX座標にオフセットしてすることにしました。(Z座標はドローイングの場合不要なので省いています)
こうすることで回転させずに向きを制御することができました。
頂点2,3のようにY座標が一致した場合は向きが(0,1)になり、direction.y
がX方向にオフセットされます。
頂点3,4ではX座標が一致し、向きが(1,0)となり、direction.x
がY方向にオフセットされます。
メッシュの影がおかしい
頂点の問題は解決しましたが影の表示がおかしくて丸みを帯びたチューブ状のよに見えます。
実際はキューブ状なので平になってるはずですが、影の付き方によっておかしく見えてしまっています。
今回作ったメッシュはキューブでいうと頂点が8で構成されています。
Unityのテンプレキューブの頂点を確認すると頂点が24あります。
つまり、単純に頂点の数が足りないため法線ベクトルの計算がうまく出来ていないことが分かりました。
8点で結ぶと頂点を使いまわしますが、そうすると1面の法線の向きは正しくても使いまわした法線の向きはかしな方向に向いていることになります。
頂点を増やして法線の向きを整える
やることは単純で頂点の数を倍にしてトライアングルを結ぶときに同じ頂点を使わないようにするだけです。
private Square[] PathSquares24()
{
int length = pathPositions.Count;
var positions = pathPositions;
var squares = new Square[length * 2 + 2];
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;
Vector3 lowerLeft = positions[i] + new Vector3(-direction.y, direction.x, 0f) * Width;
Vector3 lowerRight = positions[i] + new Vector3(direction.y, -direction.x, 0f) * Width;
var square = new Square()
{
lowerLeft = lowerLeft,
lowerRight = lowerRight,
upperLeft = lowerLeft + Vector3.back * Height,
upperRight = lowerRight + Vector3.back * Height,
};
if (i == 0)
{
squares[0] = square;
}
else if (i == length - 1)
{
squares[squares.Length - 1] = square;
}
for (int j = 0; j < 2; j++)
{
squares[i * 2 + 1 + j] = square;
}
}
return squares;
}
private Mesh GenerateMesh24()
{
var squares = PathSquares24();
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 if (i == squares.Length - 1)
{
// 背面
triangles.AddRange(SquareToTriangles(currrentLowerRight, currrentLowerLeft, currrentUpperRight, currrentUpperLeft));
}
else if(i > 2)
{
int lastLowerLeft = ((i - 2) * 4) + 0;
int lastLowerRight = ((i - 2) * 4) + 1;
int lastUpperLeft = ((i - 2) * 4) + 2;
int lastUpperRight = ((i - 2) * 4) + 3;
if (i % 2 == 1)
{
// 左側面
triangles.AddRange(SquareToTriangles(currrentLowerLeft, lastLowerLeft, currrentUpperLeft, lastUpperLeft));
// 右側面
triangles.AddRange(SquareToTriangles(lastLowerRight, currrentLowerRight, lastUpperRight, currrentUpperRight));
}
else
{
// 上側面
triangles.AddRange(SquareToTriangles(lastUpperLeft, lastUpperRight, currrentUpperLeft, currrentUpperRight));
// 下側面
triangles.AddRange(SquareToTriangles(lastLowerRight, lastLowerLeft, currrentLowerRight, currrentLowerLeft));
}
}
}
mesh.SetVertices(vertices);
mesh.SetTriangles(triangles, 0);
mesh.SetUVs(0, uvs);
mesh.RecalculateNormals();
return mesh;
}
全体のスクリプト
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DrawGenerateMesh : MonoBehaviour
{
class Square
{
public Vector3 lowerLeft;
public Vector3 upperLeft;
public Vector3 lowerRight;
public Vector3 upperRight;
}
[SerializeField] Material Material;
[SerializeField] float Width = 1f;
[SerializeField] float Height = 2f;
[SerializeField] LayerMask Mask;
[SerializeField] float DistanceInterval = 0.3f;
[SerializeField] Transform[] PathPosition;
[SerializeField] bool IsDrawGizmo = false;
GameObject currentObject;
bool isDrawing = false;
List<Vector3> pathPositions = new List<Vector3>();
// Start is called before the first frame update
void Start()
{
}
private void Update()
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit, Mathf.Infinity, Mask))
{
if (Input.GetMouseButtonDown(0) && !isDrawing)
{
isDrawing = true;
CreateMeshObject();
pathPositions.Clear();
pathPositions.Add(hit.point);
}
else if (Input.GetMouseButton(0) && isDrawing)
{
var lastPos = pathPositions[pathPositions.Count - 1];
var currentPos = hit.point;
if (Vector3.Distance(lastPos, currentPos) >= DistanceInterval)
{
pathPositions.Add(hit.point);
UpdateMeshObject();
}
}
}
else
{
DrawEnd();
}
if (Input.GetMouseButtonUp(0))
{
DrawEnd();
}
}
private void CreateMeshObject()
{
currentObject = new GameObject();
currentObject.transform.parent = transform;
currentObject.AddComponent<MeshFilter>();
currentObject.AddComponent<MeshCollider>();
var meshRen = currentObject.AddComponent<MeshRenderer>();
meshRen.material = Material;
}
private void UpdateMeshObject()
{
var mesh = GenerateMesh24();
currentObject.GetComponent<MeshFilter>().mesh = mesh;
currentObject.GetComponent<MeshCollider>().sharedMesh = mesh;
}
private void DrawEnd()
{
if (isDrawing)
{
isDrawing = false;
if(pathPositions.Count < 2)
{
Destroy(currentObject);
}
}
}
private Square[] PathSquares()
{
int length = pathPositions.Count;
var positions = pathPositions;
var squares = new Square[length];
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;
Vector3 lowerLeft = positions[i] + new Vector3(-direction.y, direction.x, 0f) * Width;
Vector3 lowerRight = positions[i] + new Vector3(direction.y, -direction.x, 0f) * Width;
var square = new Square()
{
lowerLeft = lowerLeft,
lowerRight = lowerRight,
upperLeft = lowerLeft + Vector3.back * Height,
upperRight = lowerRight + Vector3.back * Height,
};
squares[i] = square;
}
return squares;
}
private Mesh 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();
return mesh;
}
private Square[] PathSquares24()
{
int length = pathPositions.Count;
var positions = pathPositions;
var squares = new Square[length * 2 + 2];
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;
Vector3 lowerLeft = positions[i] + new Vector3(-direction.y, direction.x, 0f) * Width;
Vector3 lowerRight = positions[i] + new Vector3(direction.y, -direction.x, 0f) * Width;
var square = new Square()
{
lowerLeft = lowerLeft,
lowerRight = lowerRight,
upperLeft = lowerLeft + Vector3.back * Height,
upperRight = lowerRight + Vector3.back * Height,
};
if (i == 0)
{
squares[0] = square;
}
else if (i == length - 1)
{
squares[squares.Length - 1] = square;
}
for (int j = 0; j < 2; j++)
{
squares[i * 2 + 1 + j] = square;
}
}
return squares;
}
private Mesh GenerateMesh24()
{
var squares = PathSquares24();
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 if (i == squares.Length - 1)
{
// 背面
triangles.AddRange(SquareToTriangles(currrentLowerRight, currrentLowerLeft, currrentUpperRight, currrentUpperLeft));
}
else if(i > 2)
{
int lastLowerLeft = ((i - 2) * 4) + 0;
int lastLowerRight = ((i - 2) * 4) + 1;
int lastUpperLeft = ((i - 2) * 4) + 2;
int lastUpperRight = ((i - 2) * 4) + 3;
if (i % 2 == 1)
{
// 左側面
triangles.AddRange(SquareToTriangles(currrentLowerLeft, lastLowerLeft, currrentUpperLeft, lastUpperLeft));
// 右側面
triangles.AddRange(SquareToTriangles(lastLowerRight, currrentLowerRight, lastUpperRight, currrentUpperRight));
}
else
{
// 上側面
triangles.AddRange(SquareToTriangles(lastUpperLeft, lastUpperRight, currrentUpperLeft, currrentUpperRight));
// 下側面
triangles.AddRange(SquareToTriangles(lastLowerRight, lastLowerLeft, currrentLowerRight, currrentLowerLeft));
}
}
}
mesh.SetVertices(vertices);
mesh.SetTriangles(triangles, 0);
mesh.SetUVs(0, uvs);
mesh.RecalculateNormals();
return mesh;
}
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;
}
private Square[] PathSquaresGizmo()
{
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;
Vector3 lowerLeft = positions[i] + new Vector3(-direction.y, direction.x, 0f) * Width;
Vector3 lowerRight = positions[i] + new Vector3(direction.y, -direction.x, 0f) * Width;
var square = new Square()
{
lowerLeft = lowerLeft,
lowerRight = lowerRight,
upperLeft = lowerLeft + Vector3.back * Height,
upperRight = lowerRight + Vector3.back * Height,
};
squares[i] = square;
}
return squares;
}
private void OnDrawGizmos()
{
if ((PathPosition == null) || (PathPosition.Length < 2) || !IsDrawGizmo)
{
return;
}
var squares = PathSquaresGizmo();
for (int i = 0; i < squares.Length; i++)
{
Gizmos.DrawSphere(squares[i].lowerLeft, 0.1f);
Gizmos.DrawSphere(squares[i].lowerRight, 0.1f);
Gizmos.DrawSphere(squares[i].upperLeft, 0.1f);
Gizmos.DrawSphere(squares[i].upperRight, 0.1f);
}
}
}