完成形
そもそもCinemachineって?
映画みたいにカメラワークをいろいろできるよ~みたいなアセット(雑
今回使うのはその中のCinamachineSmoothPath(以下SmoothPath)というもの。
以下の様に線を引き、それに従ってカメラが動くよ~っていう感じ。
前置き
今回の記事では以下の問題点を解決していく。
- レールのメッシュ
SmoothPathはギズモ表示のみで、実際にメッシュとして描画されるない。レールのメッシュを別途用意する必要がある - 加減速の対応
デフォルトでは等速で移動する。重力やカーブによる加減速には対応していないため、調整が必要
なお、SmoothPathの設置方法については既に設置されている前提で進めるため、詳細は各自調べていただきたい。
メッシュを作る
頂点を一つ一つ配置し辺を結んでいくことで動的にメッシュを作成することが出来る。
そのため、SmoothPathから等間隔で位置、回転情報を取得し、それに合わせてメッシュを生成していく。
注意点としては辺用の配列の順だろうか。
辺用の配列(int)には頂点用の配列(Vector3)のインデックス番号を順に入れていく。
この時、[1,2,3,1,2,3,1,2,3...]の順でメッシュが生成されていくため、適切な順で入れてやらないとメッシュが狂う。
今回はレールなので右レール、左レール、見栄え用の中央レール、横に結ぶ枕木(?)、支える柱を生成している。
全文
using System.Collections.Generic;
using System.Linq;
using Cinemachine;
using UnityEditor;
using UnityEngine;
using PositionUnits = Cinemachine.CinemachinePathBase.PositionUnits;
[RequireComponent(typeof(MeshRenderer))]
[RequireComponent(typeof(MeshFilter))]
public class GenerateMesh : MonoBehaviour
{
[SerializeField] private CinemachineSmoothPath smoothPath; // メッシュ生成のために使用するSmooth Path
[SerializeField] private Vector4[] railSpacings; // メッシュの各部分(レール、枕木など)の間隔
[Header("枕木設定: 幅、高さ、サイズ、間隔")]
[SerializeField] private Vector4 crosstieSpacings = new Vector4(1, 1, 30, 1); // 枕木の間隔設定
[SerializeField] private float crosstieCenterY; // 枕木の中央部分の縦のオフセット
[SerializeField] private float crosstieCornerY; // 枕木の角部分の縦のオフセット
[Header("柱設定: 幅、高さ、サイズ、間隔")]
[SerializeField] private Vector4 pillerSpacings = new Vector4(1, 1, 30, 1); // 柱の間隔設定
[SerializeField] private Vector3 pillerEnd; // 柱の端のオフセット
[SerializeField] private Material meshMaterial; // 生成されたメッシュに適用するマテリアル
private const PositionUnits Units = PositionUnits.PathUnits; // パス評価に使用する単位
// インスペクターで変更があったときに呼び出される
private void OnValidate() => Generate();
// メッシュ生成のメイン関数
private void Generate()
{
var mesh = new Mesh { vertices = CalcAllVertices(out int ct) }; // 頂点を計算してメッシュを作成
int halfLength = (mesh.vertices.Length - ct) / railSpacings.Length; // 各パーツの長さを計算
var triangles = new List<int>(); // 三角形インデックスを格納するリスト
foreach (var i in Enumerable.Range(0, railSpacings.Length))
{
CalcTriangles(halfLength, halfLength * i, ref triangles); // 各パーツの三角形を計算
}
CalcTrianglesBox(ct, mesh.vertices.Length - ct, ref triangles); // 枕木部分の三角形を計算
mesh.triangles = triangles.ToArray(); // 三角形データをメッシュに設定
mesh.uv = CalcUvs(mesh.vertices); // メッシュのUVマッピングを計算
mesh.RecalculateNormals(); // 法線を再計算してライティングを調整
// 生成したメッシュとマテリアルをMeshFilterとMeshRendererに設定
gameObject.GetComponent<MeshFilter>().mesh = mesh;
gameObject.GetComponent<MeshRenderer>().material = meshMaterial;
}
/// <summary>
/// 頂点座標を計算して返す
/// </summary>
/// <returns>計算した頂点座標の配列</returns>
private Vector3[] CalcAllVertices(out int crosstieLength)
{
// 各レール部分の頂点を格納するリストを用意
var allVertices = railSpacings.Select(_ => new List<Vector3>()).ToArray();
// 各Waypointを使って頂点を計算
for (var part = 0; part < smoothPath.m_Waypoints.Length; part++)
{
// 各間隔設定に対して頂点を計算
foreach (var (i, spacing) in railSpacings.Select((s, idx) => (idx, s)))
{
allVertices[i].AddRange(CalcVerticesPart(part, spacing, spacing.w));
}
}
// 枕木の頂点を計算してリストに追加
var crosstieVertices = CalcCrosstieVerticesBox().ToList();
allVertices[0].AddRange(allVertices.Skip(1).SelectMany(v => v));
allVertices[0].AddRange(crosstieVertices); // 最終的な頂点配列に枕木頂点を追加
crosstieLength = crosstieVertices.Count; // 枕木の頂点数を返す
return allVertices[0].ToArray(); // すべての頂点を返す
}
/// <summary>
/// 2点間の頂点を計算して返す
/// </summary>
/// <param name="part">Waypointのインデックス</param>
/// <param name="space">間隔</param>
/// <param name="width">幅</param>
/// <returns>計算した頂点座標のリスト</returns>
private IEnumerable<Vector3> CalcVerticesPart(int part, Vector3 space, float width)
{
return Enumerable.Range(0, smoothPath.DistanceCacheSampleStepsPerSegment).Select(i =>
{
// 位置を計算
float pos = part + (float)i / smoothPath.DistanceCacheSampleStepsPerSegment;
Vector3 point = transform.InverseTransformPoint(smoothPath.EvaluatePositionAtUnit(pos, Units));
// パスの接線方向に基づいて回転を計算
Quaternion rot = Quaternion.LookRotation(smoothPath.EvaluateTangentAtUnit(pos, Units), Vector3.up) * CalcRollQuaternion(pos, part, smoothPath.EvaluateTangentAtUnit(pos, Units));
// 頂点の位置を計算
Vector3 r1 = rot * Vector3.right * width / 2;
Vector3 r2 = rot * Vector3.up * width / 2;
Vector3 adjustedPoint = point + rot * space;
return new[] { adjustedPoint - r1 - r2, adjustedPoint + r1 - r2, adjustedPoint + r1 + r2, adjustedPoint - r1 + r2 };
}).SelectMany(v => v);
}
/// <summary>
/// 枕木部分の頂点座標を計算して返す
/// </summary>
private IEnumerable<Vector3> CalcCrosstieVerticesBox()
{
float space = crosstieSpacings.w;
// 配置間隔が短すぎないように調整
if (space <= 1) space = 1;
// レールの長さを測定
List<float> length = new List<float>();
for (int i = 0; i < smoothPath.MaxPos; i++)
{
// ポイント間の直線距離を計算
Vector3 start = smoothPath.EvaluatePositionAtUnit(i, Units);
Vector3 end = smoothPath.EvaluatePositionAtUnit(i + 1 == smoothPath.MaxPos ? 0 : i + 1, Units);
length.Add((end - start).magnitude);
}
// レールの全長を求める
float pathLength = length.Sum();
// レールの長さに基づいてスケールを計算
float scale = smoothPath.PathLength / pathLength;
// 各区間の長さにスケールを適用
length = length.Select(len => len * scale).ToList();
pathLength = length.Sum(); // 新しい全長を計算
// 枕木を引く本数を計算
float crosstieNum = (int)(pathLength / space) - 1;
// 枕木間の距離を計算
float crosstieDist = smoothPath.PathLength / crosstieNum;
// 頂点リスト
var vertices = new List<Vector3>();
int ps = 0;
// 頂点を作成
for (float i = 0; i < pathLength; i += crosstieDist)
{
// 各位置での座標を計算
var pos = CalcLength2Pos(length, i);
var point = smoothPath.EvaluatePositionAtUnit(pos, Units);
var localPoint = transform.InverseTransformPoint(point);
var tangent = smoothPath.EvaluateTangentAtUnit(pos, Units);
var rot = Quaternion.LookRotation(tangent, Vector3.up); // 回転を修正
var rollRot = CalcRollQuaternion(pos, (int)pos, tangent);
// 各頂点の位置を計算
Vector3 r1 = (rot * rollRot * Vector3.right) * crosstieSpacings.x / 2;
Vector3 r2 = (rot * rollRot * Vector3.up) * crosstieSpacings.y / 2;
Vector3 r3 = (rot * rollRot * Vector3.forward) * crosstieSpacings.z / 2;
var y1 = (rot * rollRot * Vector3.up) * crosstieCenterY;
var y2 = (rot * rollRot * Vector3.up) * crosstieCornerY;
// 頂点作成
vertices.Add(localPoint - r1 - r2 + r3 + y1);
vertices.Add(localPoint - r2 + r3 + y2);
vertices.Add(localPoint + r2 + r3 + y2);
vertices.Add(localPoint - r1 + r2 + r3 + y1);
vertices.Add(localPoint - r1 - r2 - r3 + y1);
vertices.Add(localPoint - r2 - r3 + y2);
vertices.Add(localPoint + r2 - r3 + y2);
vertices.Add(localPoint - r1 + r2 - r3 + y1);
vertices.Add(localPoint + r1 - r2 + r3 + y1);
vertices.Add(localPoint - r2 + r3 + y2);
vertices.Add(localPoint + r2 + r3 + y2);
vertices.Add(localPoint + r1 + r2 + r3 + y1);
vertices.Add(localPoint + r1 - r2 - r3 + y1);
vertices.Add(localPoint - r2 - r3 + y2);
vertices.Add(localPoint + r2 - r3 + y2);
vertices.Add(localPoint + r1 + r2 - r3 + y1);
if (ps >= (int)pillerSpacings.w)
{
for (int n = 0; n < 2; n++)
{
r1 =(rot * rollRot * Vector3.right) * pillerSpacings.x / 2;
r2 = (Vector3.up) * pillerSpacings.y;
r3 = (rot * rollRot * Vector3.forward) * pillerSpacings.z / 2;
var r4 = (rot * rollRot * (pillerEnd * (n == 1 ? -1 : 1)));
// 頂点作成
vertices.Add(localPoint - r1 + r3);
vertices.Add(localPoint - r1 - r2 + r3 + r4);
vertices.Add(localPoint + r1 - r2 + r3 + r4);
vertices.Add(localPoint + r1 + r3);
vertices.Add(localPoint - r1 - r3);
vertices.Add(localPoint - r1 - r2 - r3 + r4);
vertices.Add(localPoint + r1 - r2 - r3 + r4);
vertices.Add(localPoint + r1 - r3);
}
ps = 0;
}
else ps++;
}
return vertices;
}
private void CalcTrianglesBox(int verticesLength, int startIndex, ref List<int> triangles)
{
/* 7 -- 6
* / /
* 3 -- 2
* 4 -- 5
* / /
* 0 -- 1
*/
for (int i = 0; i < verticesLength - 5; i += 8)
{
int vp = startIndex + i;
triangles.AddRange(new[]
{
vp, vp + 1, vp + 2,
vp, vp + 2, vp + 3,
vp + 3, vp + 2, vp + 6,
vp + 3, vp + 6, vp + 7,
vp + 7, vp + 6, vp + 5,
vp + 7, vp + 5, vp + 4,
vp + 4, vp + 5, vp + 1,
vp + 4, vp + 1, vp });
}
}
/// <summary>
/// 頂点を結ぶ順番を計算して返す
/// </summary>
/// <param name="verticesLength">頂点の数</param>
/// <param name="startIndex">開始インデックス</param>
/// <param name="triangles">格納するリスト</param>
/// <returns>頂点を結ぶ順番の配列</returns>
private void CalcTriangles(int verticesLength, int startIndex, ref List<int> triangles)
{
for (int i = 0; i < verticesLength - 5; i++)
{
int vp = startIndex + i;
triangles.AddRange(new[] { vp, vp + 1, vp + 4, vp + 1, vp + 5, vp + 4 });
}
int pn = startIndex + verticesLength - 5;
for (int n = 0; n < 4; n++)
{
int li = n - 1 == -1 ? pn + 4 : startIndex + n - 1;
triangles.AddRange(new[] { pn, pn + 1, li, pn + 1, startIndex + n, li });
pn += 1;
}
triangles.AddRange(new[] { pn, startIndex, startIndex + 3, startIndex + 3, startIndex, startIndex + 4 });
}
/// <summary>
/// UVを計算して返す
/// </summary>
/// <param name="vertices">メッシュの頂点座標</param>
/// <returns>UV座標の配列</returns>
private Vector2[] CalcUvs(Vector3[] vertices)
{
float textureScale = 1.0f;
return vertices.Select((v, i) => new Vector2(i % 2 == 0 ? 0 : textureScale, v.y * textureScale)).ToArray();
}
// ロールの回転クォータニオンを計算する
private Quaternion CalcRollQuaternion(float pos, int part, Vector3 tangent)
{
// 現在地点と次の地点のロールを取得する
int size = smoothPath.m_Waypoints.Length;
float currentRoll = smoothPath.m_Waypoints[part].roll;
float nextRoll = smoothPath.m_Waypoints[part + 1 == size ? part : part + 1].roll;
// 今見ている地点のロールを計算する
float nowRoll = Mathf.Lerp(currentRoll, nextRoll, pos - part);
// ロール用の回転クォータニオンを作成
var rollRot = new Vector3(0.0f, 0.0f, nowRoll);
return Quaternion.RotateTowards(Quaternion.Euler(tangent), Quaternion.Euler(rollRot), 200.0f);
}
// パス上の距離からPosを割り出す
private float CalcLength2Pos(List<float> lenList, float length)
{
float sum = 0.0f;
for (int i = 0; i < lenList.Count; i++)
{
// 確認したい距離が0ならループを抜ける
if (length.Equals(0.0f)) break;
// 距離を加算
sum += lenList[i];
// 距離の合計よりlengthが長いなら次の要素へ
if (sum <= length) continue;
// 一番最初の時
if (i == 0) return length / lenList[i];
// ポイントのどれぐらいの位置にいるか計算する
float dist = lenList[i] - (sum - length);
return (float)i + dist / lenList[i];
}
return 0.0f;
}
}
速度を調節する
どのぐらいの高さからどれくらい落ちたかをもとにスピードを計算
上り坂では位置エネルギーが増加し、運動エネルギーが減少するため、カートの速度が減速し
下り坂では位置エネルギーが減少し、運動エネルギーが増加するため、カートは加速する。
そして、CinemachineDollyCartのスピードを直接触ることで加減速をしている。
全文
using Cinemachine;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SpeedController : MonoBehaviour
{
/*==================== 定数 ====================*/
// 重力加速度
const float G_ACCEL = 9.8f;
/*==================== 変数 ====================*/
// レールのパス情報
[SerializeField]
private CinemachineSmoothPath cinemachinePath;
// 速度の調整値
[SerializeField, Range(0.0f, 10.0f)]
private float speedAdjustment;
public void SetSpeedAdj(float speed) => speedAdjustment = speed;
// 重さ
[SerializeField]
private float weight;
// カートオブジェクト
private GameObject carObject;
// カートスクリプト
private CinemachineDollyCart cartScript;
// レールの低い位置と高い位置
private Vector2 height = new Vector2(float.MaxValue, float.MinValue);
// カートがどのぐらいの高さにいるかの割合
float heightRatio = 0.0f;
// 速度
private float cartSpeed = 0.0f;
// 位置エネルギー
private float positionEnergy = 0.1f;
// 運動エネルギー
private float workEnergy = 0.1f;
/*==================== 関数 ====================*/
//--------------------------------------------------------
// 初期化
//--------------------------------------------------------
void Awake()
{
// カートオブジェクト、スクリプト取得
carObject = this.gameObject;
cartScript = this.GetComponent<CinemachineDollyCart>();
ResetDatas();
}
//--------------------------------------------------------
// 更新
//--------------------------------------------------------
void Update()
{
// 高さの割合を計算
heightRatio = CalculateHeightRatio(carObject.transform.position.y, height.y);
// 位置エネルギーの計算
float posEne = CalculatePositionEnergy(weight, G_ACCEL, heightRatio);
// 速度の計算
float sp = Mathf.Sqrt((workEnergy + positionEnergy - posEne) / weight * 2);
sp = Mathf.Max(sp, 0.5f);
// 運動エネルギーの計算
float workEne = CalculateWorkEnergy(weight, sp);
// 速度を調整する
cartSpeed = sp * speedAdjustment;
// 位置、運動エネルギーを保存しておく
positionEnergy = posEne;
workEnergy = workEne;
// 速度設定
SetSpeed(cartSpeed);
}
//--------------------------------------------------------
// 速度の設定
//--------------------------------------------------------
public void SetSpeed(float speed)
{
cartScript.m_Speed = speed;
}
//--------------------------------------------------------
// 高さの割合計算
//--------------------------------------------------------
private float CalculateHeightRatio(float currentHeight, float maxHeight)
{
return currentHeight / maxHeight;
}
//--------------------------------------------------------
// 位置エネルギーの計算
//--------------------------------------------------------
private float CalculatePositionEnergy(float weight, float gravityAcceleration, float height)
{
return weight * gravityAcceleration * height;
}
//--------------------------------------------------------
// 運動エネルギーの計算
//--------------------------------------------------------
private float CalculateWorkEnergy(float weight, float speed)
{
return weight * speed * speed * 0.5f;
}
//--------------------------------------------------------
// リセット
//--------------------------------------------------------
public void ResetDatas()
{
height = new Vector2(float.MaxValue, float.MinValue);
heightRatio = 0.0f;
cartSpeed = 0.0f;
positionEnergy = 0.1f;
workEnergy = 0.1f;
// レールのウェイポイントを受け取る
var wayPoints = cinemachinePath.m_Waypoints;
// レールの一番低い位置を高い位置を取得する
foreach (var wayPoint in wayPoints)
{
// ポイントの位置を取得
Vector3 position = wayPoint.position;
// 一番高い・低い物を保存
if (height.x > position.y) height.x = position.y;
if (height.y < position.y) height.y = position.y;
}
// レールオブジェクトのトランスフォーム取得
Transform railObj = cinemachinePath.transform;
// 位置と拡大を取り出す
Vector3 railPos = railObj.position;
Vector3 railScale = railObj.localScale;
// ワールドの位置に変換
height.x = railPos.y + height.x * railScale.y;
height.y = railPos.y + height.y * railScale.y;
// 高さの割合を計算する
heightRatio = CalculateHeightRatio(carObject.transform.position.y, height.y);
// 位置エネルギーの計算
positionEnergy = CalculatePositionEnergy(weight, G_ACCEL, heightRatio);
// 運動エネルギーの計算
workEnergy = CalculateWorkEnergy(weight, 0.0f);
}
}