场景理解 SDK 概述

场景理解可转换混合现实设备捕获的非结构化环境传感器数据,并将其转换为强大的抽象表示形式。 SDK 充当应用程序与场景理解运行时之间的通信层。 它旨在模拟现有标准构造,例如用于 3D 表示形式的 3D 场景图以及用于 2D 应用程序的 2D 矩形和面板。 虽然场景理解模拟的构造将映射到具体框架,但一般情况下,SceneUnderstanding 与框架无关,因而支持与其交互的各个框架之间的互操作性。 场景理解在不断改进,SDK 的作用便是确保在统一的框架中继续公开新的表示形式和功能。 本文档首先介绍有助于熟悉开发环境/使用情况的高级别概念,然后提供有关特定类和构造的更详细文档。

在哪里获取 SDK?

可通过混合现实功能工具下载 SceneUnderstanding SDK。

注意:最新版本取决于预览包,需要启用预发布包才能查看它。

对于版本 0.5.2022-rc 及更高版本,场景理解支持 C# 和 C++ 语言投影,使应用程序能够开发适用于 Win32 或 UWP 平台的应用程序。 从此版本开始,SceneUnderstanding 支持 Unity 编辑器内支持功能(除了仅用于与 HoloLens2 通信的 SceneObserver)。

SceneUnderstanding 需要 Windows SDK 版本 18362 或更高版本。

概念概述

场景

混合现实设备会不断集成它在环境中看到的内容信息。 场景理解汇总所有这些数据源,并生成单一且一致的抽象。 场景理解生成场景,场景是由表示单个物体(例如墙壁/天花板/地面)实例的 SceneObject 构成。场景对象本身是由 SceneComponent 构成,SceneComponent 表示构成此 SceneObject 的更精细的部分。 组件的示例包括四边形和网格,但未来可能表示边界框、冲突网格、元数据等。

将原始传感器数据转换为场景的操作过程可能很昂贵,中型空间(大约 10 x 10 米)可能需要几秒钟,大型空间(大约 50 x 50 米)则可能需要几分钟,因此如果没有应用程序请求,设备便不会进行计算。 而是应用程序按需触发场景生成。 SceneObserver 类提供静态方法来计算或反序列化场景,之后你可以枚举场景/与场景交互。 “计算”操作按需执行,但在 CPU 上执行时是在单独的进程(混合现实驱动程序)中。 但是,当在其他进程中执行计算时,生成的场景数据将存储和维护在应用程序的场景对象中。

以下关系图演示了此过程流,并显示了通过接口与场景理解运行时连接的两个应用程序示例。

Process Diagram

左侧是混合现实运行时关系图,该运行时始终在自己的进程中启动并运行。 此运行时负责执行设备跟踪、空间映射和其他操作,场景理解使用这些操作来理解和推理周围的世界。 关系图右侧演示了利用场景理解的两个理论应用程序。 第一个应用程序通过接口与 MRTK 连接,MRTK 在内部使用场景理解 SDK,第二个应用计算并使用两个单独的场景实例。 此关系图中的所有三个场景都会生成不同的场景实例,驱动程序不会跟踪应用程序之间共享的全局状态,且某个场景中的场景对象不会出现在另一个场景中。 场景理解的确提供一种机制进行时间段内的跟踪,但这是通过 SDK 来完成的。 跟踪代码已在应用进程的 SDK 中运行。

由于每个场景将其数据存储在应用程序的内存空间中,因此可以假定场景对象的所有函数或其内部数据始终在应用程序进程中执行。

Layout

若要使用场景理解,了解和理解运行时如何以逻辑方式和物理方式表示组件可能很有价值。 场景采用特定布局来表示数据,所选的布局必须简单,且保持一个易满足未来要求而无需进行重大修改的基础结构。 为此,场景将所有组件(所有场景对象的构建基块)存储在一个简单列表中,并通过特定组件引用其他组件的引用来定义层次结构和构成。

下面演示了采用简单形式和逻辑形式的结构的示例。

逻辑布局物理布局
    场景
    • SceneObject_1
      • SceneMesh_1
      • SceneQuad_1
      • SceneQuad_2
    • SceneObject_2
      • SceneQuad_1
      • SceneQuad_3
    • SceneObject_3
      • SceneMesh_3
  • SceneObject_1
  • SceneObject_2
  • SceneObject_3
  • SceneQuad_1
  • SceneQuad_2
  • SceneQuad_3
  • SceneMesh_1
  • SceneMesh_2

此图突出显示了场景的物理布局和逻辑布局之间的差别。 在左侧,我们看到应用程序在枚举场景时所看到的数据的层次结构布局。 在右侧,我们看到场景由 12 个不同的组件(如有必要,可单独访问)组成。 处理新场景时,我们希望应用程序以逻辑方式遍历此层次结构,但是,在跟踪各场景更新时,某些应用程序可能只希望定位两个场景之间共享的特定组件。

API 概述

以下部分简要概述了场景理解中的构造。 本部分将帮助你了解场景的表示形式,以及各种组件的作用/用途。 下一部分将提供本概述中提到的具体代码示例和其他详细信息。

下面介绍的所有类型都驻留在 Microsoft.MixedReality.SceneUnderstanding 命名空间中。

SceneComponent

了解场景的逻辑布局后,我们可以介绍 SceneComponent 的概念,以及如何使用它们来构成层次结构。 SceneComponent 是 SceneUnderunder 中最精细的分解部分,表示单个核心体,例如网格、四边形或边界框。 SceneComponent 可独立更新,并且可由其他 SceneComponent 引用,因此它们具有单个全局属性和唯一 ID,支持针对此类型的跟踪/引用机制。 ID 用于场景层次结构以及对象持久性(某个场景相对于其他场景的更新行为)的逻辑构成。

如果将每个新计算的场景视为不同场景,并且只是枚举其中的所有数据,那么你基本可以忽略 ID。 但是,如果计划跟踪组件的多个更新,就需要使用这些 ID 在场景对象中索引和查找 SceneComponent。

SceneObject

SceneObject 是一个 SceneComponent,它表示一个“物体”(例如墙壁、地面、天花板等)的实例,由其 Kind 属性来表示。 SceneObject 是几何形,因此具有函数和属性来表示它们在空间中的位置,但是它们不包含任何几何结构或逻辑结构。 相反,SceneObject 引用其他 SceneComponent,特别是 SceneQuad 和 SceneMesh,这些 SceneComponent 提供系统支持的各种表示形式。 计算新场景时,应用程序很可能枚举场景的 SceneObject 来处理其感兴趣的内容。

SceneObject 可以具有以下任一项:

SceneObjectKind 说明
背景此 SceneObject 已知不是其他可识别的场景对象类型之一。 此类不应与“未知”混淆,其中“背景”已知不是墙壁/地面/天花板等,而“未知”则表示尚未分类。
Wall物理墙壁。 假定墙壁是不可移动的环境结构。
Floor地面是可在其上行走的任何图面。 注意:楼梯不是地面。 另请注意,地面假定任何可行走的图面,因此没有明确假设单一地面。 多层结构、坡道等应全部归为地面类别。
上限房间的上表面。
平台可以放置全息影像的较大平面。 它们往往表示桌子、台面和其他较大水平面。
世界与标记无关的几何数据的保留标签。 通过设置 EnableWorldMesh 更新标志而生成的网格将归为世界类别。
未知此场景对象尚未分类和分配类型。 它不应与“背景”混淆,因为此对象可以是任何物体,只是系统尚未针对它提出足够有力的分类。

SceneMesh

SceneMesh 是一种 SceneComponent,它使用三角形列表近似表示任意几何对象的几何形状。 SceneMeshe 可用于多个不同的上下文;它们可以表示水密单元结构的组件,或表示为 WorldMesh,即与场景关联的无限空间映射网格。 每个网格提供的索引和顶点数据,使用与所有现代呈现 API 中用于呈现三角形网格的顶点和索引缓冲区相同且熟悉的布局。 在场景理解中,网格使用 32 位索引,可能需要针对某些呈现引擎分解为区块。

顶点连接顺序和坐标系

场景理解生成的所有网格都应按顺时针顶点连接顺序在右手坐标系中返回网格。

注意:.191105 版本之前的 OS 内部版本可能存在一个已知 bug,即“世界”网格按逆时针顶点连接顺序返回,该 bug 随后已修复。

SceneQuad

SceneQuad 是一个 SceneComponent,它表示占用 3D 世界的 2D 图面。 SceneQuad 的使用类似于 ARKit ARPlaneAnchor 或 ARCore Planes,但前者提供了更高级的功能,即供平面应用或增强 UX 使用的 2D 画布。它为四边形提供特定于 2D 的 API,便于轻松放置和布局,并且在使用四边形进行开发(除呈现以外)时,应该感觉更像是在使用 2D 画布而非 3D 网格。

SceneQuad 形状

SceneQuad 定义有边界的 2D 矩形图面。 但是,SceneQuad 表示的图面可能是任意复杂的形状(例如圆环形状的桌子)。若要表示复杂的四边形图面形状,可以使用 GetSurfaceMask API 将图面形状呈现到你提供的图像缓冲区上。 如果具有四边形的 SceneObject 也具有网格,则网格三角形应等效于呈现的图像,它们均表示图面的实际几何图形(在 2D 或 3D 坐标系中)。

场景理解 SDK 详细信息和引用

注意

使用 MRTK 时,请注意,你将与 MRTK 的 [`WindowsSceneUnderstandingObserver`](xref:Microsoft.MixedReality.Toolkit.WindowsSceneUnderstanding.Experimental.WindowsSceneUnderstandingObserver?view=mixed-reality-toolkit-unity-2020-dotnet-2.8.0&preserve-view=true) 交互,因此在大多数情况下可能会跳过此部分。 有关详细信息,请参阅 [MRTK 场景理解文档] (/windows/mixed-reality/mrtk-unity/features/spatial-awareness/scene-understanding)。

以下部分将帮助你熟悉 SceneUnderstanding 的基础知识。 本部分提供的基础知识应使你获得足够的背景知识来浏览示例应用程序,以了解如何全面使用 SceneUnderstanding。

初始化

使用 SceneUnderunder 的第一步是让应用程序获取对场景对象的引用。 可通过两种方式之一完成此操作:一种是由驱动程序来计算场景,另一种是反序列化过去计算的现有场景。 后者对于在开发过程中使用 SceneUnderstanding 非常有用,即无需使用混合现实设备即可快速构建应用程序和体验的原型。

使用 SceneObserver 计算场景。 在创建场景之前,应用程序应查询设备以确保其支持 SceneUnderstanding,并请求用户访问 SceneUnderstanding 所需信息的权限。

if (!SceneObserver.IsSupported())
{
    // Handle the error
}

// This call should grant the access we need.
await SceneObserver.RequestAccessAsync();

如果未调用 RequestAccessAsync(),则计算新场景将失败。 接下来,我们将计算以混合现实头戴显示设备为中心、半径为 10 米的一个新场景。

// Create Query settings for the scene update
SceneQuerySettings querySettings;

querySettings.EnableSceneObjectQuads = true;                                       // Requests that the scene updates quads.
querySettings.EnableSceneObjectMeshes = true;                                      // Requests that the scene updates watertight mesh data.
querySettings.EnableOnlyObservedSceneObjects = false;                              // Do not explicitly turn off quad inference.
querySettings.EnableWorldMesh = true;                                              // Requests a static version of the spatial mapping mesh.
querySettings.RequestedMeshLevelOfDetail = SceneMeshLevelOfDetail.Fine;            // Requests the finest LOD of the static spatial mapping mesh.

// Initialize a new Scene
Scene myScene = SceneObserver.ComputeAsync(querySettings, 10.0f).GetAwaiter().GetResult();

从数据初始化(也称为电脑路径)

虽然可以计算场景以供直接使用,但也可以采用序列化形式计算这些场景,以供之后使用。 对于开发而言,这一点经证实非常有用,因为它使开发人员无需设备即可使用和测试场景理解。 场景序列化行为与计算行为几乎相同,数据将返回到应用程序,而不是由 SDK 在本地反序列化。 之后,可以自行对其反序列化或将其保存以供未来使用。

// Create Query settings for the scene update
SceneQuerySettings querySettings;

// Compute a scene but serialized as a byte array
SceneBuffer newSceneBuffer = SceneObserver.ComputeSerializedAsync(querySettings, 10.0f).GetAwaiter().GetResult();

// If we want to use it immediately we can de-serialize the scene ourselves
byte[] newSceneData = new byte[newSceneBuffer.Size];
newSceneBuffer.GetData(newSceneData);
Scene mySceneDeSerialized = Scene.Deserialize(newSceneData);

// Save newSceneData for later

SceneObject 枚举

应用程序具有场景后,它将查看 SceneObject 并与之交互。 这是通过访问 SceneObjects 属性来完成的:

SceneObject firstFloor = null;

// Find the first floor object
foreach (var sceneObject in myScene.SceneObjects)
{
    if (sceneObject.Kind == SceneObjectKind.Floor)
    {
        firstFloor = sceneObject;
        break;
    }
}

组件更新和重新查找组件

另一个函数可用于检索场景中的组件,名为 FindComponent。 更新跟踪对象并在稍后的场景中查找它们时,此函数非常有用。 以下代码将计算相对于上一个场景的新场景,然后在新场景中查找地面。

// Compute a new scene, and tell the system that we want to compute relative to the previous scene
Scene myNextScene = SceneObserver.ComputeAsync(querySettings, 10.0f, myScene).GetAwaiter().GetResult();

// Use the Id for the floor we found last time, and find it again
firstFloor = (SceneObject)myNextScene.FindComponent(firstFloor.Id);

if (firstFloor != null)
{
    // We found it again, we can now update the transforms of all objects we attached to this floor transform
}

从场景对象访问网格和四边形

找到 SceneObject 后,应用程序很可能希望访问构成对象的四边形/网格中包含的数据。 使用四边形和网格属性访问此数据。 以下代码将枚举地面对象的所有四边形和网格。


// Get the transform for the SceneObject
System.Numerics.Matrix4x4 objectToSceneOrigin = firstFloor.GetLocationAsMatrix();

// Enumerate quads
foreach (var quad in firstFloor.Quads)
{
    // Process quads
}

// Enumerate meshes
foreach (var mesh in firstFloor.Meshes)
{
    // Process meshes
}

请注意,SceneObject 具有相对于场景原点的转换。 这是因为 SceneObject 表示“物体”的实例,它在空间中是可定位的,而四边形和网格则表示相对于其父级转换的几何图形。 单独的 SceneObject 可以引用相同的 SceneMesh/SceneQuad SceneComponent,并且 SceneObject 可以具有多个 SceneMesh/SceneQuad。

处理转换

在处理转换时,场景理解有意尝试与传统的 3D 场景表示形式保持一致。 因此,每个场景会限制在单个坐标系中,就像大多数常见的 3D 环境表示形式一样。 SceneObject 各自提供它们相对于该坐标系的位置。 如果应用程序处理的场景超出了单个原点所提供的范围,应用程序可以将 SceneObject 定位在 SpatialAnchor 上,或生成多个场景并将其合并,但为了简单起见,我们假设水密场景存在于它们自己的原点中,该原点由 Scene.OriginSpatialGraphNodeId 定义的一个 NodeId 来定位。

例如,以下 Unity 代码将演示如何使用 Windows Perception 和 Unity API 将坐标系彼此对齐。 有关 Windows Perception API 的详细信息,请参阅 SpatialCoordinateSystemSpatialGraphInteropPreview,有关如何获取与 Unity 世界原点对应的 SpatialCoordinateSystem 的详细信息,请参阅 Unity 中的混合现实原生对象

private System.Numerics.Matrix4x4? GetSceneToUnityTransformAsMatrix4x4(SceneUnderstanding.Scene scene)
{
    System.Numerics.Matrix4x4? sceneToUnityTransform = System.Numerics.Matrix4x4.Identity;

    
    Windows.Perception.Spatial.SpatialCoordinateSystem sceneCoordinateSystem = Microsoft.Windows.Perception.Spatial.Preview.SpatialGraphInteropPreview.CreateCoordinateSystemForNode(scene.OriginSpatialGraphNodeId);
    Windows.Perception.Spatial.SpatialCoordinateSystem unityCoordinateSystem = Microsoft.Windows.Perception.Spatial.SpatialCoordinateSystem.FromNativePtr(UnityEngine.XR.WindowsMR.WindowsMREnvironment.OriginSpatialCoordinateSystem);

    sceneToUnityTransform = sceneCoordinateSystem.TryGetTransformTo(unityCoordinateSystem);

    if (sceneToUnityTransform != null)
    {
        sceneToUnityTransform = ConvertRightHandedMatrix4x4ToLeftHanded(sceneToUnityTransform.Value);
    }
    else
    {
        return null;
    }
            
    return sceneToUnityTransform;
}

每个 SceneObject 都具有一个转换,之后可应用于对象。 在 Unity 中,我们转换为右手坐标并分配本地转换,如下所示:

private System.Numerics.Matrix4x4 ConvertRightHandedMatrix4x4ToLeftHanded(System.Numerics.Matrix4x4 matrix)
{
    matrix.M13 = -matrix.M13;
    matrix.M23 = -matrix.M23;
    matrix.M43 = -matrix.M43;

    matrix.M31 = -matrix.M31;
    matrix.M32 = -matrix.M32;
    matrix.M34 = -matrix.M34;

    return matrix;
}

 private void SetUnityTransformFromMatrix4x4(Transform targetTransform, System.Numerics.Matrix4x4 matrix, bool updateLocalTransformOnly = false)
 {
    if(targetTransform == null)
    {
        return;
    }

    Vector3 unityTranslation;
    Quaternion unityQuat;
    Vector3 unityScale;

    System.Numerics.Vector3 vector3;
    System.Numerics.Quaternion quaternion;
    System.Numerics.Vector3 scale;

    System.Numerics.Matrix4x4.Decompose(matrix, out scale, out quaternion, out vector3);

    unityTranslation = new Vector3(vector3.X, vector3.Y, vector3.Z);
    unityQuat        = new Quaternion(quaternion.X, quaternion.Y, quaternion.Z, quaternion.W);
    unityScale       = new Vector3(scale.X, scale.Y, scale.Z);

    if(updateLocalTransformOnly)
    {
        targetTransform.localPosition = unityTranslation;
        targetTransform.localRotation = unityQuat;
    }
    else
    {
        targetTransform.SetPositionAndRotation(unityTranslation, unityQuat);
    }
}

// Assume we have an SU object called suObject and a unity equivalent unityObject

System.Numerics.Matrix4x4 converted4x4LocationMatrix = ConvertRightHandedMatrix4x4ToLeftHanded(suObject.GetLocationAsMatrix());
SetUnityTransformFromMatrix4x4(unityObject.transform, converted4x4LocationMatrix, true);
        

四边形

四边形旨在为 2D 放置方案提供帮助,应视为 2D 画布 UX 元素的扩展。 虽然四边形是 SceneObject 的组成部分,并且可以在 3D 中呈现,但四边形 API 本身假定四边形是 2D 结构。 它们提供范围、形状等信息,并提供 API 以用于放置。

四边形提供矩形范围,但它们表示任意形状的 2D 图面。 为在这些与 3D 环境交互的 2D 图面上实现放置,四边形提供了实用程序,来实现这种交互。 目前,场景理解提供两个此类函数:FindCentermostPlacement 和 GetSurfaceMask。 FindCentermostPlacement 是一个高级别 API,它定位四边形上可以放置对象的位置,并尝试查找对象的最佳位置,确保提供的边界框始终在基础图面之上。

注意

输出的坐标是相对于“四边形空间”中的四边形而言的,其左上角坐标为(x = 0,y = 0),就像其他窗口矩形类型一样。 在对自己的对象使用原点时,请务必考虑这一点。

以下示例演示如何查找最中央的可放置位置,以及如何将全息影像定位到四边形。

// This code assumes you already have a "Root" object that attaches the Scene's Origin.

// Find the first quad
foreach (var sceneObject in myScene.SceneObjects)
{
    // Find a wall
    if (sceneObject.Kind == SceneObjectKind.Wall)
    {
        // Get the quad
        var quads = sceneObject.Quads;
        if (quads.Count > 0)
        {
            // Find a good location for a 1mx1m object  
            System.Numerics.Vector2 location;
            if (quads[0].FindCentermostPlacement(new System.Numerics.Vector2(1.0f, 1.0f), out location))
            {
                // We found one, anchor something to the transform
                // Step 1: Create a new game object for the quad itself as a child of the scene root
                // Step 2: Set the local transform from quads[0].Position and quads[0].Orientation
                // Step 3: Create your hologram and set it as a child of the quad's game object
                // Step 4: Set the hologram's local transform to a translation (location.x, location.y, 0)
            }
        }
    }
}

步骤 1-4 在很大程度上取决于特定框架/实施,但主题应类似。 需要注意的是,四边形只是表示在空间中定位的一个有边界的 2D 平台。 通过让引擎/框架知道四边形的位置,并将对象放置在相对于四边形的中心位置,全息影像将正确定位到与现实世界对应的位置。

网格

网格表示对象或环境的几何表示形式。 与空间映射非常类似,每个空间表面网格提供的索引和顶点数据,与所有现代呈现 API 中用于呈现三角形网格的顶点和索引缓冲区使用的布局相同。 Scene 坐标系中提供顶点位置。 用于引用此数据的特定 API 如下所示:

void GetTriangleIndices(int[] indices);
void GetVertices(System.Numerics.Vector3[] vertices);

以下代码提供了从网格结构生成三角形列表的示例:

uint[] indices = new uint[mesh.TriangleIndexCount];
System.Numerics.Vector3[] positions = new System.Numerics.Vector3[mesh.VertexCount];

mesh.GetTriangleIndices(indices);
mesh.GetVertexPositions(positions);

索引/顶点缓冲区必须是 > = 索引/顶点计数,但也可以任意调整大小,以便高效重用内存。

ColliderMesh

场景对象通过 Meshes 和 ColliderMeshes 属性提供对网格和碰撞体网格数据的访问。 这些网格将始终匹配,意味着 Meshes 属性的索引表示与 ColliderMeshes 属性的索引相同的几何图形。 如果运行时/对象支持碰撞体网格,则可以确保获得最少的多边形和最多的近似图形,如果应用程序要使用碰撞体,使用 ColliderMesh 是不错的做法。 如果系统不支持碰撞体,则 ColliderMesh 返回的 Mesh 对象将与网格对象相同,以减少内存约束。

使用场景理解进行开发

现在你应已了解场景理解运行时和 SDK 的核心构建基块。 其功能复杂性主要在于访问模式、与 3D 框架的交互,以及可基于这些 API 编写以便执行更高级任务(例如空间规划、房间分析、导航、物理任务等)的工具。 我们希望通过示例描述这些内容,并希望这些示例能够正确引导你成功完成方案。 如果有未涉及的示例或方案,请告知我们,我们将尝试记录/原型化所需的内容。

在哪里可以获取示例代码?

可以在 Unity 示例页找到 Unity 的场景理解示例代码。 通过此应用程序,你可以与设备通信并呈现各种场景对象,或者,可以在电脑上加载序列化场景,并在没有设备的情况下体验场景理解。

在哪里可以获取示例场景?

如果拥有 HoloLens2,可以通过将 ComputeSerializedAsync 的输出保存到文件并自行将其反序列化,来保存捕获的任何场景。

如果没有 HoloLens2 设备,但想要使用场景理解,则需要下载预先捕获的场景。 场景理解示例目前附带序列化场景,你可以自行下载并使用这些场景。 可在以下位置找到它们:

场景理解示例场景

另请参阅