写给VR手游开发小白的教程:(八)最终篇:Cardboard如何实现沉浸式VR体验之头部跟踪和Gaze的实现

对于Cardboard的介绍终于到了最终篇,感谢一路走来的你们!!


下一个阶段会推出一些示例demo,网上关于unity VR的demo很少,学习的资源也很贫乏,很多东西仍然需要自己钻研琢磨。


开始今天的主题。

最后部分来到的是人与虚拟世界的互动环节,分为了两部分,关于head tracking实现的技术比较复杂,涉及到Android高级传感器编程,所以对于头部跟踪这里只进行综述以及对其他交互技术的介绍。重点落在后者(Gaze)上面。

一、头部跟踪技术

Cardboard将头部跟踪数据的调取实现细节封装了,只提供一个Get HeadPose()来返回一个位置数组,然后对位置数组进行处理。

之前看到过一篇介绍android四元数的文章,有兴趣的同学可以仔细研究一下头部跟踪技术以及android端是怎么实现的:

http://blog.csdn.net/d_uanrock/article/details/50502840


介绍现有的一些交互技术:

眼球追踪:

http://v.youku.com/v_show/id_XMTY4NDE4Mjg0MA==.html

通过眼部运动来旋转一个地球。

http://v.youku.com/v_show/id_XMTY4NDE3ODQ0NA==.html

通过眼部运动操作一些图片。


手柄控制:

比较原始的方法,也是现在普遍采用的。


空间手势识别:

leap motion(这个强烈推荐!!!将会代替手柄的下一代技术,震撼感很强)


二、Gaze的实现

Gaze英译是凝视,它代表了一个不需要其他输入设备的一个输入,我们一般将Gaze点取在屏幕的中央,当Gaze点位于某控件上方一定时间时,会触发事件(比如说Gaze位于Button上方超过了0.1s,触发了按下按钮的事件),这样的Gaze,实际上是代替了眼球运动和手势运动的一种简化版输入,当我们在使用手机VR的时候,不能通过一个touch来操作屏幕,也没有很多高端的技术提供外部的输入,那怎么去和虚拟世界发生交互呢?显然,只能通过一个Gaze。

但是,触发事件的方式可以是多种多样的,可以是超过一定时间触发,也可以是手柄触发,等等。


Cardboard实现Gaze的方式不能说简单,但是对于熟悉Unity的同学来说也并不很复杂,在介绍Gaze的基础上,我们需要知道Unity的事件系统。



首先介绍的是BaseInputModule这个类,它是Unity当中所有输入模块的基类,而且是Unity事件系统的组件,我们见的比较多的比如说TouchInputModule等都继承自它,平时我们用的会比较少因为手游的话,一些已有的模块已经足够我们使用,但是在这里,我们还需要外部的拓展模块来提供输入,所以需要将新建的类继承自它。

BaseInputModule当中有不少的方法需要去重写,我们重写的函数大概有这些:

ShouldActivateModule()
DeactivateModule()
IsPointerOverGameObject(int pointerId)
Process()

每一个函数有什么作用我们后面再介绍。


PointerEventData这个类存储着每一次点击事件的信息,它存储着一系列变量,譬如变量button(每一次点击button时触发),click count在触发时点击的次数,click time两次点击的时间间隔,position存储点击位置等...具体就不介绍了,我们只需要知道PointerEventData这个类支持InputModel,它给每一个Input提供数据。


除此之外,触发事件当然少不了Raycast,Unity的射线系统是每一个使用过Unity的人都会烂熟于心的,射线将我们在屏幕上的touch对应到3D场景中的GameObject,所以只要涉及到了物体触发事件,就必然有Raycast的过程(甚至GUI的事件也会用射线)。

/******************************************************************************************************************************************/

using UnityEngine;
using UnityEngine.EventSystems;

public class GazeInputModule : BaseInputModule {
  [Tooltip("Whether gaze input is active in VR Mode only (true), or all the time (false).")]
  public bool vrModeOnly = false;

  [Tooltip("Optional object to place at raycast intersections as a 3D cursor. " +
           "Be sure it is on a layer that raycasts will ignore.")]
  public GameObject cursor;

  // Time in seconds between the pointer down and up events sent by a magnet click.
  // Allows time for the UI elements to make their state transitions.
  [HideInInspector]
  public float clickTime = 0.1f;  // Based on default time for a button to animate to Pressed.

  // The pixel through which to cast rays, in viewport coordinates.  Generally, the center
  // pixel is best, assuming a monoscopic camera is selected as the Canvas' event camera.
  [HideInInspector]
  public Vector2 hotspot = new Vector2(0.5f, 0.5f);

  private PointerEventData pointerData;

  public override bool ShouldActivateModule() {
    if (!base.ShouldActivateModule()) {
      return false;
    }
    return Cardboard.SDK.VRModeEnabled || !vrModeOnly;
  }

  public override void DeactivateModule() {
    base.DeactivateModule();
    if (pointerData != null) {
      HandlePendingClick();
      HandlePointerExitAndEnter(pointerData, null);
      pointerData = null;
    }
    eventSystem.SetSelectedGameObject(null, GetBaseEventData());
    if (cursor != null) {
      cursor.SetActive(false);
    }
  }

  public override bool IsPointerOverGameObject(int pointerId) {
    return pointerData != null && pointerData.pointerEnter != null;
  }

  public override void Process() {
    CastRayFromGaze();
    UpdateCurrentObject();
    PlaceCursor();
    HandlePendingClick();
    HandleTrigger();
  }

  private void CastRayFromGaze() {
    if (pointerData == null) {
      pointerData = new PointerEventData(eventSystem);
    }
    pointerData.Reset();
    pointerData.position = new Vector2(hotspot.x * Screen.width, hotspot.y * Screen.height);
    eventSystem.RaycastAll(pointerData, m_RaycastResultCache);
    pointerData.pointerCurrentRaycast = FindFirstRaycast(m_RaycastResultCache);
    m_RaycastResultCache.Clear();
  }

  private void UpdateCurrentObject() {
    // Send enter events and update the highlight.
    var go = pointerData.pointerCurrentRaycast.gameObject;
    HandlePointerExitAndEnter(pointerData, go);
    // Update the current selection, or clear if it is no longer the current object.
    var selected = ExecuteEvents.GetEventHandler<ISelectHandler>(go);
    if (selected == eventSystem.currentSelectedGameObject) {
      ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, GetBaseEventData(),
                            ExecuteEvents.updateSelectedHandler);
    }
    else {
      eventSystem.SetSelectedGameObject(null, pointerData);
    }
  }

  private void PlaceCursor() {
    if (cursor == null)
      return;
    var go = pointerData.pointerCurrentRaycast.gameObject;
    cursor.SetActive(go != null);
    if (cursor.activeInHierarchy) {
      Camera cam = pointerData.enterEventCamera;
      // Note: rays through screen start at near clipping plane.
      float dist = pointerData.pointerCurrentRaycast.distance + cam.nearClipPlane;
      cursor.transform.position = cam.transform.position + cam.transform.forward * dist;
    }
  }

  private void HandlePendingClick() {
    if (!pointerData.eligibleForClick || !Cardboard.SDK.Triggered
        && Time.unscaledTime - pointerData.clickTime < clickTime) {
      return;
    }

    // Send pointer up and click events.
    ExecuteEvents.Execute(pointerData.pointerPress, pointerData, ExecuteEvents.pointerUpHandler);
    ExecuteEvents.Execute(pointerData.pointerPress, pointerData, ExecuteEvents.pointerClickHandler);

    // Clear the click state.
    pointerData.pointerPress = null;
    pointerData.rawPointerPress = null;
    pointerData.eligibleForClick = false;
    pointerData.clickCount = 0;
  }

  private void HandleTrigger() {
    if (!Cardboard.SDK.Triggered) {
      return;
    }
    var go = pointerData.pointerCurrentRaycast.gameObject;

    // Send pointer down event.
    pointerData.pressPosition = pointerData.position;
    pointerData.pointerPressRaycast = pointerData.pointerCurrentRaycast;
    pointerData.pointerPress =
        ExecuteEvents.ExecuteHierarchy(go, pointerData, ExecuteEvents.pointerDownHandler)
        ?? ExecuteEvents.GetEventHandler<IPointerClickHandler>(go);

    // Save the pending click state.
    pointerData.rawPointerPress = go;
    pointerData.eligibleForClick = true;
    pointerData.clickCount = 1;
    pointerData.clickTime = Time.unscaledTime;
  }
}

脚本开始还是注释了一些供用户使用的量:

public bool vrModeOnly = false;

//"Whether gaze input is active in VR Mode only (true), or all the time (false)."

gaze的输入是否在非VR模式当中有效,默认是false,即在非VR模式当中gaze也是有效的。

public GameObject cursor;

//Optional object to place at raycast intersections as a 3D cursor.Be sure it is on a layer that raycasts will ignore.

在射线交汇点放置的可见3D物体,注意射线在某一层是否会碰撞。这个东西是一个object,也就是我们能看到它。


在Inspector界面中,看到这是一个圆柱体,颜色是黄色,这个就是在gaze停留在控件上时,显示的样式。

我们把gaze停留在正方体上,发现中央出现了一个小黄点,GazePointer的active属性处于enable的状态,也就是物体被激活,我们可以看见它。


同样gaze停留在地面上时,也是可见的。


但是当gaze停留在天空盒处时(这里没有天空盒,相当于蓝色的背景),物体没有激活,是不可见的


这就是Cardboard当中gaze的大概使用方法。


OK,简要的介绍做完了,再让我们回到脚本。

我们又定义了两个非用户级的变量:

public float clickTime = 0.1f;在点击事件中控制点击时间

public Vector2 hotspot = new Vector2(0.5f, 0.5f);这个变量表示我们的gaze在viewport上的位置,默认是在屏幕的中心


接下来是函数ShouldActivateModule()的重写,此函数用于判断该输入模块是否被激活,我们需要引入变量vrModeOnly用以判断是否模块应该激活

函数DeactivateModule()的重写,此模块被关闭时调用,之所以要重写,是因为我们要在关闭Gaze的时候依然需要能触发点击事件

函数IsPointerOverGameObject(int pointerId)的重写,用来判断point的位置是否在含有eventsystem组件的物体上

函数Process()的重写,此函数是核心函数,它每一帧都会被执行。接下来我们重点分析一下这个函数。

  public override void Process() {
    CastRayFromGaze();
    UpdateCurrentObject();
    PlaceCursor();
    HandlePendingClick();
    HandleTrigger();
  }

第一步CastRayFromGaze();所做的操作是实例化了一个PointerEventData型的对象,然后我们将position属性赋值,在当前情形下,点击的position就是屏幕的中心点

pointerData.position = new Vector2(hotspot.x * Screen.width, hotspot.y * Screen.height);

然后进行Raycasr,返回了与之碰撞的n个物体,存在一个List中,这里我们只需要第一个碰撞的物体

pointerData.pointerCurrentRaycast = FindFirstRaycast(m_RaycastResultCache);


第二步UpdateCurrentObject();得到射线碰撞物体

var go = pointerData.pointerCurrentRaycast.gameObject;

后面的操作实现了gaze进出物体时,物体能检测到并且做出相应的变化,如我们的demo中就是gaze的进出会带来正方体颜色的变化


第三步PlaceCursor();就是要放置我们的黄色圆点了,具体放在什么位置脚本用了如下的方法

Camera cam = pointerData.enterEventCamera;
float dist = pointerData.pointerCurrentRaycast.distance + cam.nearClipPlane;
cursor.transform.position = cam.transform.position + cam.transform.forward * dist;

就是通过camera的近裁面距离与到碰撞点的距离求和,来获得camera到positon的距离。这种方法在此环境下确实很方便。


第四步HandlePendingClick();HandleTrigger();就是处理点击事件了,只是对PointerEventData数据赋值,然后进行Raycast,其过程并不复杂。







你可能感兴趣的:(Android开发,unity3d,sdk,手游,事件系统)