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的交互中,使用起来非常的方便。