SteamVR_GazeTracker(凝視)
凝視是一種在沒有手柄等輸入設備的情況下,可以通過眼睛盯着某個物體看來與物體進行交互的體驗。
我們只需要將個輔組類添加到我們想要凝視的物體上,比如菜單等,就可以實現凝視的功能。現在我們來看看凝視的實現原理。
void Update () {
if (hmdTrackedObject == null) {
/*查找全部的SteamVR_TrackedObject組件,我們知道這個組件是用來跟蹤設備位置的,手柄,頭盔上都有這個組件*/
SteamVR_TrackedObject[] trackedObjects = FindObjectsOfType<SteamVR_TrackedObject>();
/*循環遍歷trackedObject,找到頭盔的trackedObject*/
foreach (SteamVR_TrackedObject tracked in trackedObjects){
if (tracked.index == SteamVR_TrackedObject.EIndex.Hmd) {
/*獲取頭顯的transform*/
hmdTrackedObject = tracked.transform;
break;
}
}
}
if (hmdTrackedObject) {
/*從頭顯發出一條向前的射線*/
Ray r = new Ray(hmdTrackedObject.position, hmdTrackedObject.forward);
Plane p = new Plane(hmdTrackedObject.forward, transform.position);
float enter = 0.0f;
if (p.Raycast(r, out enter)) {
Vector3 intersect = hmdTrackedObject.position + hmdTrackedObject.forward * enter; float dist = Vector3.Distance(intersect, transform.position);
/*如果凝視的點與凝視目標在gazeIncutoff的范圍內,則目標為凝視狀態,並調用OnGazeOn()回調方法*/
if (dist < gazeInCutoff && !isInGaze) { isInGaze = true; GazeEventArgs e; e.distance = dist; OnGazeOn(e);
}
/*如果凝視的點與凝視目標大於gazeIncutoff這個范圍,則目標為非凝視狀態,並調用OnGazeOff()回調方法*/
else if (dist >= gazeOutCutoff && isInGaze){
isInGaze = false; GazeEventArgs e; e.distance = dist; OnGazeOff(e);
}
}
}
}
*通過上面的代碼我們知道了凝視的原理實際上是從頭盔的位置發出一條射線判斷是否與物體相交來做選中或者交互的。而且因為凝視的精確度不高,所以沒有做直接與物體相交,而是在物體的位置創建了一個平面,通過射線與平面相交的交點的位置與物體的距離來大概判斷的。這個距離值是可以調的,缺省是0.15到0.4米之間就算選中了。
*我們現在知道了凝視的交互是如何實現的,實現的方式其實還是挺簡單的,下面我們在來看看射線這種交互方式。
SteamVR_LaserPointer(激光束)
SteamVR_LaserPointer的作用是從指定位置(通常是手柄)發出一條射線,它會將這條射線顯示出來,然后也是判斷這條視線與場景中的物體是否相交。與凝視不一樣的是,它可以精確操作,所以不需要一個輔助平面。用法和凝視也不太一樣,需要將這個組件添加發出射線的物體上,比如手柄。
/*射線事件觸發的回調參數,凝視也是類似的用法*/
public struct PointerEventArgs {
/*手柄的索引*/
public uint controllerIndex;
/*暫時無用的參數*/
public uint flags;
/*射線源到目標的距離*/
public float distance;
/*射線射中的transform對象*/
public Transform target;
}
/*定義命中事件委托函數*/
public delegate void PointerEventHandler(object sender, PointerEventArgs e);
public class SteamVR_LaserPointer : MonoBehaviour {
/*光線顏色*/
public Color color;
/*光線厚度*/
public float thickness = 0.002f;
/*空的GameObject,用來存放光的gameobject*/
public GameObject holder;
public GameObject pointer;
bool isActive = false;
/*是否給激光束添加剛體*/
public bool addRigidBody = false;
/*激光束命中和離開的委托事件*/
public event PointerEventHandler PointerIn;
public event PointerEventHandler PointerOut;
Transform previousContact = null;
void Start () {
/*一些初始化操作,創建激光束父類的GameObject(holder) */
holder = new GameObject();
/*2,將holder的transform的parent設為當前腳本所在的物體(手柄)上面*/
holder.transform.parent = this.transform;
/*3,將holder本地坐標初始為0*/
holder.transform.localPosition = Vector3.zero;
/*4,創建激光束,用長方體模擬(這一點其實不太合理,用圓柱模擬會更好一點)*/
pointer = GameObject.CreatePrimitive(PrimitiveType.Cube);
/*5,將激光束父類設為holder*/
pointer.transform.parent = holder.transform;
/*6,設置激光束locale為(0.002,0.002,100),使它看起來像一條很長的線*/
pointer.transform.localScale = new Vector3(thickness, thickness, 100f);
pointer.transform.localPosition = new Vector3(0f, 0f, 50f);
/*7,是否添加剛體*/
BoxCollider collider = pointer.GetComponent<BoxCollider>();
if (addRigidBody) {
if (collider) {
collider.isTrigger = true;
} Rigidbody rigidBody = pointer.AddComponent<Rigidbody>();
rigidBody.isKinematic = true;
} else {
if(collider) {
Object.Destroy(collider);
}
}
/*8,設置激光束的材質*/
Material newMaterial = new Material(Shader.Find("Unlit/Color"));
newMaterial.SetColor("_Color", color);
pointer.GetComponent<MeshRenderer>().material = newMaterial;
}
public virtual void OnPointerIn(PointerEventArgs e) {
if (PointerIn != null) PointerIn(this, e);
}
public virtual void OnPointerOut(PointerEventArgs e) {
if (PointerOut != null) PointerOut(this, e);
}
// Update is called once per frame
void Update () {
/*第一次調用時將holder設為active*/
if (!isActive) {
isActive = true;
this.transform.GetChild(0).gameObject.SetActive(true);
}
/*將激光束的最遠距離設為100米*/
float dist = 100f;
/*獲取當前物體(手柄)上的SteamVR_TrackedController腳本*/
SteamVR_TrackedController controller = GetComponent<SteamVR_TrackedController>();
/*構造一條射線*/
Ray raycast = new Ray(transform.position, transform.forward);
RaycastHit hit;
bool bHit = Physics.Raycast(raycast, out hit);
/*射線命中物體后移出,說明物體不在命中,調用OnPointerOut的通知*/
if(previousContact && previousContact != hit.transform) {
PointerEventArgs args = new PointerEventArgs();
if (controller != null) {
args.controllerIndex = controller.controllerIndex;
}
args.distance = 0f;
args.flags = 0;
args.target = previousContact; OnPointerOut(args);
previousContact = null;
}
/*射線命中物體,調用OnPointerIn的通知*/
if(bHit && previousContact != hit.transform) {
PointerEventArgs argsIn = new PointerEventArgs();
if (controller != null) {
argsIn.controllerIndex = controller.controllerIndex;
}
argsIn.distance = hit.distance;
argsIn.flags = 0;
argsIn.target = hit.transform;
OnPointerIn(argsIn);
previousContact = hit.transform;
}
if(!bHit) {
previousContact = null;
}
/*如果命中物體距離大於100,則無效,否則有效*/
if (bHit && hit.distance < 100f) {
dist = hit.distance;
}
if (controller != null && controller.triggerPressed) {
/*當按下扳機鍵時,將光束的粗細增大5倍,長度會設為dist,通過這種方法讓光線不會穿透物體*/
pointer.transform.localScale = new Vector3(thickness * 5f, thickness * 5f, dist);
}
else {
/*沒按下扳機或者當前控制器沒有添加SteamVR_TrackedController時,顯示原始粗細的光束*/
pointer.transform.localScale = new Vector3(thickness, thickness, dist);
}
/*將光束的位置設在光束長度的一半的位置,使得光束看起來是從手柄發出來的*/
pointer.transform.localPosition = new Vector3(0f, 0f, dist/2f);
}
}
看完了SteamVR_LaserPointer的代碼,我們就知道了激光束實現的原理,其實激光束實現起來還是蠻簡單的,但是在VR的交互中,使用起來非常的方便。