空间映射 允许检索表示 HoloLens 设备周围世界中表面的三角形网格。 可以使用表面数据进行放置、遮挡和房间分析,为Unity项目提供额外的沉浸感。
Unity包括对空间映射的完全支持,它通过以下方式向开发人员公开:
- MixedRealityToolkit 中提供的空间映射组件,为空间映射入门提供了方便快捷的路径
- 较低级别的空间映射 API,可提供完全控制并启用更复杂的特定于应用程序的自定义
若要在应用中使用空间映射,需要在 AppxManifest 中设置 SpatialPerception 功能。
设备支持
| 功能 | HoloLens (第一代) | HoloLens 2 | 沉浸式头戴显示设备 |
|---|---|---|---|
| 空间映射 | ✔️ | ✔️ | ❌ |
设置 SpatialPerception 功能
为了使应用能够使用空间映射数据,必须启用 SpatialPerception 功能。
如何启用 SpatialPerception 功能:
- 在Unity 编辑器中,打开“播放器设置”窗格, (编辑>项目设置>播放器)
- 在 “Windows 应用商店” 选项卡上选择
- 展开“发布设置”,检查“功能”列表中的“SpatialPerception”功能
注意
如果已将Unity项目导出到 Visual Studio 解决方案,则需要导出到新文件夹或在 Visual Studio 的 AppxManifest 中手动设置此功能。
空间映射还需要至少 10.0.10586.0 的 MaxVersionTested:
- 在 Visual Studio 中,右键单击解决方案资源管理器中的 Package.appxmanifest,然后选择“查看代码”
- 找到指定 TargetDeviceFamily 的 行,并将 MaxVersionTested=“10.0.10240.0” 更改为 MaxVersionTested=“10.0.10586.0”
- 保存 Package.appxmanifest。
如何在Unity中添加映射
空间感知系统
在 MRTK 中,请查看 空间感知入门 指南,了解有关设置各种空间网格观察程序的信息。
有关设备观察程序的信息,请参阅 为设备配置网格观察程序 指南。
有关场景理解观察程序的信息,请参阅 场景理解观察者 指南。
更高级别网格分析:空间理解
警告
空间理解已弃用,转而支持 场景理解。
MixedRealityToolkit 是基于Unity全息 API 构建的全息开发的实用工具代码集合。
空间理解
在物理世界中放置全息影像时,通常需要超越空间映射的网格和表面平面。 当按程序放置时,需要更高级别的环境理解。 这通常需要决定什么是地板、天花板和墙壁。 还可以针对一组放置约束进行优化,以确定全息对象的最佳物理位置。
在 Young Conker 和 Fragments 的开发过程中,Asobo Studios 通过开发房间求解器来正面面对这个问题。 这些游戏中的每一个都有特定于游戏的需求,但它们共享核心空间理解技术。 HoloToolkit.SpatialUnderstanding 库封装了此技术,使你可以快速查找墙壁上的空白空间、将对象放在天花板上、标识要放置的字符以及大量其他空间理解查询。
包含所有源代码,使你可以根据需要对其进行自定义,并与社区共享改进。 C++求解器的代码已包装到 UWP dll 中,并公开给 Unity,其中预制件包含于 MixedRealityToolkit 中。
了解模块
模块公开了三个主要接口:用于简单图面和空间查询的拓扑、用于对象检测的形状,以及用于基于约束放置对象集的对象放置求解器。 下面介绍了其中的每一项。 除了三个主要模块接口外,光线投射接口还可用于检索标记的表面类型,并且可以复制自定义水密游戏空间网格。
光线投射
房间扫描完成后,会在内部为地面、天花板和墙壁等表面生成标签。 如果光线与已知表面相撞,函数 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 厘米立方体素表示形式来计算光线投射。 每个体素包含一组具有已处理拓扑数据的 surface 元素, (又名 surfels) 。 将比较相交体素单元格中包含的冲浪和用于查找拓扑信息的最佳匹配项。 此拓扑数据包含以“SurfaceTypes”枚举形式返回的标记,以及相交图面的外围应用。
在Unity示例中,光标每帧投射一条光线。 首先,对抗Unity的碰撞体。 其次,针对理解模块的世界表示形式。 最后,再次显示 UI 元素。 在此应用程序中,UI 优先于理解结果,最后Unity碰撞体。 SurfaceType 报告为光标旁边的文本。
在光标旁边标记 Surface 类型
拓扑查询
在 DLL 中,拓扑管理器处理环境的标记。 如上所述,大部分数据存储在体素卷中包含的冲浪器中。 此外,“PlaySpaceInfos”结构还用于存储有关游戏空间的信息,包括世界对齐 (下面) 、地板和天花板高度的更多详细信息。 试探法用于确定地板、天花板和墙壁。 例如,面积大于 1-m2 的最大和最低水平表面被视为地板。
注意
扫描过程中的相机路径也用于此过程。
拓扑管理器公开的查询子集通过 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模块中提供了包装函数,以便轻松创建自定义形状定义。 组件和形状约束的完整列表可以在“ShapeComponentConstraint”和“ShapeConstraint”结构的“SpatialUnderstandingDll.cs”中找到。
在此图面上找到矩形形状
对象放置规划求解器
对象放置求解器可用于确定物理房间中放置对象的理想位置。 求解器将找到给定对象规则和约束的最佳拟合位置。 此外,对象查询一直保留,直到通过“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”结构包含一组用于创建这些定义的静态帮助程序函数。 例如,若要查找将对象放在地板上的位置,可以使用以下函数。 公共静态 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”文件包含更多示例查询。
图 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 厘米大小的体素立方体的网格。 在扫描的初始部分,将完成主要组件分析以确定房间的轴。 在内部,它存储与这些轴对齐的体素空间。 大约每秒通过从体素体中提取等值表面生成一个网格。
从体素卷生成的网格
疑难解答
- 确保已设置 SpatialPerception 功能
- 当跟踪丢失时,下一个 OnSurfaceChanged 事件将删除所有网格。
混合现实工具包中的空间映射
有关将空间映射与 混合现实 工具包配合使用的详细信息,请参阅 MRTK 文档的空间感知部分。
下一个开发检查点
如果你遵循我们布局的Unity开发旅程,则你正在探索 MRTK 核心构建基块。 在此处,可以继续下一个构建基块:
或者跳转到混合现实平台功能和 API:
可以随时返回到Unity开发检查点。