专题介绍:
暑期着手开发第一个多功能的MR项目,这个系列的文章是我学习Quest MR开发的笔记,希望给小伙伴们提供一些参考经验。如果想要继续交流或者有合作意象,请通过以下方式联系:
- vx:always_lelele;b站:我是一直LE;E-mail:XieclAbby@163.com;
文章更新时间:2023.7.28
系列文章
-
第一篇:Quest X MR应用开发 | 基本环境+开始搭建_Le0829的博客-CSDN博客
-
第二篇:Quest X MR应用开发 | 创建一个Git仓库来管理Unity项目_Le0829的博客-CSDN博客
-
第三篇:Quest X MR应用开发 | 如何开启Quest透视功能_Le0829的博客-CSDN博客
开发环境配置
电脑OS:Win11
VR设备:Quest 2 (未来计划移植Quest 3)
开发平台及版本:Unity 2022.3.3f1c1
参考资料:
https://developer.oculus.com/documentation/unity/unity-scene-overview/
https://github.com/dilmerv/MetaAdvancedFeatures
引言
简单的介绍一下MR体验中一个非常重要的技术:场景感知。
它分为场景捕捉+场景理解两个过程。
场景捕捉是利用传感器设备对周边的环境进行角落、边缘和移动区域等视觉特征的视觉识别,并实现数据的存储和呈现。例如,在第三篇中的透视技术,头盔对环境进行扫描后可以在在显示屏中看到场景物体的描边(之后我会写一篇文章来进一步介绍如何利用透视功能制作风格化场景)。
场景理解就是利用传感器设备对周边的环境进行捕捉后分析的过程。这个分析就是对场景数据利用语义分割技术来对场景元素进行空间定位、语义标注和语义提取的过程。语义简单来说就是Tag,比如桌面上一个苹果的tag可以是apple。通过场景理解,就可以进一步的利用环境信息做更深度的开发。例如,可以实现自动化的对周边环境的三维扫描,并它们的位置信息、虚拟形象都呈现在显示器上。
相比Hololens2,Quest的场景感知就比较的鸡肋了。Quest可以进行场景捕捉,但是它的场景理解是需要人工标注的,而不是理想状态中的机器自动标注。要启用Quest的场景功能,你需要对房间环境自行用手柄射线描边,然后自行选择标签来进行标注。通过这一步,就可以实现下图这种房间中有很多不同标签的立方体区域的效果,可以说是很鸡肋😂不过,据说Quest3可以实现Hololens2级别的场景感知功能,就是所有过程都自动化实现。

虽然这个手动扫描的过程还是挺麻烦的,但是它至少可以实现物理空间环境信息和虚拟空间环境信息的一个融合,来创造一个MR空间,因此值得去学习和探索一下。
这两天刚刚升级了我手中的Quest2,终于可以兼容Oculus的场景感知功能了🤩我自己做了一个小demo来尝试MR体验,目标功能:
- 将房间数据同步至Unity中
- 利用按键实现透视模式的开关
- 利用手部射线对房间虚拟摆件的开关
- 放置一个虚拟NPC,并且通过手势来跟它互动
本篇文章将通过这个demo来介绍一下如何制作一个MR小应用。
正文
基础搭建
创建一个新场景命名为demo,删除里面的所有元素。因为涉及到一些基础的交互功能,如射线和手势等,为了减少搭建时间(是的做到后面我才意识到这一点),咱们直接导航到Assets>Oculus>Interaction>Samples>Scenes>Examples找到PoseExample这个场景,拖拽到层级面板中。然后按下Ctrl选中OVRCameraRig和Poses,添加到demo场景里,再移除掉示例场景。

这么做是因为后面需要通过和NPC进行手势交互,因此直接Poses场景里配置好的预制体来进行设置(如果不这样的话,后面需要很麻烦的搭建步骤,计划后面写几篇关于手势交互的文章进行介绍)。
之后为我们的OVR相机添加射线控制器,实现射线交互功能。在搜索框里搜索RayInteractor,然后展开OVRCameraRig下的OVRInteraction(用来管理交互功能的预制体),把这个代表手柄射线的ControllerRayInteractor和HandRayInteractor射线控制器prefab分别添加到左右手的ControllerInteractors和HandInteractors下面作为子物体。

为了能够操控射线进行选择,需要让射线控制器和头盔进行关联。找到ControllerInteractors的Best Hover Interactor Group(哈哈哈之前的SDK里不是这么叫的,meta加个Best是说很🐂🍺的意思吗),点击下面的+号,把刚刚的添加的子物体ControllerRayInteractor拖到列表栏里替换调自动补充的物体。保证左右手操作一致。
左右手部的射线控制器配置方式也是一样的:这里以左手为例
进行了以上操作后,就实现了和射线控制器的数据绑定,可以进行射线的操作了。
在进行第一个功能开发之前,我们还需要做一件事,就是设置OVRCameraRig,让它可以实现透视,在Quest Features中需要勾选上Enable Passthrough,然后还需要给OVRCameraRig添加OVRPassthrough Layer组件,并把Projection Surface改成User Defined。详情见本专题系列的第三篇文章。
还需要更改CenterEyeAnchor的Camera组件中Background为纯黑。
功能1:场景模型加载
搜索框里搜索OVRSceneManager,选择那个名字里带Variant的物体,拖拽到场景里。OVRSceneManager是实现场景感知基础组件之一,它可以实现对头盔保存的场景模型的访问、加载和管理。
它有两个重要的脚本,一个是OVRSceneManager.cs另一个是MyCustomSceneModelLoader.cs:
- OVRSceneManager可以访问标注好的场景锚点数据,并通过LoadSceneModel()来加载场景模型,以及返回事件加载结果(成功加载或不成功加载)。通过OVRSceneManager还可以指定访问和加载特定类型的场景锚点,通过 RequestSceneCapture(),传递语义标签的字符串数据进行处理。但这个比较恶心的是,它在初始必须要求你设置好房间数据,如果你之前没有设置的话,必须退出程序进行设置,再开始体验,尤其是直接用USB串流运行项目的时候,会一致弹窗报错。
- MyCustomSceneModelLoader 继承自OVRSceneModelLoader,它提供了DelayedLoad()的协程方法来延迟加载场景模型,可以优化程序性能,避免多个任务同时执行产生冲突。
下面,设置OVRSceneManager组件,导航到Assets>Oculus>SampleFramework>Usage>SceneManager>Prefabs,找到SpawnablesPlane和SpawnablesVolume预制体,拷贝它们创建一个变体。打开旁边的FurniturePrefabs文件夹,将所有的预制体再复制出来一份变体。此时再去Assets>Resources文件夹里场景一个新文件夹取名为Prefabs,把刚刚创建的所有变体都移动到这个文件夹内进行管理。
把刚刚创建的SpawnablesPlane和SpawnablesVolume预制体的变体分关联到OVRSceneManager组件中的PlanePrefab和VolumePrefab中。
这两个预制体都带有OVRSceneAnchor和FurnitureSpawner脚本,它为空间场景创建锚点,并让家居预制体加载在对应的锚点上。
对这两个变体进行设置:把FurnitureSpawner组件中所有的Realizable Prefab都替换成我们刚刚创建的虚拟家居变体。
这里补充一下Oculus里对场景理解的处理过程:
- 在SDK里还有一个叫Volume And Plane Switcher的组件和这个FurnitureSpawner组件有一个相似功能,就是对场景物体进行语义分类。Volume And Plane Switcher的功能是通过手动数据标注将平面与 2D 表面以及体积与 3D 对象正确对齐。
- 下面是Oculus SDK里涵盖的基础语义示例。不过目前的限制就是没办法自定义一些语义来进行标注。
通过上述步骤就可以实现场景模型成功加载到我们的虚拟空间中了。
功能2:透视模式的控制
创建一个脚本叫RoomPassthroughSwitcher,双击打开VS编辑。
using UnityEngine;
public class RoomPassthroughSwitcher : MonoBehaviour
{
Camera eyeCamera;
public OVRInput.Button passthroughBtn;
OVRPassthroughLayer passthroughLayer;
[HideInInspector] public bool enablePassthrough = true;
private void Start()
{
eyeCamera=Camera.main;
eyeCamera.clearFlags=CameraClearFlags.SolidColor;
GameObject ovrCameraRig = GameObject.Find("OVRCameraRig");
if (ovrCameraRig == null)
{
Debug.LogError("Scene does not contain an OVRCameraRig");
return;
}
passthroughLayer = ovrCameraRig.GetComponent<OVRPassthroughLayer>();
if (passthroughLayer == null)
{
Debug.LogError("OVRCameraRig does not contain an OVRPassthroughLayer component");
}
private void Update()
{
if (OVRInput.GetDown(passthroughBtn))
{
enablePassthrough = !enablePassthrough;
passthroughLayer.enabled = enablePassthrough;
}
eyeCamera.clearFlags = (enablePassthrough) ? CameraClearFlags.SolidColor : CameraClearFlags.Skybox;
}
}
}
在上述脚本中,实现了在初始时候找到OVR相机中的OVR透视层脚本,然后在更新时通过检测按键是否按下来控制透视脚本的enable属性,并同时实现在透视时候将主相机的视野背景设置为纯黑色,在关闭透视时候设置为天空盒。这里关键是反转标志位enablePassthrough布尔变量的值,就是通过enablePassthrough = !enablePassthrough;在每次按键的时候进行反转。
这个控制按键可以自定义,我使用的是右手手柄的A键,对应在Inspector面板中映射的是One选项。
- 关于按键命名的对应关系,可以参考下面这几张maps:



功能3:场景模型的交互
下面,我们将使用射线+按键来控制虚拟家居模型的隐藏和显示——射线投射到模型上时候按下前边的扳机键可以控制模型
在Oculus SDK里实现射线交互不仅需要有Ray Interactor物体,还需要对应的Ray Interactable物体以及事件处理脚本等:
- 射线控制器会找寻场景里所有带有RayInteractble.cs脚本的物体,实现射线的准确投射。除此之外,Oculus SDK里有一个叫Interactable Unity Event Wrapper的脚本,它专门用来配合处理可交互物体关联的事件。因此下面我们需要对所有的家居物体添加上Ray Interactble.cs和Interactable Unity Event Wrapper.cs两个脚本。
- 我们还需要添加Collider Surface和Box Collider/Mesh Collider来实现射线碰撞检测。
- 最后,绑定我们自定义的脚本RoomPassthroughSwitcher。
添加完成后,需要进行相关参数的配置:
- RoomPassthroughSwitcher上选择对应的按键。
- 将Ray Interactble拖到Interactable Unity Event Wrapper中的Interactable View里来指定事件的响应物体。
- 把Box Collider配置到Collider Surface组件中的Collider一栏里
下面我们将继续编写RoomPassthroughSwitcher脚本来实现目标功能。
using UnityEngine;
public class RoomPassthroughSwitcher : MonoBehaviour
{
Camera eyeCamera;
public OVRInput.Button passthroughBtn;
OVRPassthroughLayer passthroughLayer;
MeshRenderer meshRenderer;
[HideInInspector]public bool isHide = true;
[HideInInspector] public bool enablePassthrough = true;
private void Start()
{
eyeCamera=Camera.main;
eyeCamera.clearFlags=CameraClearFlags.SolidColor;
GameObject ovrCameraRig = GameObject.Find("OVRCameraRig");
if (ovrCameraRig == null)
{
Debug.LogError("Scene does not contain an OVRCameraRig");
return;
}
passthroughLayer = ovrCameraRig.GetComponent<OVRPassthroughLayer>();
if (passthroughLayer == null)
{
Debug.LogError("OVRCameraRig does not contain an OVRPassthroughLayer component");
}
meshRenderer = GetComponent<MeshRenderer>();
passthroughLayer.AddSurfaceGeometry(gameObject);
meshRenderer.enabled = false;
}
private void Update()
{
if (OVRInput.GetDown(passthroughBtn))
{
enablePassthrough = !enablePassthrough;
passthroughLayer.enabled = enablePassthrough;
}
eyeCamera.clearFlags = (enablePassthrough) ? CameraClearFlags.SolidColor : CameraClearFlags.Skybox;
}
public void ToggleTheMesh()
{
if (isHide)
{
passthroughLayer.RemoveSurfaceGeometry(gameObject);
meshRenderer.enabled = true;
}
else
{
passthroughLayer.AddSurfaceGeometry(gameObject);
meshRenderer.enabled = false;
}
isHide = !isHide;
}
}
在脚本里,我们又添加了网格渲染器meshRenderer、标志位isHide两个变量。在Start函数里将meshRenderer绑定到模型身上MeshRenderer的组件,并禁用掉它,实现的效果就是一开始看不见任何的模型,是完全透视的;还需要调用OVRPassthroughLayer.AddSurfaceGeometry(GameObject)函数实现透视层的注册,让指定的家居模型呈现通透状态,这也是为什么一开始需要把Projection Surface改成User Defined的原因。之后我们使用ToggleTheMesh()函数,在其中通过isHide的布尔值控制网格渲染器的开关和透视层的添加或移除。
接下来,我们需要手动注册射线交互的事件。在Interactable Unity Event Wrapper组件中找到When Select(),点击下面的加号,将RoomPassthroughSwitcher组件拖拽到左边的框里,右边的下拉菜单找到RoomPassthroughSwitcher脚本,选中后在展开的选项菜单中选择ToggleTheMesh。
请注意,上述所有的步骤对每一个物体都是同样的操作。
功能4:用手势与NPC进行交互
导入我们用的模型资产包后,首先进行场景的搭建。
这里我设计的小功能是,通过按下手柄上的摇杆键对NPC角色实现切换,通过做出不同的手势来让NPC做不同的动作。
首先,我们需要创建脚本来管理所有的NPC控制。新建一个脚本,取名叫CatController,然后双击进入VS进行编辑。
using UnityEngine;
public class CatController : MonoBehaviour
{
public OVRInput.Button switcher;
public GameObject[] character;
public int index=0;
Animator animator;
void Awake()
{
foreach (GameObject c in character)
{
c.SetActive(false);
}
character[0].SetActive(true);
SetAnimation("Idle");
}
// Update is called once per frame
void Update()
{
//按键控制切换角色
if (OVRInput.GetDown(switcher))
{
character[index].SetActive(false);
index++;
index %= character.Length;
character[index].SetActive(true);
}
}
public void SetAnimation(string type)
{
animator = character[index].GetComponent<Animator>();
string Type = type;
switch (Type)
{
case "Idle":
animator.SetInteger("animation",1);
break;
case "Hi":
animator.SetInteger("animation", 13);
break;
case "Cute":
animator.SetInteger("animation", 10);
break;
case "Walk":
animator.SetInteger("animation", 21);
break;
case "Worship":
animator.SetInteger("animation", 27);
break;
}
}
}
这个脚本里,我们创建了一个储存所有的NPC预制体的列表,然后在初始化之前只激活列表里的第一个NPC,并让它做Idle状态的动作,之后再Update中我们通过按键控制npc按列表顺序切换。这里SetAnimation()函数是实现我们输入相应的状态名称,然后调用这个函数匹配不同的状态值,去设置不同的动画播放。
场景里新建一个空物体叫ToyManager,把这个脚本绑定到它身上。在Switcher中选择Secondary Thumbstick Right;在Character左边的数字栏输入10,把所有的npc物体手动拖拽到列表里注册。
接下来,需要完成手势交互的设置。
展开层级面板中的Poses预制体,点击每一个动作名称。在Inspector面板中找到Selector Unity Event Wrapper组件,手动注册When Selected()和When Unselected()的处理事件:点击右下角+,将ToyManager物体拖进左边方框,右边设置为CatController.SetAnimation,并输入对应脚本中的字符串变量名称。
这里我简单的配置了两个动作。一个是竖起大拇指的时候让npc作出Worship(对我进行跪拜)的动作,我招手的时候作出Hi(招手手)的动作,我没做任何动作的时候让它保持Idle(舞动身体)状态。上面我仅展示了左手的配置作为示例。
运行效果
下面按下Ctrl+S保存我们的场景。请确保头显Link电脑,并且已经做好了房间设置。点击Play按钮,运行场景。