環境:Unity 2022.3.40f1、Houdini 20.0.751
空港の手荷物受取所 (Baggage Claim)でスーツケースが流れている様子を見て、ゲームとして再現したら面白そうと思いベルトコンベアの仕組みを試作してみました。
こんなカーブなら待っている時間も退屈しませんよね。大惨事にもなりそうですが・・。
概要
カーブのポリラインに進行方向のベクトル情報を保存しておき、ベルトコンベアに接触しているオブジェクトはその位置から一番近いカーブのポイントを参照にして進むべき方向を取得します。後はリジッドボディがよしなに流してくれる、という仕組みです。
Houdiniからカーブ情報を出力
カーブにはフォワードベクターのNとアップベクターのupをあらかじめ計算して設定しておく(upは省いても可)。
以下はカーブの1次元座標を計算するコード。
//
// カーブの始点からの距離を計算する
// Run Over: Detail
//
float curveLength = primintrinsic(0, "measuredperimeter", 0);
float pos = 0;
for(int i = 1; i < npoints(0); i++)
{
vector p0 = point(0, "P", i - 1);
vector p1 = point(0, "P", i);
float dist = length(p1 - p0);
pos += dist;
setpointattrib(0, "v", i, pos);
}
PythonでCSV出力する。
node = hou.pwd()
geo = node.geometry()
# Unityで読み込む形式でCSVを出力する
filePath = hou.node(".").parm("file").eval()
# aは追加書き込みモード
file = open(str(filePath), "w")
index = 0
for point in geo.points():
pos = point.position()
N = point.attribValue("N")
up = point.attribValue("up")
v = point.attribValue("v")
file.write(str(pos[0]) + '\t' + str(pos[1]) + '\t' + str(pos[2]))
file.write('\t' + str(N[0]) + '\t' + str(N[1]) + '\t' + str(N[2]))
file.write('\t' + str(up[0]) + '\t' + str(up[1]) + '\t' + str(up[2]))
file.write('\t' + str(v))
# 最後に空白の行をつくらないようにする
if index < len(geo.points())-1:
file.write("\n")
index+=1
file.close
以下がCSVの一部です。タブ区切りで、座標、方向ベクトル、アップベクトル、一次元座標が順に記録されています。
-0.0734337568283081 0.9239802360534668 -4.10433292388916 -0.49754565954208374 -0.12597721815109253 0.8582412600517273 0.0 1.0 0.0 0.0
-0.7899094223976135 0.803830623626709 -3.0477712154388428 -0.6702364087104797 -0.029401708394289017 0.74088454246521 0.0 1.0 0.0 1.2822229862213135
-1.9044303894042969 0.894450843334198 -2.231799602508545 -0.8989036679267883 0.15703070163726807 0.40779486298561096 0.0 1.0 0.0 2.6664838790893555
-3.672412872314453 1.3747223615646362 -1.9409217834472656 -0.9483020305633545 0.31669583916664124 -0.020187044516205788 0.0 1.0 0.0 4.521485805511475
-5.197384834289551 1.850439429283142 -1.8978019952774048 -0.9540020227432251 0.22318901121616364 0.19865503907203674 0.0 1.0 0.0 6.1195173263549805
-6.019707679748535 1.980631947517395 -1.5830527544021606 -0.8651965856552124 0.06568475067615509 0.4956851601600647 0.0 1.0 0.0 7.009591579437256
-6.600997447967529 1.9799829721450806 -1.1466864347457886 -0.7150841951370239 -0.06954646855592728 0.6954508423805237 0.0 1.0 0.0 7.736443519592285
-7.006156921386719 1.8964163064956665 -0.6507397890090942 -0.5321370959281921 -0.18517862260341644 0.8254241347312927 0.0 1.0 0.0 8.38227653503418
カーブクラス
Houdiniから出力したCSVを読み込んで、ポリラインを構成するクラスをUnityでつくる。
構成要素
構造体
CurvePointという構造体を作成する。Houdiniのポイントのようなものですね。CurvePointは座標Positionと軸のベクトルForwardとUp、そして1次元の座標Vを要素として持ちます。
関数
GetCurvePoint()
一次元座標Vの値から、その座標のポイントの情報をCurvePoint構造体で取得する。
GetClosestV()
入力された座標に対して一番近いカーブ上の一次元座標Vを取得する。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//
// ポリラインのポイント構造体
//
public struct CurvePoint
{
public Vector3 Position; // 座標
public Vector3 Forward; // 進行方向ベクトル
public Vector3 Up; // アップベクトル
public float V; // カーブの1次元座標(実距離)
}
public class Curve : MonoBehaviour
{
public TextAsset csvFile; // 外部から読み込むカーブポイントのCSVファイル
public CurvePoint[] curvePoint; // カーブポイントのリスト(カーブの各ポイントを格納する)
public float curveLength; // カーブの長さ
//
// CSVファイルを読み込んでカーブの各ベクトル情報をCurvePoint構造体に格納していく
//
public CurvePoint[] LoadCSV(string text)
{
// CSVファイルを読み込む
string TextLines = text;
string[] textMessage = TextLines.Split('\n');
CurvePoint[] cp = new CurvePoint[textMessage.Length];
for (int i = 0; i < textMessage.Length; i++)
{
string[] values = textMessage[i].Split('\t');
cp[i].Position = new Vector3(float.Parse(values[0]), float.Parse(values[1]), float.Parse(values[2]));
cp[i].Forward = new Vector3(float.Parse(values[3]), float.Parse(values[4]), float.Parse(values[5]));
cp[i].Up = new Vector3(float.Parse(values[6]), float.Parse(values[7]), float.Parse(values[8]));
cp[i].V = float.Parse(values[9]);
}
return cp;
}
//
// カーブの初期化
//
public void Initialize()
{
// CSVからデータを読み込み、カーブの長さを計算する
curvePoint = LoadCSV(csvFile.text);
curveLength = curvePoint[curvePoint.Length-1].V; // 最後のポイントのV=カーブの長さ
}
//
// vの値から、その座標のポイントの情報をCurvePoint構造体で取得する
//
public CurvePoint GetCurvePoint(float v)
{
CurvePoint result = new CurvePoint();
// 線形探索で該当する区間を探す
for (int i = 1; i < curvePoint.Length; i++)
{
if(curvePoint[i-1].V <= v && v < curvePoint[i].V)
{
// 2点間の線形補完した値を計算する
float ratio = (v - curvePoint[i - 1].V) / (curvePoint[i].V - curvePoint[i - 1].V);
result.Position = Vector3.Lerp(curvePoint[i - 1].Position, curvePoint[i].Position, ratio);
result.Forward = Vector3.Slerp(curvePoint[i - 1].Forward, curvePoint[i].Forward, ratio);
result.Up = Vector3.Slerp(curvePoint[i - 1].Up, curvePoint[i].Up, ratio);
break;
}
}
return result;
}
//
// 特定の座標に対して一番近いカーブ上の座標を取得する
//
public float GetClosestV(Vector3 pos)
{
float minDist = 100000.0f;
float t; // エッジのパラメトリック座標(0-1)
float closestV = 0f;
// 一番近い区間を探す
for (int i = 0; i < curvePoint.Length - 1; i++)
{
Vector3 start = curvePoint[i].Position;
Vector3 end = curvePoint[i+1].Position;
Vector3 line_vec = Vector3.Normalize(end - start);
float d = Vector3.Dot(pos - start, line_vec);
// 直線の範囲内か判別する
if (d >= 0 && d <= (start - end).magnitude)
{
Vector3 projection = start + line_vec * d;
float dist = (projection - pos).magnitude;
// 最小値なら更新する
if (dist < minDist)
{
minDist = dist;
t = d / (start - end).magnitude;
closestV = curvePoint[i].V + (curvePoint[i + 1].V - curvePoint[i].V) * t;
}
}
// 始点より手前なら、始点の座標を返り値に
else if(d < 0)
{
float dist = (pos - start).magnitude;
if (dist < minDist)
{
minDist = dist;
closestV = curvePoint[i].V;
}
}
// 終点より後ろなら、終点の座標を返り値に
else if (d > (start - end).magnitude)
{
float dist = (pos - end).magnitude;
if (dist < minDist)
{
minDist = dist;
closestV = curvePoint[i+1].V;
}
}
}
return closestV;
}
}
点が線分の最短距離を計算する方法はこのページを参考に
線分と点の距離
このスクリプトをベルトコンベアのメッシュにアタッチして、CSVファイルをリンクしておきます。
ベルトコンベアのスクリプト
ベルトコンベア上のオブジェクトを流すスクリプトです。接触のあるリジッドボディをリストに格納して、ループ処理で力を加えて動かしています。
このスクリプトもベルトコンベアのメッシュにアタッチします。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Conveyor : MonoBehaviour
{
// 線形カーブのクラス
Curve curve;
bool IsOn = false;
public float TargetDriveSpeed = 3.0f;
public float CurrentSpeed { get { return _currentSpeed; } }
// ベルトコンベアの方向
Vector3 DriveDirection = Vector3.forward;
[SerializeField] private float _forcePower = 50.0f;
private float _currentSpeed = 0;
// リジッドボディを格納するリスト
private List<Rigidbody> _rigidbodies = new List<Rigidbody>();
// ベルトコンベアに触れたらリジッドボディをリストに追加する
private void OnCollisionEnter(Collision collision)
{
var rigidbody = collision.gameObject.GetComponent<Rigidbody>();
_rigidbodies.Add(rigidbody);
}
// ベルトコンベアから離れたらリジッドボディをリストから除外する
private void OnCollisionExit(Collision collision)
{
var rigidbody = collision.gameObject.GetComponent<Rigidbody>();
_rigidbodies.Remove(rigidbody);
}
// Start is called before the first frame update
void Start()
{
// 線形カーブの読み込みと初期化
curve = GetComponent<Curve>();
curve.Initialize();
}
// Update is called once per frame
void FixedUpdate()
{
_currentSpeed = IsOn ? TargetDriveSpeed : 0f;
_rigidbodies.RemoveAll(r => r == null);
// リジッドボディごとの処理
foreach(var r in _rigidbodies)
{
// 一番近いカーブ座標を取得する
float v = curve.GetClosestV(r.position);
// カーブ座標から、ベクトルを取得する
CurvePoint curvePoint = curve.GetCurvePoint(v);
DriveDirection = curvePoint.Forward.normalized;
// 移動速度のベルトコンベア方向の成分を取り出す
var objectSpeed = Vector3.Dot(r.velocity, DriveDirection);
// 移動速度が目標速度に達していなかったら加速させる
if(objectSpeed < Mathf.Abs(TargetDriveSpeed))
{
r.AddForce(DriveDirection * _forcePower, ForceMode.Acceleration);
}
}
}
}
これでベルトコンベア上にリジッドボディを設定したインスタンスオブジェクトを配置すれば流れるようになります。このギミックはいろいろと応用できそうですね。