一、概述:
在使用Unity實現VR中在黑板上寫字(初級篇)中的最后留下了一些有待完善的地方,首先完善畫筆穿透畫板的問題;
在之前使用畫筆會出現這種情況:
可以看到畫筆是穿透了畫板,這樣在VR中會給用戶很差的體驗,而且因為代碼的原因會造成畫的過程中中斷,所以這個問題必須解決;
解決后的使用情況:
可以看到現在不會穿透了,而且畫起來不會有中斷,其實我的手的位置已經穿到畫板后面了;
實現這個功能,其實有很多種方法,但是最終覺得參照The Lab里實現的方法比較好————用一個Plane類型解決;
二、知識點
Plane這個類中有兩個方法:
1.public bool GetSide(Vector3 inPt);判斷一個點在Plane的哪一側
Plane把一個空間分成了兩部分,當一個點在Plane的normal(法線)指向的一側的時候,這個值返回True,否則返回False;
2.public float GetDistanceToPoint(Vector3 inPt);判斷一個點距離平面的距離,這個值是帶符號的,當點在平面的正面的時候,返回正值,否則返回負值;
利用這兩個方法可以判斷出筆尖是不是穿透了畫板;
三、代碼原理
筆尖穿透了多少距離,我們就補多少回去,以俯視的視角,想像這個情況
根據上圖,可以等到筆尖穿透的距離,從筆尖的位置減去這個距離,則剛好可以讓筆尖處於平面上;
因為用的是VRTK插件,這個插件集成了很好的物理交互功能,比如抓起東西;VRTK中寫了不少抓起物體的機制:
1.VRTK_ChildOfControllerGrabAttach
2.VRTK_ClimbableGrabAttach
3.VRTK_CustomJointGrabAttach
4.VRTK_FixedJointGrabAttach
5.VRTK_RotatorTrackGrabAttach
6.VRTK_SpringJointGrabAttach
7.VRTK_TrackObjectGrabAttach
對於穿透這種情況,沒有一種是比較合適的,因此需要自己擴展一種抓附機制;上面的抓附機制都是直接或者間接繼承自VRTK_BaseGrabAttach的,因為定義一個VRTK_InteractableObject的抓起方式的字段就是VRTK_BaseGrabAttach類型;
所以綜上,需要擴展一個VRTK_BaseGrabAttach類型的抓附機制,並且Board類有了一些改變,需要增加一個UnityEngine.Plane類型的字段,以及封裝一些函數;
四、代碼實現
首先Board類增加一個Plane類型的字段
internal Plane boardPlane;
然后初始化這個字段
private void Start() { //初始化Plane,讓它的法線是這個畫板的forward向量,並且法線通過畫板的中心位置,由此確定一個平面 boardPlane = new Plane(transform.forward, transform.position);
......
}
最后增加核心的方法
/// <summary> /// 判斷筆尖是在畫板的正面還是背面 /// </summary> /// <param name="point">筆尖的位置</param> /// <returns>true 在正面;false 在背面</returns> public bool GetSideOfBoardPlane(Vector3 point) { return boardPlane.GetSide(point); } /// <summary> /// 筆尖與平面的距離 /// </summary> /// <param name="point">筆尖的位置</param> /// <returns>當在正面的時候返回正值,當在背面的時候返回負值</returns> public float GetDistanceFromBoardPlane(Vector3 point) { return boardPlane.GetDistanceToPoint(point); } /// <summary> /// 矯正后的筆尖應該在的位置 /// </summary> /// <param name="point">筆尖的位置</param> /// <returns>矯正后的筆尖位置</returns> public Vector3 ProjectPointOnBoardPlane(Vector3 point) { float d = -Vector3.Dot(boardPlane.normal, point - transform.position); return point + boardPlane.normal * d; }
然后擴展一個抓附機制
using VRTK.GrabAttachMechanics; using UnityEngine; public class PainterGrabAttach : VRTK_BaseGrabAttach { [Header("Painter Options")] [SerializeField] private Transform tips;//筆尖 private static Board board;//畫板 #region 重寫的父類方法 protected override void Initialise() { //初始化父類的一些字段,這些字段只是標識這個抓附機制的作用 tracked = false; kinematic = false; climbable = false; //初始化自定義的屬性 if (precisionGrab)//最好不要用精確抓取,因為這樣很有可能會讓筆處於一個不合理的位置,這樣使用的時候,會很變扭(比如必須手腕旋轉一個角度,筆才是正的) { Debug.LogError("PrecisionGrab cant't be true in case of PainterGrabAttach Mechanic"); } board = FindObjectOfType<Board>(); } public override bool StartGrab(GameObject grabbingObject, GameObject givenGrabbedObject, Rigidbody givenControllerAttachPoint) { if (base.StartGrab(grabbingObject, givenGrabbedObject, givenControllerAttachPoint)) { SnapObjectToGrabToController(givenGrabbedObject); grabbedObjectScript.IsKinematic = true; return true; } return false; } public override void StopGrab(bool applyGrabbingObjectVelocity) { ReleaseObject(applyGrabbingObjectVelocity); base.StopGrab(applyGrabbingObjectVelocity); } public override void ProcessFixedUpdate() { if (grabbedObject)//只有抓住物體后,grabbedObject才不會 { grabbedObject.transform.rotation = controllerAttachPoint.transform.rotation * Quaternion.Euler(grabbedSnapHandle.transform.localEulerAngles); grabbedObject.transform.position = controllerAttachPoint.transform.position - (grabbedSnapHandle.transform.position - grabbedObject.transform.position); float distance = board.GetDistanceFromBoardPlane(tips.position);//筆尖距離平面的距離 bool isPositiveOfBoardPlane = board.GetSideOfBoardPlane(tips.position);//筆尖是不是在筆尖的正面 Vector3 direction = grabbedObject.transform.position - tips.position;//筆尖位置指向筆的位置的差向量 //當筆尖穿透的時候,需要矯正筆的位置 if (isPositiveOfBoardPlane || distance > 0.0001f) { Vector3 pos = board.ProjectPointOnBoardPlane(tips.position); grabbedObject.transform.position = pos - board.boardPlane.normal * 0.001f + direction;//pos是筆尖的位置,而不是筆的位置,加上direction后才是筆的位置 } } } #endregion //讓手柄抓住物體 private void SnapObjectToGrabToController(GameObject obj) { if (!precisionGrab) { SetSnappedObjectPosition(obj); } } //設置物體和手柄連接的位置 private void SetSnappedObjectPosition(GameObject obj) { if (grabbedSnapHandle == null) { obj.transform.position = controllerAttachPoint.transform.position; } else { //設置旋轉,controllerAttachPoint是手柄上的一個與物體的連接點 obj.transform.rotation = controllerAttachPoint.transform.rotation * Quaternion.Euler(grabbedSnapHandle.transform.localEulerAngles); //因為grabbedSnapHandle和obj.transform之間可能不是同一個點,所以為了讓手柄抓的位置是grabbedSnapHandle,需要減去括號中代表的向量 obj.transform.position = controllerAttachPoint.transform.position - (grabbedSnapHandle.transform.position - obj.transform.position); } } }
五、場景中的設置
1.筆尖的位置還是要設置好
筆尖的位置要設置在筆芯的尖端,snapPoint的位置和旋轉,決定了手柄抓住筆時的位置和旋轉;
2.其它需要注意的一些設置