简体中文 | English

English

本教程示范如何在大朋M2、E3、P1等设备中使用大朋Unity 插件SDK,让Unity的Canvas响应头盔的虚拟Cursor(头控光标)。

前言

与传统游戏不同,VR游戏中常常使用位于屏幕中央的准心来完成与GUI的交互。Unity的Canvas是目前使用最广泛的GUI系统,但由于该系统是为传统手游设计的,所以许多开发者或多或少地遇到了交互的难题。

在VR游戏中,我们推荐使用WorldSpace的Canvas,将整个GUI融入到游戏中。而ScreenSpace的GUI则会受双目视觉(瞳距)的影响,GUI会有叠影。在此我们仅讨论WorldSpace的Canvas。

教程

教程基于插件DpnUnity 0.7.2c开发。只做一个简单的讲解,更详细的部分可参考插件中的场景GazeCursor。

教程仅适用于E3,P1等头控设备的Cursor

下载,解压,并打开Unity(Unity5.3.0)项目TestDpnGraphicRaycaster2,里面有一个叫做“TestDpnGraphicRaycaster”的(Assets-DPN-TestDpnGraphicRaycaster)场景,打开这个场景。

1. 场景介绍:

图(1)

a) Canvas:Render Mode为WorldSpace,固定在3D空间中,不会移动。下面挂了4个用于与Cursor交互的Button。当与Cursor交互时,会有颜色变化。

b) DpnCameraRig:下面挂了3个Camera(LeftEyeAnchor, RightEyeAnchor,CenterEyeAnchor), 这些Camera会随着头戴设备的移动而移动。在CenterEyeAnchor下挂了一个Cursor(十字架形状),用于标示准心的位置。

2. 系统设置:

Menu-DPVR-Setting

为了不造成冲突,Peripheral Support设置为None,不使用外设。(图2)

点击apply。

图(2)

3. 脚本:

为了Button与Cursor交互,我们在Canvas下面挂一个新的Component “DpnGraphicRaycaster”,用于取代Unity的GraphicRaycaster。所以需要禁用Unity的“GraphicRaycaster”,如图(3)去掉钩子或者直接删掉这个Component。然后我们将CenterEyeAnchor拖到DpnGraphicRaycaster的Interact Camera。这样所有的交互都是这个Interact Camera和Canvas之间的交互。

图(3)

4. 结果:

完成以上步骤之后再生成并运行,就可以看到Cursor能正常和这些Button交互了。Button可以接收到OnMouseEnter,OnMouseOver和OnMouseExit事件。运行效果如下(图4)

图(4)


代码解析

整个代码实际上非常简单,100行左右完成了所有的工作。主要是DpnGraphicRaycaster的Raycast和Update函数。

我们继承了Unity标准的GraphicRaycaster类,用来完成Canvas的碰撞检测。

public class DpnGraphicRaycaster : GraphicRaycaster

重载Raycast函数

这个函数原本用于鼠标或者触摸屏与Canvas的碰撞检测。但是在VR应用中,则是使用了Cursor,所以我们需要重载这个函数。此外做曲面Canvas的时候,也需要重载这个函数。

它先从Canvas获得所有Graphic的引用。

IList<Graphic> list = GraphicRegistry.GetGraphicsForCanvas(_canvas);

然后是矩形和射线的求交算法,判断射线是否与这个矩形碰撞。

Vector3[] corners = new Vector3[6];
g.rectTransform.GetWorldCorners(corners);
corners[4] = corners[0];
corners[5] = corners[1];
 
Plane rect_plane = new Plane();
rect_plane.Set3Points(corners[0], corners[1], corners[2]);
 
float enter;
if (false == rect_plane.Raycast(ray, out enter)) continue;
 
Vector3 intersection = ray.GetPoint(enter);
bool is_inside = true;
for (int j = 0; j < 4; ++j)
{
    Vector3 b = corners[j + 1];
    Vector3 ba = corners[j + 0] - b;
    Vector3 bc = corners[j + 2] - b;
    Vector3 bp = intersection - b;
 
    Vector3 cross_ba_bp = Vector3.Cross(ba, bp);
    Vector3 cross_bp_bc = Vector3.Cross(bp, bc);
 
    if (Vector3.Dot(cross_ba_bp, cross_bp_bc) < 0)
    {
        is_inside = false;
        break;
    }
}
 
if (false == is_inside) continue;

接着将碰撞的Graphic排序,放入返回列表。

sorted_graphic.Sort((g1, g2) => g2.graph.depth.CompareTo(g1.graph.depth));
 
for (int i = 0; i < sorted_graphic.Count; ++i)
{
    var castResult = new RaycastResult
    {
        gameObject = sorted_graphic[i].graph.gameObject,
        module = this,
        distance = (sorted_graphic[i].worldPos - ray.origin).magnitude,
        index = resultAppendList.Count,
        depth = sorted_graphic[i].graph.depth,
        worldPosition = sorted_graphic[i].worldPos,
    };
    resultAppendList.Add(castResult);
}

至此,已经可以看到Cursor进入、移出、点击这些Graphic时,它会在外观上有反应。这是Unity3D内部完成的工作。

重载Update函数

前面已经实现了Raycast函数,并且Graphic的外观会随着Cursor而发生变化。但是这些Graphic并不会响应OnMouseEnter、OnMouseOver和OnMouseExit事件。于是我们需要另外重载Update函数。

这个函数更加简单,首先利用Raycast函数获得碰撞的Graphic List。

// raycast
List<RaycastResult> list = new List<RaycastResult>();
Raycast(null, list);

然后比较上一帧与这一帧的List,查找哪些Graphic是这一帧新添的,就发送“OnMouseEnter”消息和“OnMouseOver消息”。哪些是最近两帧都有的,那么就只发送“OnMouseOver”消息。

// enter and over
foreach (GameObject co in cur_objects)
{
    if (false == _last_objects.Contains(co))
    {
        co.SendMessage("OnMouseEnter");
    }
    co.SendMessage("OnMouseOver");
}

而上一帧有,这一帧没有的Graphic,则发送“OnMouseExit”消息。

// exit
foreach (GameObject lo in _last_objects)
{
    if (false == cur_objects.Contains(lo))
    {
        lo.SendMessage("OnMouseExit");
    }
}

至此,就可以用Cursor模拟出鼠标的消息了。

我们可以在那些Graphic挂一个脚本来检查是否能收到这些消息。

public class OnMouseEvents : MonoBehaviour
{
    void Start()
    {
        Debug.Log("UGUI: Start " + transform.name);
    }
    void OnMouseEnter()
    {
        Debug.Log("UGUI: OnMouseEnter " + transform.name);
    }
    void OnMouseOver()
    {
        Debug.Log("UGUI: OnMouseOver " + transform.name);
    }
    void OnMouseExit()
    {
        Debug.Log("UGUI: OnMouseExit " + transform.name);
    }
}


Copyright © dpvr.cn, All Rights Reserved 沪ICP备15019466号-2 (Shanghai) Shanghai Lexiang Technology Co., Ltd