VR空間内の自分の腰にあるオブジェクト
実際にユーザーからのフィードバックで多いのが、
**腰にあるオブジェクトがわかりづらい(視認しづらい)**という内容です。
そのフィードバックを頂いた際の実装は、
カメラの位置から相対的に腰のオブジェクトの位置を固定していました。
ただ、実空間の腰の位置と相違の無い位置へVR空間内で正確に固定した場合、
実空間での視野角とVR空間での視野角との差異に気付かずに、
腰のオブジェクトが視覚に入らない状態が続いてしまうことが多いようです。
単純に、腰のオブジェクトの位置を実空間の腰より少し前に出すだけで、
ある程度解決はするのですが、明らかに腰を見ていない場合においても
視界に入り込んでくるので違和感を生んでしまいます。
視線に応じて判定
解決策を結論から書くと、
視線に応じて腰のオブジェクトを前後させる です。
このGIFのようなイメージです。わかり易いように少し大げさに動かしています。
下を見ると腰の赤いオブジェクトが前に出てきてます。
一人称視点はこのような感じです。
腰のオブジェクトが動いている感覚はほとんどありません。
図に示した二つのベクトルが正規化されていた場合、
内積の返す値はcosΘの値そのものとなります。
なので、自分で閾値を設定して、
例えば、80°以下になったら前に出てくる、、、などとすればよいのでは?と考えました。
コード
今回のコードはVIVEでしか試してないですが、
階層構造を同様に再現すればQuestでも問題ないかと思います。
using UnityEngine;
/// <summary>
/// 階層構造
/// Camera
/// ┗ Waist -----ここにアタッチ
/// ┗ WaistObj
/// </summary>
public class WaistObjAdjuster: MonoBehaviour
{
[SerializeField, Header("下を見ると腰のオブジェクトが前に飛び出る")]
GameObject m_waistObj;
[SerializeField, Header("どれくらい前に出てくるのか")]
float m_maxComeOutValue = 0.5f;
[SerializeField]
public enum WAISTSIDE
{
LEFT,
RIGHT
}
[SerializeField, Header("左腰or右腰")]
WAISTSIDE m_waistSide = WAISTSIDE.LEFT;
Vector3 waistObjLocalPos;
//定位置に戻る速度
float m_moveSpeedAdjust = 10f;
//腰との間隔
float m_waistObjSpace = 0.2f;
//視線判定の閾値
float m_thresholdDot = -0.1f;
void Start()
{
SetWaistObjSide(m_waistSide);
}
//腰のオブジェクトをどちら側で固定するか
public void SetWaistObjSide(WAISTSIDE waistSide)
{
waistObjLocalPos = m_waistObj.transform.localPosition;
m_waistSide = waistSide;
if (m_waistSide == WAISTSIDE.LEFT)
{
m_waistObj.transform.localPosition = new Vector3(-m_waistObjSpace, waistObjLocalPos.y, waistObjLocalPos.z);
}
else
{
m_waistObj.transform.localPosition = new Vector3(m_waistObjSpace, waistObjLocalPos.y, waistObjLocalPos.z);
}
}
void Update()
{
//角度の制限
Quaternion waistRotation = this.gameObject.transform.parent.transform.rotation;
waistRotation.z = 0;
waistRotation.x = 0;
this.gameObject.transform.rotation = waistRotation;
//腰の位置の固定 0.5fは個人的にベストと感じた腰の位置
Vector3 cameraPos = this.gameObject.transform.parent.position;
this.gameObject.transform.position = new Vector3(cameraPos.x, cameraPos.y-0.5f, cameraPos.z);
//顔面→腰のオブジェクト 、 顔面→顔面の正面 の内積 > 閾値 ) ---> 前へ出てくる
float dot = Vector3.Dot(Vector3.down, this.gameObject.transform.parent.transform.forward);
waistObjLocalPos = m_waistObj.transform.localPosition;
if (dot > m_thresholdDot)
{
float bendedValue = Mathf.Lerp(waistObjLocalPos.z, m_maxComeOutValue, Mathf.Clamp01(dot)/ m_moveSpeedAdjust);
waistObjLocalPos.z = bendedValue;
m_waistObj.transform.localPosition = waistObjLocalPos;
}
else
{
float bendedValue = Mathf.Lerp(waistObjLocalPos.z, 0, Mathf.Clamp01(Mathf.Abs(dot))/ m_moveSpeedAdjust);
waistObjLocalPos.z = bendedValue;
m_waistObj.transform.localPosition = waistObjLocalPos;
}
}
}
カメラの位置から相対的に腰のオブジェクトの位置を固定
これが結構、頭こんがらがりました。
何がしたいかをもう少し具体的に言うと、
カメラ(プレーヤー)がどこを見ようと、どこに移動しようと、腰の位置にオブジェクトがある
という状態を作るということです。
ただ、子にするだけでは、カメラのローテーションに追従して
腰ではない明後日の方向にオブジェクトが移動してしまいます。
なので、Y軸方向の回転だけカメラに追従する、腰の役割を果たすオブジェクト
を間に挟むことにしました。
//角度の制限
Quaternion waistRotation = this.gameObject.transform.parent.transform.rotation;
waistRotation.z = 0;
waistRotation.x = 0;
this.gameObject.transform.rotation = waistRotation;
また、腰の役割を果たすオブジェクトを間に挟むことで、腰の役割を切り離すことに成功し、
子に設定したオブジェクトはその役割を意識せずに
自由に操作できるというメリットも生まれています。
2019/10/28 追記
子に設定したオブジェクトはその役割を意識せずに自由に操作できる
これがどうやら上手くいってませんでした。
というのも、親子関係を解除した場合でも、結局 Inspectorでアタッチしたオブジェクトを参照しているので
腰の動きに連動して動いてしまいました。
何が問題かというと、掴んで自由に動かす...などの際に、
該当する制御スクリプトを毎回オンオフしないと腰の制御するスクリプトと連動してしまい、おかしな挙動になります。
それではあまりスマートではないので親子関係で参照するスクリプトに変更したバージョンを下記に用意しました。
※掴む
の実装が掴んだオブジェクトを手の子にする
などの実装である必要がありますが。。。
using UnityEngine;
/// <summary>
/// 階層構造
/// Camera
/// ┗Waist -----ここにアタッチ
/// ┗WaistObj
/// </summary>
public class WaistObjRestriction : MonoBehaviour
{
// [SerializeField, Header("下を見ると腰のオブジェクトが前に飛び出る")]
GameObject m_waistObj;
[SerializeField, Header("どれくらい前に出てくるのか")]
float m_maxComeOutValue = 0.5f;
[SerializeField]
public enum WAISTSIDE
{
LEFT,
RIGHT
}
[SerializeField, Header("左腰or右腰")]
WAISTSIDE m_waistSide = WAISTSIDE.LEFT;
Vector3 waistObjLocalPos;
//定位置に戻る速度
float m_moveSpeedAdjust = 10f;
//腰との間隔
float m_waistObjSpace = 0.2f;
//視線判定の閾値
float m_thresholdDot = -0.1f;
//腰とカメラの距離
float m_waistHeight = 0.5f;
void Start()
{
ChangeWaistObjSide(m_waistSide);
}
//腰のオブジェクトをどちら側で固定するか
public void ChangeWaistObjSide(WAISTSIDE waistSide)
{
if (CheckChild() == false) return;
waistObjLocalPos = m_waistObj.transform.localPosition;
m_waistSide = waistSide;
if (m_waistSide == WAISTSIDE.LEFT)
{
m_waistObj.transform.localPosition = new Vector3(-m_waistObjSpace, waistObjLocalPos.y, waistObjLocalPos.z);
}
else
{
m_waistObj.transform.localPosition = new Vector3(m_waistObjSpace, waistObjLocalPos.y, waistObjLocalPos.z);
}
}
//子が存在するか
bool CheckChild()
{
if (this.gameObject.transform.childCount > 0)
{
m_waistObj = this.gameObject.transform.GetChild(0).gameObject;
return true;
}
else
{
m_waistObj = null;
return false;
}
}
void Update()
{
//角度の制限
Quaternion waistRotation = this.gameObject.transform.parent.transform.rotation;
waistRotation.z = 0;
waistRotation.x = 0;
this.gameObject.transform.rotation = waistRotation;
//腰の位置の固定
Vector3 cameraPos = this.gameObject.transform.parent.position;
this.gameObject.transform.position = new Vector3(cameraPos.x, cameraPos.y - m_waistHeight, cameraPos.z);
//子が存在していたら腰のオブジェクトとして認識
if (CheckChild() == false) return;
//顔面→腰のオブジェクト 、 顔面→顔面の正面 の内積 > 閾値 ) ---> 前へ出てくる
float dot = Vector3.Dot(Vector3.down, this.gameObject.transform.parent.transform.forward);
waistObjLocalPos = m_waistObj.transform.localPosition;
if (dot > m_thresholdDot)
{
float bendedValue = Mathf.Lerp(waistObjLocalPos.z, m_maxComeOutValue, Mathf.Clamp01(dot) / m_moveSpeedAdjust);
waistObjLocalPos.z = bendedValue;
m_waistObj.transform.localPosition = waistObjLocalPos;
}
else
{
float bendedValue = Mathf.Lerp(waistObjLocalPos.z, 0, Mathf.Clamp01(Mathf.Abs(dot)) / m_moveSpeedAdjust);
waistObjLocalPos.z = bendedValue;
m_waistObj.transform.localPosition = waistObjLocalPos;
}
}
}
おもな変更点としては、子のオブジェクトが存在しているかどうかの判定を行ったことです。
//子が存在するか
bool CheckChild()
{
if (this.gameObject.transform.childCount > 0)
{
m_waistObj = this.gameObject.transform.GetChild(0).gameObject;
return true;
}
else
{
m_waistObj = null;
return false;
}
}
なので、腰に複数のオブジェクトをじゃらじゃらさせたい場合は、下記のように腰ごと増やします。
Camera
┗Waist
┗WaistObj1
┗Waist
┗WaistObj2
まとめ
毎回設定するのが面倒なので、
腰のオブジェクトの位置はスクリプトで制御されるようになってます。
右腰なのか左腰なのかどうかもスクリプトで一撃で変えられるようにしました。
今回の実装は現実空間とVR空間の視野角の差による矛盾に慣れていないVR初心者への対策
として実装しましたが、単純にユーザビリティー向上という意味合いで考えれば、
VRヘビーユーザー向けにも検討される要素の一つではないかと感じました。