Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9547

UnityとQuest2でVR空間を移動する手振り歩行の実装

$
0
0

モチベーション

VRアプリを開発する際、バーチャル空間をどのように移動するかは悩みどころの一つだと思います。既存のアプリで多い実装は、瞬時に指定位置に移動する「テレポーテーション」や、従来の2Dゲームを踏襲した「ジョイスティック移動」、それに加えて移動の際に視野を絞って視界の変化を減らす対策などが採られることが多いと思います。VR空間を移動する際にVR酔いが起きてしまうのは、現実世界での体の動きとVR空間での視界の動きに乖離が生まれることが主な理由で、これを防ぐためにはなるべく大きいフィジカルな動作を採用した方が良さそうです。そこで今回は、前腕(ぜんわん)を上下に運動させる通称「手振り歩行」を実装してみました。

目的

Unity と Oculus Quest 2 を用いて、VR空間で「手振り歩行」を実装すること。

実行環境

  • Unity 2019.4.14f1
  • Oculus Integration for Unity - v25
  • Mac OSX 11.2.3

実装

前提として、Oculus Integrationをインストールします。パッケージに含まれる"PlayerController.cs"の処理に倣いつつ、同クラスに登場する"PreCharacterMove()"を新しく定義する形で処理を記述しました。

usingSystem;usingSystem.Collections;usingUnityEngine;publicclassPlayerMotion:MonoBehaviour{[SerializeField]privateGameObjectOVRPlayerControllerGameObject=null;[SerializeField]privateTransformLeftHandAnchorTransform=null;[SerializeField]privateTransformRightHandAnchorTransform=null;privateOVRPlayerControllerOVRPlayerControllerComponent;// identical to fields of OVRPlayerController classprivateCharacterControllerController;privateVector3MoveThrottle=Vector3.zero;privatefloatMoveScale=1.0f;privatefloatMoveScaleMultiplier=1.0f;privatefloatSimulationRate=60f;privatefloatFallSpeed=0.0f;privatefloatAcceleration;privatefloatDamping;privatefloatGravityModifier;privatefloatJumpForce;// original fields for this scriptprivateVector3touchVelocityL;privateVector3touchVelocityR;privateVector3touchAccelerationL;privateVector3touchAccelerationR;privateboolmotionInertia=false;privatefloatmotionInertiaDuration=1.0f;constfloatWALK_THRESHOLD=0.8f;constfloatRUN_THRESHOLD=1.3f;constfloatJUMP_THRESHOLD=1.5f;privatevoidAwake(){Controller=OVRPlayerControllerGameObject.GetComponent<CharacterController>();OVRPlayerControllerComponent=Controller.GetComponent<OVRPlayerController>();}privatevoidStart(){// store public fields of OVRPlayerController-class to local private filedsAcceleration=OVRPlayerControllerComponent.Acceleration;Damping=OVRPlayerControllerComponent.Damping;GravityModifier=OVRPlayerControllerComponent.GravityModifier;JumpForce=OVRPlayerControllerComponent.JumpForce;// pre-setting for overriding character-controlOVRPlayerControllerComponent.PreCharacterMove+=()=>CharacterMoveByHandShake();OVRPlayerControllerComponent.EnableLinearMovement=false;// necessary for initial grounded-evaluationController.Move(Vector3.zero*Time.deltaTime);}privatevoidUpdate(){}privatevoidCharacterMoveByHandShake(){HandShakeControler();UpdateController();// display for development purposeDebug.Log("L-touch velocity: "+touchVelocityL);Debug.Log("R-touch velocity: "+touchVelocityR);}privatevoidHandShakeControler(){touchVelocityL=OVRInput.GetLocalControllerVelocity(OVRInput.Controller.LTouch);touchVelocityR=OVRInput.GetLocalControllerVelocity(OVRInput.Controller.RTouch);touchAccelerationL=OVRInput.GetLocalControllerAcceleration(OVRInput.Controller.LTouch);touchAccelerationR=OVRInput.GetLocalControllerAcceleration(OVRInput.Controller.RTouch);if(!IsGrounded())MoveScale=0.0f;elseMoveScale=1.0f;MoveScale*=SimulationRate*Time.deltaTime;floatmoveInfluence=Acceleration*0.1f*MoveScale*MoveScaleMultiplier;TransformactiveHand;Vector3handShakeVel;Vector3handShakeAcc;if(Math.Abs(touchVelocityL.y)>Math.Abs(touchVelocityR.y)){activeHand=LeftHandAnchorTransform;handShakeVel=touchVelocityL;handShakeAcc=touchAccelerationL;}else{activeHand=RightHandAnchorTransform;handShakeVel=touchVelocityR;handShakeAcc=touchAccelerationR;}Quaternionort=activeHand.rotation;Vector3ortEuler=ort.eulerAngles;ortEuler.z=ortEuler.x=0f;ort=Quaternion.Euler(ortEuler);MoveThrottle+=CalculateMoveEffect(moveInfluence,ort,handShakeVel,handShakeAcc);}privateVector3CalculateMoveEffect(floatmoveInfluence,Quaternionort,Vector3handShakeVel,Vector3handShakeAcc){Vector3tmpMoveThrottle=Vector3.zero;boolisWalk=DetectHandShakeWalk(Math.Abs(handShakeVel.y))||motionInertia;if(isWalk){if(!motionInertia)SetMotionInertia();tmpMoveThrottle+=ort*(OVRPlayerControllerGameObject.transform.lossyScale.z*moveInfluence*Vector3.forward)*0.2f;boolisRun=DetectHandShakeRun(Math.Abs(handShakeVel.y));if(isRun)tmpMoveThrottle*=2.0f;}boolisJump=DetectHandShakeJump();if(isJump)tmpMoveThrottle+=newVector3(0.0f,JumpForce,0.0f);returntmpMoveThrottle;}IEnumeratorSetMotionInertia(){motionInertia=true;yieldreturnnewWaitForSecondsRealtime(motionInertiaDuration);motionInertia=false;}privateboolDetectHandShakeWalk(floatspeed){if(!IsGrounded())returnfalse;if(speed>WALK_THRESHOLD)returntrue;returnfalse;}privateboolDetectHandShakeRun(floatspeed){if(!IsGrounded())returnfalse;if(speed>RUN_THRESHOLD)returntrue;returnfalse;}privateboolDetectHandShakeJump(){if(!IsGrounded())returnfalse;if(touchVelocityL.y>JUMP_THRESHOLD&&touchVelocityR.y>JUMP_THRESHOLD)returntrue;returnfalse;}privateboolIsGrounded(){if(Controller.isGrounded)returntrue;varpos=OVRPlayerControllerGameObject.transform.position;varray=newRay(pos+Vector3.up*0.1f,Vector3.down);vartolerance=0.3f;returnPhysics.Raycast(ray,tolerance);}privatevoidUpdateController(){Vector3moveDirection=Vector3.zero;floatmotorDamp=1.0f+(Damping*SimulationRate*Time.deltaTime);MoveThrottle.x/=motorDamp;MoveThrottle.y=(MoveThrottle.y>0.0f)?(MoveThrottle.y/motorDamp):MoveThrottle.y;MoveThrottle.z/=motorDamp;moveDirection+=MoveThrottle*SimulationRate*Time.deltaTime;// calculate gravity influenceif(Controller.isGrounded&&FallSpeed<=0)FallSpeed=Physics.gravity.y*(GravityModifier*0.002f);elseFallSpeed+=Physics.gravity.y*(GravityModifier*0.002f)*SimulationRate*Time.deltaTime;moveDirection.y+=FallSpeed*SimulationRate*Time.deltaTime;if(Controller.isGrounded&&MoveThrottle.y<=OVRPlayerControllerGameObject.transform.lossyScale.y*0.001f){// offset correction for uneven groundfloatbumpUpOffset=Mathf.Max(Controller.stepOffset,newVector3(moveDirection.x,0,moveDirection.z).magnitude);moveDirection-=bumpUpOffset*Vector3.up;}Vector3predictedXZ=Vector3.Scale(Controller.transform.localPosition+moveDirection,newVector3(1,0,1));// update character positionController.Move(moveDirection);Vector3actualXZ=Vector3.Scale(Controller.transform.localPosition,newVector3(1,0,1));if(predictedXZ!=actualXZ)MoveThrottle+=(actualXZ-predictedXZ)/(SimulationRate*Time.deltaTime);}}

上のコードをスクリプトに貼り付け、シーン中のGameObjectにアタッチすると動作しました。ベストな実装かはわかりませんが、意図した動作は実現できました。
実装のポイントは、「HMDの正面の方向に移動」ではなく、「速度の大きい方のコントローラーの向いている方向」へ移動する点です。この点は他の「VR, arm swing, walk in place (WIP)」などで検索して出てくる実装と異なる点かな、と思っています。両手を同時に振り上げると「ジャンプ!」をする処理なども入れています。

結果

実行した際のキャプチャを撮影しました:
handshakeWalk.gif
インスペクタからAccelerationやDampなどの値を調整することで、それなりにヌルヌルと動くようになりました。Oculus Integration のデフォルトではGravityの値もかなり大きめに設定されているので、この値も0.1程度に下げた方が落下した際に酔いを感じにくくなります。

終わりに

実装当初は「コントローラーの速度情報を一次配列に格納して、腕振りの波形データとの類似度を計算して…」などと考えていましたが、フレーム毎の速度情報のみでも、きちんと動きたい方向に動けていることは確認できました。自分しか試していない(N=1)ので実際はわかりませんが、主観的にはそれなりにVR酔いを抑えられていそうです。(私の知る限りでは)Oculus Store などに並ぶアプリでこの移動手法が使われている例を見たことがありませんが、こういうゲームがあってもいいような気がしています。そのうち自分で作ろう!


Viewing all articles
Browse latest Browse all 9547

Trending Articles