OculusのGrab
まずはこちらをご覧ください。
掴んだキューブがデスクをすり抜けているのがわかるかと思います。
実はこれ、OculusQuestのチュートリアルで、
技術力がどうとかの問題ではなく、すり抜けるのはVRのベストプラクティスになりつつあるんですよね。
というのも、手はコントローラーの動きに追従して動くので、
もし、キューブがデスクに衝突してそれ以上先に進めない...となった場合でも、
手は現実空間でフィードバックを受けていないのでそのまま動かせてしまいます。
そうなると、VR空間内の手の動きと現実の手の動きに乖離が生じて違和感を生み出す要因になるということです。
現実世界をコピーした挙動なんて存在しない
VR空間での手の挙動にプレイヤーの自由を与えた場合、
現実世界と同様の物理挙動を再現する方法は現時点のデバイスには存在し得ません。
もし、現実世界と一致する完璧な挙動をVR空間内でも再現したいのであれば、
VR空間での映像にリンクさせて、現実空間でも実際に物体を掴み、
周りのオブジェクトとの衝突も考慮する...といった かなり大掛かりな仕掛けが必要になります。
そうは言っても、手に持っているオブジェクトがすり抜けるのを見てしまうことで、
没入感が減少しているのは紛れもない事実です。
Kinematicな物体に衝突させる
今回はケースによってはベストプラクティスとなり得る可能性を秘めた案を提唱します。
そのために犠牲にしたのは、この2つです。
①オブジェクトを掴んだ時の手は消える
②実際の手の位置とVR空間内で掴んだオブジェクトの位置がずれる
白いキューブ(Kinematic)に、掴んだ赤いキューブがめり込むことなく、
動作しているのが見てとれます。
もともと、Kinematicに設定していて、コライダーの付いているオブジェクトは
静的で外部からの力を受けているようには見えない、いわば自然界の岩のような存在です。
よって、掴んだオブジェクトとの関係性も
GIFのようになるのが望んだ挙動である場合もあり得るのではないか と思い、
今回のような実装を試みました。
実は、VIVEのHome画面では実際に似たような実装がされています。
コード
コードはOculusIntegrationのOVRGrabbable
を継承しています。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PhysicsGrabbable : OVRGrabbable
{
GameObject anchor;
GameObject grabHand;
Rigidbody rb;
Vector3 offsetPos;
Vector3 thisObjPos;
Vector3 tmpGripTrans;
bool isCollision;
bool grabMomentKinematic;
float acceptableDistance = 0.1f;
protected override void Start()
{
rb = this.gameObject.GetComponent<Rigidbody>();
anchor = GameObject.Find("RightControllerAnchor");
grabHand = GameObject.Find("hands:Rhand");
}
public override void GrabBegin(OVRGrabber hand, Collider grabPoint)
{
m_grabbedBy = hand;
m_grabbedCollider = grabPoint;
grabHand.SetActive(false);
this.gameObject.transform.parent = anchor.transform;
tmpGripTrans = this.gameObject.transform.localPosition;
rb.useGravity = false;
grabMomentKinematic = rb.isKinematic;
rb.isKinematic = false;
rb.constraints = RigidbodyConstraints.FreezeRotation;
}
public override void GrabEnd(Vector3 linearVelocity, Vector3 angularVelocity)
{
grabHand.SetActive(true);
this.gameObject.transform.parent = null;
rb.useGravity = true;
rb.isKinematic = grabMomentKinematic;
rb.constraints = RigidbodyConstraints.None;
rb.velocity = linearVelocity;
rb.angularVelocity = angularVelocity;
m_grabbedBy = null;
m_grabbedCollider = null;
}
void OnCollisionEnter(Collision other)
{
isCollision = true;
}
void OnCollisionExit(Collision other)
{
isCollision = false;
}
void Update()
{
if (isGrabbed)
{
rb.velocity = Vector3.zero;
rb.angularVelocity = Vector3.zero;
offsetPos = anchor.transform.position;
thisObjPos = this.gameObject.transform.position;
if (isCollision == false && Vector3.Distance(thisObjPos,offsetPos)>acceptableDistance)
{
this.gameObject.transform.localPosition = tmpGripTrans;
}
}
}
}
先ほど犠牲にしたと説明した、
②実際の手の位置とVR空間内で掴んだオブジェクトの位置がずれる
の箇所ですが、少しでもマシにするために、
○○cm以上離れたら掴んだ時点のポジションに戻す という実装で対処しています。
親子関係で掴んだ表現
今回の掴み実装は親子関係により再現しています。
トリガーを引いた瞬間、掴んだオブジェクトを手のアンカーの子にすることで、
あたかも掴んだかのような表現を実現しています。
そして、掴んだオブジェクトのuseGravity
、isKinematic
をfalseにしています。
これだけでは、無重力空間に浮かんだような
外部からの力に対して摩擦が無い限り、無限に影響を受け続ける状態になるので、
velocity
、angularVelocity
を0に更新し続けています。
ちなみに、通常のOVRGrabbable
は
MovePosition
,MoveRotation
で掴んだ表現が実装されています。
protected virtual void MoveGrabbedObject(Vector3 pos, Quaternion rot, bool forceTeleport = false)
{
if (m_grabbedObj == null)
{
return;
}
Rigidbody grabbedRigidbody = m_grabbedObj.grabbedRigidbody;
Vector3 grabbablePosition = pos + rot * m_grabbedObjectPosOff;
Quaternion grabbableRotation = rot * m_grabbedObjectRotOff;
if (forceTeleport)
{
grabbedRigidbody.transform.position = grabbablePosition;
grabbedRigidbody.transform.rotation = grabbableRotation;
}
else
{
grabbedRigidbody.MovePosition(grabbablePosition);
grabbedRigidbody.MoveRotation(grabbableRotation);
}
}
より現実世界に寄せるには...
そもそも論ですが、現実世界にKinematicなオブジェクトなんて存在しないんですよね。
どんなに大きい岩でも、山でも、それこそ大陸だってとてつもないパワーで押せば
原型をとどめるかどうかは置いといて、理論上動かせるはずです。
でもあまりにも必要なパワーが大きすぎて、全くもって動かせない存在(Kinematic)と言われても通用します。
なので、より現実に近い世界をVRで創造したいのであれば、
全てのオブジェクトをNot Kinematicな状態にして、
動かない理由をDragやPhysics Materialで与えてあげるってのも
一つの正解の形なんじゃないでしょうか。
あと、全然関係ないんですが、ゲームオブジェクト名で手のアンカーやモデルのオブジェクトを
取得しているダサダサコーディングなので両手にも対応できるようにしてからGithubに載せます...
2019/09/21 追記
コメントでご指摘頂いてた、
掴んだオブジェクトがKinematicと衝突すると手との相対位置がずれていき、
一定距離離れると強制リリース(掴みがキャンセル)される
という実装を加えたものを用意しました。
releasable
というbool値を用意したので、Inspectorでオンオフできます。
あと、左右どちらでもいけるようにしました。(ただし、名前指定に変わりはないです)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PhysicsGrabbable : OVRGrabbable
{
[SerializeField] bool releasable;
[SerializeField] float acceptableDistance = 0.1f;
GameObject anchor,anchorL,anchorR;
GameObject grabHand,grabHandL,grabHandR;
Rigidbody rb;
Vector3 offsetPos;
Vector3 thisObjPos;
Vector3 tmpGripTrans;
bool isCollision;
bool grabMomentKinematic;
protected override void Start()
{
rb = this.gameObject.GetComponent<Rigidbody>();
grabHandL = GameObject.Find("hands:Lhand");
anchorL = GameObject.Find("LeftControllerAnchor");
grabHandR = GameObject.Find("hands:Rhand");
anchorR = GameObject.Find("RightControllerAnchor");
}
public override void GrabBegin(OVRGrabber hand, Collider grabPoint)
{
m_grabbedBy = hand;
if (m_grabbedBy.name =="CustomHandLeft")
{
if(grabHandL!=null) grabHand = grabHandL;
if(anchorL!=null) anchor = anchorL;
}
else if (m_grabbedBy.name =="CustomHandRight")
{
if(grabHandR!=null) grabHand = grabHandR;
if(anchorR!=null) anchor = anchorR;
}
m_grabbedCollider = grabPoint;
grabHand.SetActive(false);
this.gameObject.transform.parent = anchor.transform;
tmpGripTrans = this.gameObject.transform.localPosition;
rb.useGravity = false;
grabMomentKinematic = rb.isKinematic;
rb.isKinematic = false;
rb.constraints = RigidbodyConstraints.FreezeRotation;
}
public override void GrabEnd(Vector3 linearVelocity, Vector3 angularVelocity)
{
grabHand.SetActive(true);
this.gameObject.transform.parent = null;
rb.useGravity = true;
rb.isKinematic = grabMomentKinematic;
rb.constraints = RigidbodyConstraints.None;
rb.velocity = linearVelocity;
rb.angularVelocity = angularVelocity;
m_grabbedBy = null;
m_grabbedCollider = null;
}
void OnCollisionEnter(Collision other)
{
isCollision = true;
}
void OnCollisionExit(Collision other)
{
isCollision = false;
}
void Update()
{
if (isGrabbed)
{
rb.velocity = Vector3.zero;
rb.angularVelocity = Vector3.zero;
offsetPos = anchor.transform.position;
thisObjPos = this.gameObject.transform.position;
if (isCollision == false && Vector3.Distance(thisObjPos,offsetPos)>acceptableDistance)
{
this.gameObject.transform.localPosition = tmpGripTrans;
}
if (releasable && Vector3.Distance(thisObjPos, offsetPos) > acceptableDistance)
{
m_grabbedBy.ForceRelease(this.gameObject.GetComponent<PhysicsGrabbable>());
}
}
}
}
無茶しようとすると掴みが自動解除されます。
なんかOculusの権利関係ややこしそうなので、
コピーライティング書いときます。
Copyright © Facebook Technologies, LLC and
its affiliates. All rights reserved.