Unity 中的空间映射

使用空间映射可以检索代表 HoloLens 设备周围世界表面的三角形网格。 你可以使用位置、遮挡和房间分析的表面数据,增强 Unity 项目的浸入式体验。

Unity 包含对空间映射的完全支持,可通过以下方式向开发人员公开:

  1. MixedRealityToolkit 中提供了空间映射组件,为空间映射入门提供了方便快捷路径
  2. 较低级别的空间映射 API,它提供完全控制并实现更复杂的特定于应用程序的自定义

若要在应用中使用空间映射,需要在 AppxManifest 中设置 SpatialPerception 功能。

设备支持

功能 HoloLens(第一代) HoloLens 2 沉浸式头戴显示设备
空间映射

设置 SpatialPerception 功能

为了使应用能够使用空间映射数据,必须启用 SpatialPerception 功能。

如何启用 SpatialPerception 功能:

  1. 在 Unity 编辑器中,打开“播放器设置”窗格(“编辑”>“项目设置”>“播放器”)
  2. 选择“Microsoft Store”选项卡
  3. 展开“发布设置”并勾选“功能”列表中的“SpatialPerception”功能

注意

如果已将 Unity 项目导出到 Visual Studio 解决方案,则需要导出到新文件夹或在 Visual Studio 的 AppxManifest 中手动设置此功能

空间映射还需要至少 10.0.10586.0 的 MaxVersionTested:

  1. 在 Visual Studio 的解决方案资源管理器中,右键单击“Package.appxmanifest”项目,然后选择“查看代码
  2. 查找指定 TargetDeviceFamily 的行,并将 MaxVersionTested="10.0.10240.0" 更改为 MaxVersionTested="10.0.10586.0"
  3. 保存 Package.appxmanifest

如何在 Unity 中添加映射

空间感知系统

在 MRTK 中,查看空间感知入门指南,了解有关设置各种空间网格观察程序的信息。

有关设备观察程序的信息,请参阅为设备配置网格观察程序指南。

有关场景理解观察程序的信息,请查看场景理解观察程序指南。

更高级别的网格分析:空间理解

注意

空间理解已弃用,取而代之的是场景理解

MixedRealityToolkit 是一系列实用工具代码,适用于基于 Unity 的全息 API 构建的全息开发。

空间理解

在物理世界中放置全息影像时,通常需要超越空间映射的网格和平面。 按程序放置完成后,需要更高级别的环境理解。 这通常需要作出有关楼层、天花板和墙壁的决策。 还可以根据一组放置约束进行优化,以确定全息对象的最佳物理位置。

在 Young Conker 和 Fragments 的开发过程中,Asobo Studios 通过开发房间求解器来直面这一问题。 每个游戏都有特定于游戏的需求,但它们共享核心空间理解技术。 HoloToolkit.SpatialUnderstanding 库封装了此技术,使你可以快速找到墙上的空白区域、将对象放置在天花板上、识别角色坐的位置以及大量其他空间理解查询。

其中包含了所有源代码,你可以根据需要对其进行自定义并与社区分享你的改进意见。 C++ 求解器的代码已封装到 UWP DLL 中,并通过包含在 MixedRealityToolkit 中的嵌入式预制件公开给 Unity。

理解模块

模块公开了三个主要接口:简单表面和空间查询拓扑、用于对象检测的形状,以及用于基于约束的对象集放置的对象放置求解器。 下面介绍上述每种方式。 除了三个主要查询之外,还有一个光线投射接口,可以使用它来检索标记的表面类型,并可以复制出自定义的密封游戏空间网格。

光线投射

房间扫描完成后,将在内部为表面(如地面、天花板和墙)生成标签。 PlayspaceRaycast 函数接收一条光线,并返回该光线是否与已知表面发生碰撞的结果,如果发生碰撞,则以 RaycastResult 的形式返回有关该表面的信息。

struct RaycastResult
{
    enum SurfaceTypes
    {
        Invalid,    // No intersection
        Other,
        Floor,
        FloorLike,  // Not part of the floor topology,
                    //  but close to the floor and looks like the floor
        Platform,   // Horizontal platform between the ground and
                    //  the ceiling
        Ceiling,
        WallExternal,
        WallLike,   // Not part of the external wall surface,
                    //  but vertical surface that looks like a
                    //  wall structure
    };
    SurfaceTypes SurfaceType;
    float SurfaceArea;  // Zero if unknown
                        //  (i.e. if not part of the topology analysis)
    DirectX::XMFLOAT3 IntersectPoint;
    DirectX::XMFLOAT3 IntersectNormal;
};

在内部,光线投射的计算依据是游戏空间的计算出的 8 立方厘米体素表示形式。 每个体素包含一组表面元(简称面元),其中包含已处理的拓扑数据。 将比较相交体素单元中包含的面元,最佳匹配项用于查找拓扑信息。 此拓扑数据包含以“SurfaceTypes”枚举形式返回的标签,以及相交表面的表面积。

在 Unity 示例中,游标每帧投射一条光线。 首先,针对 Unity 的碰撞器。 其次,针对理解模块的世界表示形式。 最后针对 UI 元素。 在此应用中,优先级从高到低分别为 UI、理解结果和 Unity 碰撞器。 SurfaceType 报告为游标旁边的文本。

Surface type is labeled next to the cursor
表面类型标记在游标旁边

拓扑查询

在 DLL 中,拓扑管理器处理环境的标记。 如上所述,大部分数据存储在体素体中包含的面元内。 此外,“PlaySpaceInfos”结构用于存储有关游戏空间的信息,包括世界对齐(下文提供了更多详细信息)、地面和天花板高度。 试探法用于确定地面、天花板和墙壁。 例如,表面积大于 1 平方米的最大和最低水平面被视为地面。

注意

在扫描期间使用的相机路径也在此过程中使用。

拓扑管理器公开的一部分查询通过 DLL 公开。 公开的拓扑查询如下。

QueryTopology_FindPositionsOnWalls
QueryTopology_FindLargePositionsOnWalls
QueryTopology_FindLargestWall
QueryTopology_FindPositionsOnFloor
QueryTopology_FindLargestPositionsOnFloor
QueryTopology_FindPositionsSittable

每个查询都有一组特定于查询类型的参数。 在以下示例中,用户指定所需立体的最小高度和宽度、地面上方的最小放置高度,以及立体前面的最小净空量。 所有测量值均以米为单位。

EXTERN_C __declspec(dllexport) int QueryTopology_FindPositionsOnWalls(
    _In_ float minHeightOfWallSpace,
    _In_ float minWidthOfWallSpace,
    _In_ float minHeightAboveFloor,
    _In_ float minFacingClearance,
    _In_ int locationCount,
    _Inout_ Dll_Interface::TopologyResult* locationData)

其中每个查询采用预分配的“TopologyResult”结构数组。 “locationCount”参数指定传入的数组的长度。 返回值报告返回的位置数。 此数字永远不会大于传入的“locationCount”参数。

“TopologyResult”包含返回的立体的中心位置、朝向(即法线)以及找到的空间的尺寸。

struct TopologyResult
{
    DirectX::XMFLOAT3 position;
    DirectX::XMFLOAT3 normal;
    float width;
    float length;
};

注意

在 Unity 示例中,其中每个查询已链接到虚拟 UI 面板中的某个按钮。 该示例将其中每个查询的参数硬编码为合理值。 有关更多示例,请参阅示例代码中的“SpaceVisualizer.cs”。

形状查询

在 DLL 中,形状分析器(“ShapeAnalyzer_W”)使用拓扑分析器来匹配用户定义的自定义形状。 Unity 示例定义一组形状,并通过应用内查询菜单在形状选项卡中显示结果。目的是让用户能够定义自己的对象形状查询,并根据应用程序的需要使用这些查询。

形状分析仅适用于水平面。 例如,沙发是由平坦的座椅表面和沙发靠背的平坦顶部定义的。 形状查询查找具有特定大小、高度和纵横比范围的两个表面,这两个表面相互对齐并连接。 根据 API 的术语,沙发座椅和靠背是形状组件,对齐要求是形状组件约束。

Unity 示例 (ShapeDefinition.cs) 中为“sittable ”对象定义了如下所示的示例查询。

shapeComponents = new List<ShapeComponent>()
{
    new ShapeComponent(
        new List<ShapeComponentConstraint>()
        {
            ShapeComponentConstraint.Create_SurfaceHeight_Between(0.2f, 0.6f),
            ShapeComponentConstraint.Create_SurfaceCount_Min(1),
            ShapeComponentConstraint.Create_SurfaceArea_Min(0.035f),
        }
    ),
};
AddShape("Sittable", shapeComponents);

每个形状查询由一组形状组件定义,其中每个组件具有一组组件约束,以及一组形状约束,它们列出了组件之间的依赖关系。 此示例在单个组件定义中包含三个约束,组件之间没有形状约束(因为只有一个组件)。

相比之下,沙发形状具有两个形状组件和四个形状约束。 组件是在用户的组件列表中按索引(在本例中为 0 和 1)标识的。

shapeConstraints = new List<ShapeConstraint>()
{
    ShapeConstraint.Create_RectanglesSameLength(0, 1, 0.6f),
    ShapeConstraint.Create_RectanglesParallel(0, 1),
    ShapeConstraint.Create_RectanglesAligned(0, 1, 0.3f),
    ShapeConstraint.Create_AtBackOf(1, 0),
};

Unity 模块中提供了包装器函数,用于轻松创建自定义形状定义。 可以在“SpatialUnderstandingDll.cs”中的“ShapeComponentConstraint”和“ShapeConstraint”结构内找到组件和形状约束的完整列表。

Rectangle shape is found on this surface
可在此表面上找到矩形

对象位置求解器

对象位置求解器可用于确定对象在物理房间中的理想放置位置。 求解器根据对象规则和约束查找最合适的位置。 此外,在使用“Solver_RemoveObject”或“Solver_RemoveAllObjects”调用删除对象之前,对象查询会一直存在,从而可以实现受约束的多对象放置。 对象位置查询由三个部分构成:带参数的位置类型、规则列表和约束列表。 若要运行查询,请使用以下 API。

public static int Solver_PlaceObject(
            [In] string objectName,
            [In] IntPtr placementDefinition,        // ObjectPlacementDefinition
            [In] int placementRuleCount,
            [In] IntPtr placementRules,             // ObjectPlacementRule
            [In] int constraintCount,
            [In] IntPtr placementConstraints,       // ObjectPlacementConstraint
            [Out] IntPtr placementResult)

此函数采用对象名称、位置定义以及规则和约束列表。 C# 包装器提供构造帮助程序函数来简化规则和约束的构造。 位置定义包含查询类型 - 即以下类型之一。

public enum PlacementType
{
    Place_OnFloor,
    Place_OnWall,
    Place_OnCeiling,
    Place_OnShape,
    Place_OnEdge,
    Place_OnFloorAndCeiling,
    Place_RandomInAir,
    Place_InMidAir,
    Place_UnderFurnitureEdge,
};

每个位置类型都有一组对该类型唯一的参数。 “ObjectPlacementDefinition”结构包含一组用于创建这些定义的静态帮助程序函数。 例如,若要在地面上查找一个可放置对象的位置,可使用以下函数。 public static ObjectPlacementDefinition Create_OnFloor(Vector3 halfDims) 除了位置类型之外,还可以提供一组规则和约束。 不能违反规则。 然后根据一组约束来优化可能的、与类型和规则相符的放置位置,以选择最佳放置位置。 每个规则和约束可以由提供的静态创建函数来创建。 下面提供了一个示例规则和约束构造函数。

public static ObjectPlacementRule Create_AwayFromPosition(
    Vector3 position, float minDistance)
public static ObjectPlacementConstraint Create_NearPoint(
    Vector3 position, float minDistance = 0.0f, float maxDistance = 0.0f)

以下对象位置查询查找一个位置,以便将一个半米的立方体放置在表面的边缘,远离其他先前放置的对象并靠近房间的中心。

List<ObjectPlacementRule> rules =
    new List<ObjectPlacementRule>() {
        ObjectPlacementRule.Create_AwayFromOtherObjects(1.0f),
    };

List<ObjectPlacementConstraint> constraints =
    new List<ObjectPlacementConstraint> {
        ObjectPlacementConstraint.Create_NearCenter(),
    };

Solver_PlaceObject(
    “MyCustomObject”,
    new ObjectPlacementDefinition.Create_OnEdge(
        new Vector3(0.25f, 0.25f, 0.25f),
        new Vector3(0.25f, 0.25f, 0.25f)),
    rules.Count,
    UnderstandingDLL.PinObject(rules.ToArray()),
    constraints.Count,
    UnderstandingDLL.PinObject(constraints.ToArray()),
    UnderstandingDLL.GetStaticObjectPlacementResultPtr());

如果成功,则返回包含放置位置、尺寸和方向的“ObjectPlacementResult”结构。 此外,该位置将添加到 DLL 的内部放置对象列表中。 后续位置查询会考虑到此对象。 Unity 示例中的“LevelSolver.cs”文件包含更多示例查询。

Results of object placement
图 3:蓝色框显示了带有“远离相机位置”规则的三个“放在地上”查询的结果

在求解某个级别或应用程序方案所需的多个对象的放置位置时,请先求解必需的大对象,以最大限度地提高找到所需空间的概率。 放置顺序非常重要。 如果找不到对象位置,请尝试使用约束较低的配置。 准备好一组回退配置对于多个房间配置的支持性功能至关重要。

房间扫描过程

虽然 HoloLens 提供的空间映射解决方案在设计上足够通用,可以满足所有问题空间的需求,但构建空间理解模块是为了支持两款特定游戏的需求。 它的解决方案是围绕特定过程和一组假设构建的,总结如下。

Fixed size playspace – The user specifies the maximum playspace size in the init call.

One-time scan process –
    The process requires a discrete scanning phase where the user walks around,
    defining the playspace.
    Query functions will not function until after the scan has been finalized.

用户驱动的游戏空间“绘制” - 在扫描阶段,用户移动并环顾游戏空间,从而有效地绘制出所要包括的区域。 在此阶段,生成的网格对于提供用户反馈非常重要。 家庭或办公室室内布置 - 查询功能是围绕平面和墙壁按直角设计的。 这是一种软限制。 但是,在扫描阶段,会完成主轴分析以优化长轴和短轴的网格细分。 包含的 SpatialUnderstanding.cs 文件管理扫描阶段过程。 它调用以下函数。

SpatialUnderstanding_Init – Called once at the start.

GeneratePlayspace_InitScan – Indicates that the scan phase should begin.

GeneratePlayspace_UpdateScan_DynamicScan –
    Called each frame to update the scanning process. The camera position and
    orientation is passed in and is used for the playspace painting process,
    described above.

GeneratePlayspace_RequestFinish –
    Called to finalize the playspace. This will use the areas “painted” during
    the scan phase to define and lock the playspace. The application can query
    statistics during the scanning phase as well as query the custom mesh for
    providing user feedback.

Import_UnderstandingMesh –
    During scanning, the “SpatialUnderstandingCustomMesh” behavior provided by
    the module and placed on the understanding prefab will periodically query the
    custom mesh generated by the process. In addition, this is done once more
    after scanning has been finalized.

由“SpatialUnderstanding”行为驱动的扫描流调用 InitScan,然后对每一帧调用 UpdateScan。 当统计数据查询报告了合理的覆盖范围时,用户可以通过隔空敲击手势来调用 RequestFinish 以指示扫描阶段结束。 持续调用 UpdateScan,直到它的返回值指示 DLL 已完成处理。

理解网格

理解 DLL 在内部将游戏空间存储为大小为 8 厘米网格的体素立方体。 在扫描的初始环节,完成主要成分分析以确定房间的轴。 在内部,该模块存储与这些轴对齐的体素空间。 大约每秒通过从体素体中提取等值面生成一个网格。

Generated mesh produced from the voxel volume
生成从体素体中产生的网格

疑难解答

  • 确保设置 SpatialPerception 功能
  • 当跟踪丢失时,下一个 OnSurfaceChanged 事件将删除所有网格。

混合现实工具包中的空间映射

有关将空间映射与混合现实工具包结合使用的更多信息,请参阅 MRTK 文档的空间感知部分

下一个开发检查点

如果你遵循了我们规划的 Unity 开发旅程,则目前正处于探索 MRTK 核心构建基块的过程中。 从这里,你可以继续了解下一部分基础知识:

或跳转到混合现实平台功能和 API:

你可以随时返回到 Unity 开发检查点

另请参阅