使用Unity实现VR中在黑板上写字(升级篇)(一)-----解决画笔穿透画板的问题
一、概述:
在使用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.其它需要注意的一些设置