IOS 增强现实的 .NET 开发者指南(一)

原文:.NET Developer’s Guide to Augmented Reality in iOS

协议:CC BY-NC-SA 4.0

一、设置您的环境

首先,我们需要确保你已经安装了一些你需要的东西;之后,我们可以开始编写基本的增强现实应用并将其部署到您的 iOS 设备上。

这是你需要的东西的清单:

  • 苹果身份证

  • 合适的 iOS 设备

  • 运行 macOS 的电脑

  • x mode(x mode)-x mode(x mode)-x mode(x mode)(x mode)(x mode)(x mode)(x mode)(x mode)(x mode)(x mode)

  • 用于 Mac 的 Visual Studio

苹果 ID

好消息是,你不需要注册付费的苹果开发者计划来将应用部署到你的 iOS 设备上;你只需要你的 Apple ID 就可以开始了。然而,如果你希望最终将应用发布到 App Store,你需要加入苹果开发者计划并支付费用。你可以在 https://developer.apple.com/programs/ 找到更多关于苹果开发者计划的信息。

合适的 iOS 设备

虽然 ARKit 自 iOS 11 以来就已经存在,但旧手机可能没有足够复杂的摄像头或 CPU 来使用 ARKit 的一些新功能,如身体遮挡。您至少需要一部 iPhone 6s 或更新的 iPhone 才能使用本书中的增强现实示例。

还值得一提的是,你需要合适的电缆将你的设备连接到你的 PC 或笔记本电脑,以便你可以从 Xcode 和 Visual Studio for Mac 部署应用。值得注意的是,经过一点设置后,还可以通过 Wi-Fi 从您的电脑上将您的应用部署到您的设备上,而无需线缆。

安装 Xcode

虽然我们将主要使用 Visual Studio for Mac 来创建本书中的增强现实应用,但由于其他原因,Xcode 也是必需的,以便在您的 iOS 设备上为我们的应用提供和安装代码签名证书。

如果你还没有安装 Xcode,可以从 App Store 安装(图 1-1 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-1

从 App Store 下载并安装 Xcode

安装 Visual Studio for Mac

你还需要最新版本的 Visual Studio for Mac,在撰写本文时是 2019 年,你会很高兴听到我们将花大部分时间在它上面。我发现,由于 Visual Studio for Mac 是一个相当新的产品,它一直在不断更新和改进。

如果你是 Windows 上的 Visual Studio 的用户,你会注意到,虽然 Mac 上的 Visual Studio 与 Windows 上的 Visual Studio 相似,但它确实有一些差异;它们不是 100%等同的。

Visual Studio for Mac 有很多要求,其中最主要的是 Xcode ( https://docs.microsoft.com/en-us/visualstudio/productinfo/vs2019-system-requirements-mac )。

可以从 https://visualstudio.microsoft.com/vs/mac/ 安装 Visual Studio for Mac,如图 1-2 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-2

下载并安装 Visual Studio for Mac

在 Xcode 中创建新项目

一旦你安装了 Xcode 和 Visual Studio for Mac,让我们开始创建我们的第一个项目。如果您想知道为什么我们要从 Xcode 中的项目开始,那是因为我们需要在 Xcode 中创建一个空白应用,并将其部署到我们的设备上,以便将相关的代码签名证书部署到设备上。

启动 Xcode,选择“新建 Xcode 项目”,如图 1-3 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-3

在 Xcode 中创建新项目

第一步。选择项目模板

在下一个名为“为您的新项目选择模板”的屏幕上,当您选择模板时,选择“单视图应用”,然后单击“下一步”,如图 1-4 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-4

选择“单一视图应用”作为项目模板

第二步。提供项目详情

在下一个名为“为您的新项目选择选项”的屏幕上,在“产品名称”字段中为您的应用提供一个名称。在图 1-5 中,你可以看到我已经编造了一些细节。

如果您在使用 Apple ID 之前已经登录 Xcode,则您可能已经在“团队”栏中有了一个(个人团队)条目。如果没有,也不用担心。我们稍后将登录以生成团队。

您可以将语言和用户界面保留为默认值;此外,我们不会使用单元测试或 UI 测试,所以你最好不要检查它们。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-5

提供项目选项详细信息

Note

请特别注意创建的包标识符,因为我们在 Visual Studio for Mac 中创建增强现实应用时将需要它。

在本例中,它是 AwesomeCompany.HelloWorldAR。

单击下一步。

第三步。提供项目位置

为您的项目选择一个位置。我通常会为此创建一个新文件夹。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-6

为您的项目选择一个位置

单击创建。

第四步。查看新项目

您应该会看到 Xcode 中新创建的 Swift 项目,如图 1-7 所示。这个不用太担心。我们不会更改任何 Swift 代码!

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-7

您新创建的 Swift 项目

但是,我们将把该项目部署到我们的设备上,以生成和部署我们稍后需要的代码签名证书。

您会很高兴地听到,这是在我们能够专注于使用 C# 代码在 Visual Studio for Mac 中工作之前所需的最后一步。

如果您单击“播放”按钮或立即运行项目,而没有提前提供团队,构建将会失败。所以我们去选一个队吧。

第五步。选择一个团队或使用 Apple ID 登录

双击项目名称以打开项目设置,然后转到签名和功能部分。

如果列表中没有团队,请选择“添加帐户…”然后用你的 Apple ID 登录,如图 1-8 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-8

选择开发团队

第六步。更改部署目标

如果您现在运行项目,它将启动设备模拟器。我们不希望这样,所以请确保您的计算机通过适当的电缆连接到您的设备,然后将部署目标更改为您的设备名称(如图 1-9 所示)并单击播放或运行(确保您的设备已解锁)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-9

更改部署目标

Note

可以通过 Wi-Fi 设置调试和部署,无需在电脑和设备之间使用电缆。

第七步。信任开发商

如果您现在运行项目,它会将应用部署到设备;但是,如果您之前没有部署到您的设备,您可能会看到如图 1-10 所示的以下消息。别担心。这只是意味着我们需要在您的设备上执行一个简单的安全步骤。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-10

信托开发商

为了在您的 iOS 设备上信任开发者和您的应用,您必须前往设置➤通用➤设备管理并选择开发者应用。

按下信任按钮并确认,如图 1-11 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-11

信任设备管理中的开发人员

如果你现在从 Xcode 运行这个应用,一切都按计划进行,你应该会在手机上看到默认的 Hello world 屏幕。

第八步。完成的

恭喜你。对于一些人来说,这可能是你第一次在你的设备上部署应用。你会很高兴知道我们不会在这个项目上做任何其他事情。但是,您可能需要此项目将证书重新部署到您的设备,这样我就不会删除它。把它放在你的机器上。

Note

个人代码签名仅持续 7 天,之后你需要将你的应用重新部署到你的设备,以使其再次工作。

Reminder

请务必记下步骤 2 中的包标识符,因为当我们在 Visual Studio for Mac 中创建应用时将需要它。

在 Visual Studio for Mac 中创建新项目

接下来,我们将创建我们的应用,它将包含我们在 Visual Studio for Mac 中的增强现实实验,并将其部署到我们的 iOS 设备上。

启动 Visual Studio for Mac 并选择“新建项目”。

第一步。创建新项目并选择项目类型

从模板类别列表中选择 iOS,然后选择单视图 App,如图 1-12 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-12

选择项目类型

第二步。提供应用详细信息

您将希望使用与您在 Xcode 应用中使用的相同的应用名称和组织标识符,以便捆绑包标识符与 Xcode 相同,如图 1-13 所示。这样就可以使用相同的代码签名证书将应用配置和部署到您的 iOS 设备上。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-13

提供应用详细信息

第三步。提供项目详情

现在您需要提供项目名称、解决方案名称以及项目的位置,如图 1-14 所示。这些可以是您想要的任何位置,但请确保您为 Xcode 应用提供了不同的位置。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-14

提供项目详情

单击创建。

第四步。选择部署设备并运行

在前面的步骤中创建您的项目后,您应该看到新创建的项目框架,如图 1-15 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-15

查看新项目

确保将部署目标更改为连接的 iOS 设备,并且该设备已解锁,然后运行项目。该设备应该运行应用,这是一个相当无聊的空白白屏。

恭喜你!你已经部署了你的第一个。NET iOS 项目下载到您的设备上。

值得注意的是,这个应用还没有任何增强现实功能;我们还没有为此编写代码。您创建的项目和部署的应用将承载我们将在本书中介绍的所有增强现实功能。

设置相机权限

我们将用于增强现实的新应用将需要使用您的摄像头,因此您需要在 projects Info.plist 文件中显式声明此权限。

您可以从下拉列表中选择“隐私-相机使用说明”,并提供您喜欢的任何信息,如图 1-16 所示。首次运行应用时会显示此消息,要求用户授予应用使用摄像头的权限。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-16

设置相机权限

摘要

现在,您应该已经设置好了本地环境,并准备好开始体验增强现实。这正是我们将要做的,但首先让我们在下一章讨论增强现实和 ARKit 的一些基本概念。

二、基本概念

在这一章中,我们看一些基本概念,这些概念使使用 ARKit 的移动增强现实体验成为可能,并且您将很快使用它们来构建您的增强现实应用。

在继续之前,很好地理解这些基础知识是很重要的,因为这将对你有很大的帮助,然后再继续本书中的进一步主题,在那里我们将回头参考这些基础知识。只有在理解了这些基本概念之后,我们才能继续探索更高级的概念。

场景视图

在 ARKit 中,增强现实场景视图(ARSCNView)是所有神奇事情发生的地方。当一个ARSCNView的会话运行时,它将摄像机设置为视图的背景,并显示我们添加到场景顶部的任何东西。

在清单 2-1 中,您可以看到场景视图是在 ViewController 构造函数中创建的,并且可以设置一些初始属性(我们将在后面讨论)。然后这个场景视图被添加为当前视图的子视图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-1

坐标系统

ViewDidLoad事件中,我们也将场景视图的框架设置为这个视图的框架。

您将在整本书的所有 AR 示例中使用这个基本的设置/样板代码。

private readonly ARSCNView sceneView;

public ViewController(IntPtr handle) : base(handle)
{
    this.sceneView = new ARSCNView();
    {
        AutoenablesDefaultLighting = true

    };

    this.View.AddSubview(this.sceneView);
}

public override void ViewDidLoad()
{
    base.ViewDidLoad();
    this.sceneView.Frame = this.View.Frame;
}

Listing 2-1Creating the Scene View

会议

在调用Session.Run()方法之前,SceneView 中不会发生任何事情。一旦会话开始运行,它会做很多事情。

首先,它将相机设置为视图的背景。

然后,当你四处移动设备/相机时,它开始尝试了解你的即时环境,记录兴趣点及其在相机帧之间的相对位置,同时使用设备的陀螺仪和加速度计来了解设备的方向。这个有趣的名字叫做视觉惯性里程计,这就是当我们移动相机时,它如何能够理解环境并保持我们放入场景中的东西的位置。

它开始将不可见的Anchors放置在它发现的感兴趣的点上,并在你放置它们的位置覆盖你已经放置到场景中的任何 3D 对象。锚点(我们将在第三章 ??,“节点、几何体、材质和锚点”中详细讨论)是我们 AR 场景中的参考点,可以自动检测或手动放置在场景中。

当调用Session.Run()方法时,你必须提供一种类型的ARConfiguration,它定义了你想要在场景中使用的 AR 功能的类型,如清单 2-2 所示。根据会话中使用的配置类型和设置,它可能会根据在场景中检测到的内容(如平面、图像或面)表现不同。

public override void ViewDidAppear(bool animated)

{
   base.ViewDidAppear(animated);

    this.sceneView.Session.Run(
      new ARWorldTrackingConfiguration());
}

Listing 2-2Starting the SceneView Session

SceneKit

虽然 ARKit 使本书中提到的增强现实功能成为可能,但我们实际上将广泛使用 SceneKit(这是苹果的 3D 图形框架),包括将对象放置到我们的 AR 场景中。接下来的“尺寸”和“定位”部分都来自 SceneKit,节点、几何体和材质将在下一章讨论,动画将在第五章“动画”中讨论

如果您想知道 ARKit 在哪里结束,SceneKit 在哪里开始,下面的内容可能会有所帮助。

您通常可以分辨出哪些代码类型来自 ARKit 或 SceneKit,因为它们通常分别以 AR 或 SCN 为前缀。比如ARSCNView来自 ARKit,SCNNode来自 SceneKit。

配置

了解坐标系在 SceneKit 中的工作方式非常重要,这样您就可以在 AR 场景中定位自己,并能够在三维空间中围绕您的环境放置多个对象。

有三个维度你需要记住并习惯,X,Y,Z 如图 2-1 所示。其中 X 是从左到右,Y 是从下到上,Z 是从前到后。幸运的是,有一个内置的功能,你可以打开它,在你的应用中显示坐标轴。我们将在第四章的“内置增强现实指南”中介绍这一点

在清单 2-3 中,我们可以看到当设置一个对象的位置时,我们使用了一个SCNVector3的实例,并为 X、Y 和 Z 坐标提供浮点值,其中 1f 实际上是 1 米,0.1f 10 厘米,0.01f 1 厘米。

一旦我们创建了SCNVector3的实例,有效地声明了 3D 空间中的位置,我们就可以使用 nodes Position属性为它设置一个节点位置。

public override void ViewDidAppear(bool animated)
{
    base.ViewDidAppear(animated);

    this.sceneView.Session.Run(new
      ARWorldTrackingConfiguration());

    // Creates and assigns a position to a node
    // In this case it is setting it 1m above and 1m in front
    // of the devices initial position
    var position = new SCNVector3(0, 1f, -1f);
    var node = new SCNNode();
    node.Position = position;

    // Adds the node to the scene
    // (will be invisible as we haven't told it what
    // to look like yet)
   this.sceneView.Scene.RootNode.AddChildNode(node);
}

Listing 2-3Setting the position of an object in 3D space

可能值得注意的是,在将对象放置到场景中后,您可以随时更改对象的位置,只需用具有不同 X、Y 和 Z 坐标值的SCNVector3实例更新其Position属性即可。

Hint

我花了一段时间才想起来,要把一个物体放在我面前,我必须把它放在 Z 值为负的地方(也就是你面前)。把一个东西放在 Z 轴的正方向实际上是把它放在你的后面。多。很多次,我都在困惑地寻找一个我放在前面场景中的物体,而实际上它就在我后面!

世界起源

默认情况下,当您启动 AR 应用时,您的世界原点是应用启动时您的设备所在的点。默认世界原点的位置将是(0,0,0),其中 X、Y 和 Z 都是 0。不管你将你的设备相对于世界原点移动到哪里,你放置在场景中的所有物体都将相对于世界原点,而不是你设备的当前位置。也就是说,如果需要,在应用启动后,可以通过编程方式更改世界原点的位置。

在你创建的 AR 体验中,世界原点的位置是如此准确,以至于你甚至会注意到不同的视角,这取决于你在启动应用时是坐着还是站着。

值得注意的是,如果您在将对象添加到场景时没有显式设置对象的位置,它将被放置在这个世界原点(0,0,0)。

Hint

如果你在世界原点放置一个相当大的物体,你可能看不到它,因为除非你改变了你的物理位置,否则你实际上占据了与虚拟物体相同的空间。在这种情况下,你可能需要后退一步才能看到放在世界原点的东西,因为它会在你面前。或者,当在场景中放置某物时,给它一个-Z 值,使它放在你面前。

可以想象,世界原点很重要,因为它几乎成为了你的场景或 AR 体验的系绳或中心参考点。

世界对齐

当您的ARSession启动时,它的ARWorldTrackingConfiguration将使用一个特定的WorldAlignment值来默认确定应用中轴的设置和行为,以及它的初始方向。

这很重要,因为它将决定哪个方向是向前的(-Z),因此哪个方向是向左的(-X)和向右的(+X),以及哪个方向是向上的(Y),因此哪个方向是向下的(-Y)。

如果我们愿意,我们可以改变ARWorldTrackingConfiguration的默认属性WorldAlignment,也就是WorldAlignment.Gravity

除了重力之外,还有三种不同的WorldAlignment设置可以让你的轴以不同的方式工作。

重力

Y 轴平行于重力,即直下;当应用启动时,其他轴与设备的初始方向对齐。也就是说,-Z 是应用启动时设备面对的方向,-X 在左边,X 在右边,而+Z 在后面。

例如,使用此选项并将一个对象放置在坐标为(0,0,-1f)的场景中,会将其放置在应用启动时所面对的方向上距离世界原点 1 米的位置。如果您关闭应用,转向您面对的方向,然后再次启动应用,并再次将一个对象放入场景中的(0,0,-1f),它将出现在您现在面对的方向上距离世界原点 1 米处。

在大多数情况下,WorldAlignment = ARWorldAlignment.Gravity会给你的轴你想要的行为,所以我建议你暂时坚持使用它。

重力和航向

同样,Y 轴平行于重力,尽管这次 Z 轴对准南北,X 轴对准东西。也就是说,-Z 总是北,+Z 南,-X 西,+X 东的方向。

我想如果您正在构建某种导航功能,您可能会想要使用这个设置。使用这个设置可以有效地将你的轴变成一个指南针,使你的轴始终指向北、南、东、西。

例如,使用该选项并将一个对象放置在坐标为(0,0,-1f)的场景中,会将其放置在磁北方向距离世界原点 1 米处,因此将某个对象放置在(-1f,0,0)处会将其放置在当前位置以西 1 米处,依此类推。

对于GravityGravityAndHeading来说,沿 Y 轴的任何位置都与重力对齐,其中-Y 垂直向下朝向地球中心,而+Y 垂直向上远离地球中心。

照相机

该设置对GravityGravityHeading的作用非常不同。使用WorldAlignment. Camera设置场景的坐标系,以始终匹配摄像机的方向,因此使-Z 始终与您面对的方向对齐,-X 始终在您的左侧,Y 从摄像机向上。如何定位相机将对轴系产生影响,包括 Y 轴的对齐方向。

如果这些世界排列现在看起来很混乱,不要太担心。习惯它们的一个很好的方法是打开一个调试标志,该标志将 X、Y 和 Z 轴的视觉表示放置在场景的世界原点,这在您尝试第一次 AR 体验时非常有用。我们将在第四章“内置 AR 指南”中讨论如何做到这一点

大小

ARKit 中的尺寸(嗯,实际上 SceneKit 记得吗?)存储为浮点数据类型,其中 1f 值相当于 1 米,这意味着 0.1f 相当于 10 厘米,0.01f 相当于 1 厘米。记住这一点是很有用的,因为很容易把事情做得太大(你看不到它,因为你在里面!)或者太远。巧合的是,在 AR 场景中制作一些东西的大小和位置的动画可以产生很好的效果,我们将在第 5 “动画”中学习如何做

在图 2-2 中,我们可以看到盒子的相对尺寸分别为 1 厘米、10 厘米、50 米和 1 米,以及在清单 2-4 和 2-5 中创建它们的代码。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-2

不同大小的虚拟对象

这一次,如 ViewDidAppear 方法中的清单 2-4 所示(我们将在本书中实现大部分 ar 代码),我们创建了不同大小的四个不同的 CubeNode 实例(一个我们从 SCNNode 继承的自定义类,可以在清单 2-5 中看到),并使用名为this.sceneView.Scene.RootNode.AddChildNode()的重要方法将它们添加到场景中。

我们将在下一章“节点、几何图形、材质和锚点”中更详细地讨论节点

public class CubeNode : SCNNode
{
    public CubeNode(float size, UIColor color)
    {
        var material = new SCNMaterial();
        material.Diffuse.Contents = color;

        var geometry = SCNBox.Create(size, size, size,0);
        geometry.Materials = new[] { material };

        var rootNode = new SCNNode();
        rootNode.Geometry = geometry;

        AddChildNode(rootNode);
    }
}

Listing 2-5CubeNode class

public override void ViewDidAppear(bool animated)
{
     base.ViewDidAppear(animated);

     this.sceneView.Session.Run(
       new ARWorldTrackingConfiguration());

     // 1cm
     var cubeNode1 = new CubeNode(0.01f, UIColor.Red);
     cubeNode1.Position = new SCNVector3(0, 0, 0);

     // 10cm

     var cubeNode2 = new CubeNode(0.1f, UIColor.Green);
     cubeNode2.Position = new SCNVector3(0.1f, 0, 0);

     // 50cm (0.5m)
     var cubeNode3
      = new CubeNode(0.5f, UIColor.Orange);
     cubeNode3.Position = new SCNVector3(0.5f, 0, 0);

     // 100cm (1m)
     var cubeNode4 = new CubeNode(1f, UIColor.Yellow);
     cubeNode4.Position = new SCNVector3(1.5f, 0, 0);

     this.sceneView.Scene.RootNode
        .AddChildNode(cubeNode1);
     this.sceneView.Scene.RootNode
        .AddChildNode(cubeNode2);
     this.sceneView.Scene.RootNode
        .AddChildNode(cubeNode3);
     this.sceneView.Scene.RootNode
        .AddChildNode(cubeNode4);
}

Listing 2-4Adding objects of different sizes 

配置

当您用ARSession.Run()开始一个会话时,您提供了一个ARConfiguration的实例。你希望你的增强现实应用拥有的能力和你希望它如何表现将决定你使用的配置类型。

例如,如果您想进行人脸检测,您可以向它传递一个ARFaceTrackingConfiguration实例以及一些配置变量,比如要跟踪的人脸数量。

这是我们将在本书后面看到的配置列表。

  • 启用世界跟踪,包括平面、图像和物体检测,我们在本书的大部分例子中都使用它。

  • ARFaceTrackingConfiguration启用面部跟踪,我们将在第十一章“面部跟踪和表情检测”中了解这一点

  • 启用身体追踪,我们将在第十六章“身体追踪”中了解这一点

摘要

现在,您应该已经很好地理解了启动增强现实会话以设置 AR 场景所必需的基本概念,并理解了一旦场景运行后如何找到场景周围的路,包括大小调整和轴、坐标以及定位系统。

在下一章,我们将会看到你可以在场景中放置的东西,包括节点、几何图形、材质和锚点。

三、节点、几何图形、材质和锚

在这一章中,我们将看看在增强现实体验中共同创造我们可以看到和互动的一切的构建模块。让我们开始添加东西到我们的 AR 场景中。

节点

在您的 AR 场景中,您几乎肯定会有一个或多个节点(SCNNode的实例)。默认情况下,这些节点没有任何形状或形式,因此看起来不像任何东西。我们通过应用几何图形来赋予它们形状,通过将材质应用于几何图形来赋予它们视觉外观。

你想知道你会用节点做什么?嗯,几乎所有的事情。例如,它可以简单到将显示图像的彩色 3D 球体或 2D 平面放置到场景中。这两项都是节点。

我们可以使用SCNVector3来指定节点的position,就像我们在第 2 “基本概念”中看到的那样;否则,当添加到场景中时,其默认位置将是世界原点(0,0,0)。

一个节点可以有许多子节点,这些子节点又有自己的子节点,依此类推。你想知道为什么要有子节点?嗯,如果你在一个场景中放置了 50 个节点,然后想改变所有 50 个节点的位置,你必须依次改变每个节点的位置。除非您创建一个节点,然后将这 50 个节点添加为该节点的子节点,然后您只需更改父节点的位置,子节点的相对位置就会相应地增加。

我喜欢把节点想象成乐高积木,每一块都有自己的形状、大小、外观和功能,它们本身是没用的,但是把它们放在一起,我们可以做出更好的东西,更复杂和有用的东西。

不透明

可以在一个节点上设置几个属性,包括Opacity,这是我喜欢使用的东西,即使只是微妙地使用。通过改变一个节点的不透明度,我们可以使它变得更不透明,反之亦然。

不透明度是一个浮动值,范围从 0f(完全透明)到 1f(完全不透明),默认情况下,节点的不透明度值将为 1f(完全不透明)。

在清单 3-1 中,你可以看到我们如何声明一个新的材质(SCNMaterial),在这个例子中是一个纯蓝色。然后,我们创建一个新的几何体(一种 2D 或 3D 形状),在本例中是一个高度、宽度和深度均为 1m 的盒子(SCNBox),并将材质分配给这个盒子,生成一个蓝色的盒子。然后我们创建一个新的节点(SCNNode),并将其几何图形设置为新的盒子。之后,我们设置节点的opacity为 0.5f,有效地使其 50%不透明。最后,我们通过调用this.sceneView.Scene.RootNode.AddChildNode()将节点添加到场景中。

// Create the Material
var material = new SCNMaterial();
material.Diffuse.Contents = UIColor.Blue;

// Create the Box Geometry and set its Material
var geometry = SCNBox.Create(1f, 1f, 1f, 0);
geometry.Materials = new[] { material };

// Create the Node and set its Geometry
var cubeNode = new SCNNode();
cubeNode.Geometry = geometry;

// Make the cube 50% opaque
cubeNode.Opacity = 0.5f;

// Add the Node to the Scene
// Remember, as we are not explicitly setting a position,
// The Node will appear at the WorldOrigin (0,0,0)
this.sceneView.Scene.RootNode.AddChildNode(cubeNode);

Listing 3-1Creating a simple node with shape, size, and color

不用担心,材质和几何图形将在接下来的章节中讨论。

几何

几何体是一个节点可以拥有的形状或网格,没有它们,我们的场景会非常无聊;事实上,如果没有它们,我们将只有一堆看不见的无形节点。几何图形可以是简单的形状或复杂的网格。在下面的部分中,您可以看到可供我们使用的不同类型的基本内置几何图形。

内置几何形状

有许多内置几何体形状可用于节点。但别担心。你不受这些基本形状的约束;你可以提供一个自定义的几何图形,或者在另一个工具中构建一个 3D 模型,并将其导入到你的应用中,我们将在第十三章“3D 模型”中对此进行讨论

清单 3-2 中的以下代码为节点创建了一个简单的长方体几何体,宽、高、深均为 10 厘米,然后在添加到场景中之前为其赋予红色材质。

var material = new SCNMaterial();
material.Diffuse.Contents = UIColor.Red;

var boxNode = new SCNNode();
boxNode.Geometry = SCNBox.Create(0.1f, 0.1f, 0.1f, 0);
boxNode.Geometry.Materials = new SCNMaterial[] { material };
this.sceneView.Scene.RootNode.AddChildNode(boxNode);

Listing 3-2Creating a simple 10 cm red cube

以下是我们可以使用的内置几何图形:

  • 这是一个 2D 的四边长方形或正方形;它们对于将图像放置在场景中的显示图像上或作为放置其他对象的表面非常有用。值得注意的是,您可以调整平面的CornerRadius属性,将那些尖角变成更柔和、更圆的角。

  • SCNBox–如果您选择使用相同的宽度、深度和高度值,您的箱子将像一个规则的立方体,或者通过使用不同的值,它可能更像一个扁平的邮政包裹。类似于一个SCNPlane,你可以把你的尖角变成更柔和、更圆的角,但是这次是通过改变盒子的ChamferRadius属性。

  • 一个球体,用于描绘像行星这样的东西。

  • SCNCylinder–实心圆柱形。

  • 圆环是甜甜圈或环形的一个花哨的词。

  • SCNCone–实心圆锥形,一端为圆形底座,另一端为一个点。

  • SCNTube–类似于SCNCylinder,除了这是一个空心管,像一根管子。

  • SCNText–您可以放置在场景中的 3D 文本,像大多数文本一样,您可以设置其字体和大小。

  • 就像埃及人建造的一样。

当调用其.Create()方法来定义形状的不同方面时,每个几何图形需要一组不同的参数。例如,SCNSphere.Create()只接受一个参数,即球体的半径,而SCNBox.Create()接受三个参数来定义其宽度、高度和深度。

图 3-1 显示了我们可以使用的上述不同类型的几何图形。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-1

不同类型的内置几何图形

但是,即使在创建几何体并将其指定给节点后,也只有在创建并指定材质后才能看到它。所以我们最好看看如何使用材质。

材质

您可以将一种或多种材质(SCNMaterial的实例)应用到一个几何图形中,为其提供视觉外观。我们将特别关注如何给一个项目一个纯色或者用图片包装它。

纯色材质

你可以给一个几何体一个最基本的材质是纯色,如清单 3-3 所示,其中我们将材质的Diffuse属性的Contents属性设置为UIColor.Red

// Create the Material
var material = new SCNMaterial();
material.Diffuse.Contents = UIColor.Red;

// Create the Box Geometry and set its Material
var geometry = SCNBox.Create(1f, 1f, 1f, 0);
geometry.Materials = new[] { material };

// Create the Node and set its Geometry
var cubeNode = new SCNNode();
cubeNode.Geometry = geometry;

Listing 3-3Setting a material to be a solid color

你可能想知道为什么一个几何体接受一组材质;这是因为我们可以在几何体的不同面使用不同的材质。例如,如果我们声明六种不同的材质,每种材质使用不同的颜色,并在长方体几何体的数组中提供这六种材质,那么我们将得到一个具有六个不同颜色边的长方体。

图像材质

另一种可以赋予几何体的材质是图像。如果我们想在一幅图像中包裹一个几何图形,或者把一幅图像放在一个 2D 平面上,这是很有用的。注意这一次,我们设置了一个UIImage给材质扩散内容属性,如清单 3-4 所示。这个内容属性接受一些不同的类型,包括我们已经看到的UIColorUIImage

// Load the image
var image = UIImage.FromFile("img/pineapple.jpg");

// Create the Material
var material = new SCNMaterial();
material.Diffuse.Contents = image;
material.DoubleSided = true;

// Create the Plane Geometry and set its Material
var geometry = SCNPlane.Create(1f, 1f);
geometry.Materials = new[] { material };

// Create the Node and set its Geometry
var rootNode = new SCNNode();
rootNode.Geometry = geometry;

// Add the Node to the Scene
this.sceneView.Scene.RootNode.AddChildNode(rootNode);

Listing 3-4Setting a material to be an image

Hint

如果您不使用material.DoubleSided = true,那么您的几何图形可能只有在从某些角度查看时才可见。

值得一提的是,也可以使用包含透明度的 PNG 图像,并且会保持透明度。例如,如果您创建了一个包含一些文本的透明 PNG,并将该图像用作SCNPlane上的材质,您将只能看到浮动文本。这是一个非常有用和漂亮的效果。

材质填充模式

默认情况下,材质的填充模式是实心的。但是,您始终可以将填充模式更改为线条,以查看组成形状的网格。在清单 3-5 和图 3-2 中,你可以看到球体几何体的填充模式可以是实线或线条。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-2

不同的材质填充模式

var material = new SCNMaterial();
material.Diffuse.Contents = colour;
material.FillMode = SCNFillMode.Lines;

Listing 3-5Material fill modes

锚点是自动检测或手动放置在场景中的参考点。例如,像我们在第十章“图像检测”中所做的那样进行图像检测时,ImageAnchor 会自动放置在场景中被检测图像的位置。它们有助于将我们的虚拟物体与现实世界联系起来。

我们将在本书中使用的锚包括

  • ARPlaneAnchor–代表场景中检测到的水平或垂直平面,我们将在第九章“平面检测”中使用,以帮助可视化墙壁、地板和表面。

  • ARImageAnchor–代表在场景中检测到的图像,我们将在第十章“图像检测”中使用,检测场景中预定义的图像。

  • ARFaceAnchor–代表场景中检测到的人脸,我们将在第十一章“人脸跟踪和表情检测”中使用,在这里我们可以向检测到的人脸几何图形添加其他节点,甚至检测一系列的面部表情。

  • ARObjectAnchor–代表场景中检测到的物体,我们将在第十五章“物体检测”中使用,在场景中检测到预定义的“扫描”3D 物体的形状。

  • ARBodyAnchor–代表场景中检测到的身体,我们将在第十六章“身体跟踪”中使用它来跟踪场景中身体的位置和方向。

锚点对于跟踪我们 AR 体验中感兴趣点的存在和位置至关重要。

要尝试的事情

使用本章中讨论的概念,如节点、几何形状和材质,并将它们与第二章“基本概念”中讨论的内容相结合,如定位和尺寸,你现在应该能够自己尝试一些事情。

这里有一些让你开始的想法。

用基本的几何图形和材质制作雪人。

首先创建一个节点,然后添加其他具有不同位置、大小和材质的基本几何体的节点来创建一个基本雪人。你可以从白色球体作为身体和头部开始,黑色球体作为眼睛,棕色球体作为按钮,黑色圆柱体作为帽子。

看看你能在场景中不同的地方放置多少物品。

现在你知道了如何在不同的地方放置物品,看看你能用一个大的fordo while循环在场景的不同位置放置多少个。你甚至可以使用Random,将它们放置在任意位置。

在场景中放置不同大小的物品。

感受一下虚拟的 1 厘米、10 厘米和 1 米的物体在场景中有多大。

在场景中放置不同颜色和不透明度的物品。

使用不同颜色的材质,创建不同颜色的节点,看看它们在不同的opacity值下是什么样子。

创建透明的 png,并将其用作几何材质。

创建一个透明的 PNG,给它添加一些大的厚文本,并使用该图像作为SCNPlane的素材,看看以这种方式使用透明 PNG 有多有效。

看你能做多大或多小的节点。

看看你能在场景中放置多小的一个物体而仍然能看到它;然后看你能在一个场景中放置多大的物品(对于后者,你可能需要把它放置在离你很远的地方;否则,如果你占据了与项目相同的空间,你就有被项目内部的风险。

摘要

我们已经讨论了 ARKit 中作为增强现实的物理构建块的节点,我们将大量使用这些节点,如何利用内置的几何形状,如何为它们提供视觉外观,以及如何将它们放置在场景中。

在下一章,我们将看看一些内置工具和指南,我们可以用它们来帮助开发和理解我们的增强现实场景。

四、内置 AR 指南

ARKit 附带了一些有用的内置指南和工具,可以在开发您的第一次增强现实体验时提供帮助。我们可以在设置场景时通过在SCNDebugOptions中设置它们的标志来启用其中一些。

显示特征点

我建议你在创建第一个应用时,打开标志来显示功能点。它有助于向您展示应用和相机对照明条件和表面的依赖程度。但是,在以后的应用中,您将很少需要打开此功能。

您可以通过设置清单 4-1 中所示的DebugOptions标志来启用它。

public ViewController(IntPtr handle) : base(handle)
{
    this.sceneView = new ARSCNView
    {
        DebugOptions = ARSCNDebugOptions.ShowFeaturePoints
    };

    this.View.AddSubview(this.sceneView);
}

Listing 4-1Enabling feature points in the code

ShowFeaturePoints调试被激活时,你会看到黄色的点出现在你场景的表面上,如图 4-1 所示。丰富的特征点意味着 ARKit 可以检测场景中的许多特征点。这很好,因为 ARKit 使用特征点来帮助保持虚拟对象在场景中的位置。

你会注意到,当打开ShowFeaturePoints并在光线不好的环境中或对着毫无特色的表面(如普通的墙壁或玻璃表面)运行你的应用时,黄点会少得多。这有助于确认,为了让您的应用以最佳方式运行,它应该在光线充足、功能丰富的环境中使用。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-1

显示场景中的特征点有助于我们理解应用如何在场景中寻找兴趣点

显示世界原点和坐标轴

正如我们在第二章“基本概念”中介绍位置概念时简要提到的,可以打开显示世界原点 X、Y 和 Z 坐标轴的指南,如清单 4-2 所示。这可以帮助我们定位,提醒我们 X、Y 和 Z 轴在哪个方向,如图 4-2 所示。

由于轴显示在世界原点,它指示会话开始时设备的位置,即位置 0,0,0。请记住,添加到场景中的节点,如果没有给出具体的位置,将会出现在世界原点。

public ViewController(IntPtr handle) : base(handle)
{
    this.sceneView = new ARSCNView
    {
        DebugOptions = ARSCNDebugOptions.ShowWorldOrigin
    };

    this.View.AddSubview(this.sceneView);
}

Listing 4-2Enabling WorldOrigin helper

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-2

在世界原点显示坐标轴

请注意,您可以同时启用多个调试选项。例如,在清单 4-3 中,您可以看到我们在场景中显示了特征点和世界原点/轴。

public ViewController(IntPtr handle) : base(handle)
{
    this.sceneView = new ARSCNView

    {
        DebugOptions
        = ARSCNDebugOptions.ShowFeaturePoints |
          ARSCNDebugOptions.ShowWorldOrigin
    };

    this.View.AddSubview(this.sceneView);
}

Listing 4-3Enabling multiple debug options

显示统计数据

通过打开清单 4-4 中所示的ShowStatistics选项,并按下底部栏上的+按钮,当您的应用运行时,附加信息会显示在屏幕底部,如图 4-3 所示。统计视图显示了一些有用的信息,尤其是当你的应用运行缓慢或者不如你希望的那样流畅时。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-3

显示统计数据提供了关于场景渲染所花费的工作量的信息

public ViewController(IntPtr handle) : base(handle)
{
    this.sceneView = new ARSCNView {
        ShowsStatistics = true
    };

    this.View.AddSubview(this.sceneView);
}

Listing 4-4Enabling Statistics in the code

统计视图以每秒帧数(fps)显示帧速率,以及视图的 GPU 使用情况。如果 fps 开始下降得太低,您将需要密切关注它;60 fps 是最大值,30 以上的值也是可以接受的。它还显示了场景中的节点数(菱形)和多边形数(三角形)。如果您的应用开始遇到性能问题,您可能希望显示统计数据来调查可能导致速度下降的原因。

教练覆盖

由于应用了解其周围环境以在场景中准确地运行和放置事物非常重要,因此,为了帮助实现这一点,您可以使用内置的指导覆盖,鼓励用户移动他们的相机,直到应用收集到足够的信息,以便能够准确地了解场景。您可以向您的应用添加一个教练覆盖图,如清单 4-5 所示。

public partial class ViewController : UIViewController, IARCoachingOverlayViewDelegate
{
    private readonly ARSCNView sceneView;
    ARCoachingOverlayView coachingOverlay;

    public ViewController(IntPtr handle) : base(handle)
    {
        this.sceneView = new ARSCNView();
        this.View.AddSubview(this.sceneView);
    }

    public override void ViewDidLoad()

    {
        base.ViewDidLoad();
        this.sceneView.Frame = this.View.Frame;
    }

    public override void ViewDidAppear(bool animated)
    {
        base.ViewDidAppear(animated);

        this.sceneView.Session.Run(new
           ARWorldTrackingConfiguration {
           PlaneDetection = ARPlaneDetection.Horizontal,
        });

        coachingOverlay = new ARCoachingOverlayView();
        coachingOverlay.Session = sceneView.Session;
        coachingOverlay.Delegate = this;
        coachingOverlay.ActivatesAutomatically = true;
        coachingOverlay.Goal = ARCoachingGoal.HorizontalPlane;
        coachingOverlay.TranslatesAutoresizingMaskIntoConstraints = false;

        sceneView.AddSubview(coachingOverlay);

        // Keeps the coaching overlay in the center of the screen

        var layoutConstraints = new NSLayoutConstraint[]
        {
            coachingOverlay.CenterXAnchor.ConstraintEqualTo(
               View.CenterXAnchor),
            coachingOverlay.CenterYAnchor.ConstraintEqualTo(
               View.CenterYAnchor),
            coachingOverlay.WidthAnchor.ConstraintEqualTo(
               View.WidthAnchor),
            coachingOverlay.HeightAnchor.ConstraintEqualTo(
               View.HeightAnchor),
        };

        NSLayoutConstraint.ActivateConstraints(
           layoutConstraints);
    }

    public override void ViewDidDisappear(bool animated)
    {
        base.ViewDidDisappear(animated);
        this.sceneView.Session.Pause();
    }

    public override void DidReceiveMemoryWarning()
    {
        base.DidReceiveMemoryWarning();
    }
}

Listing 4-5Enabling coaching overlay in code

结果如图 4-4 所示;一个透明的动画图像覆盖在屏幕上,鼓励用户移动手机;在它充分理解了这个场景之后,它就消失了。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-4

教练覆盖图可以帮助指导用户实现一个目标(例如检测一架飞机)

摘要

当你第一次开始创建和熟悉增强现实体验时,其中一些内置指南可能会很有用,但当你开始发布和分发你的应用时,你几乎肯定会想禁用它们。

在下一章中,我们将会看到我最喜欢的、令人印象深刻的创造引人入胜的体验的特性之一,那就是动画,它对于给你的体验一种动态的感觉至关重要。

五、动画

让你的增强现实应用看起来令人印象深刻的一个简单方法是通过动画制作一个或多个节点来添加一点点运动。不然看起来会有点静态和做作。这可能就像淡入淡出节点或动画显示它们的位置或大小一样简单,幸运的是,这很容易做到。

从技术上讲,在 SceneKit 中,我们将使用一个叫做SCNAction的东西。但是因为我们要看的动作是激活我们的动画的,所以在本章中我将把动作称为动画。

动画不透明度

通过对场景中的一个或多个对象的不透明度进行动画处理,可以实现像淡入和淡出它们的外观这样的漂亮效果。清单 5-1 展示了如何将一个节点的不透明度从 0f(零不透明度)设置为 1f(完全不透明度)。

var material = new SCNMaterial();
material.Diffuse.Contents = UIColor.Blue;

var geometry = SCNSphere.Create(0.5f);
geometry.Materials = new[] { material };

var opacityAction = SCNAction.FadeOpacityTo(1f, 3);
var sphereNode = new SCNNode();
sphereNode.Geometry = geometry;
sphereNode.Opacity = 0f;
sphereNode.RunAction(opacityAction);
this.sceneView.Scene.RootNode.AddChildNode(sphereNode);

Listing 5-1Fading in a node from 0% opacity to 100% opacity over 3 seconds

将一个物品制作成动画是将物品引入你的虚拟环境的好方法,如图 5-1 所示。感觉比一眨眼突然出现的东西自然多了。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-1

在 3 秒钟内将不透明度从 0f 更改为 1f

动画缩放

虽然设置对象比例(大小)的动画是可能的,但我建议只使用比例的微小变化来达到所需的效果。可以在 X、Y 和 Z 轴(或所有方向)上设置对象缩放的动画。清单 5-2 展示了如何在一秒钟内将一个节点的大小缩放到其原始大小的 10%。

var material = new SCNMaterial();
material.Diffuse.Contents = UIColor.Yellow;

var geometry = SCNSphere.Create(0.2f);
geometry.Materials = new[] { material };

var scaleAction = SCNAction.ScaleBy(0.1f, 1);
var sphereNode = new SCNNode();
sphereNode.Geometry = geometry;
sphereNode.RunAction(scaleAction);
this.sceneView.Scene.RootNode.AddChildNode(sphereNode);

Listing 5-2Decreasing a nodes size by 90% over a second

而图 5-2 显示的是收缩球体动画。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-2

在 1 秒钟内将节点的比例更改为其原始大小的 10%

动画位置

可以将一个节点的位置从一个位置动画到另一个位置,这可以使用清单 5-3 中的代码来实现。您可能希望使用此动画来使节点离您更近或更远。

var material = new SCNMaterial();
material.Diffuse.Contents = UIColor.Blue;

var geometry = SCNSphere.Create(0.5f);
geometry.Materials = new[] { material };

var positionAction = SCNAction.MoveBy(new SCNVector3(0, 0.5f, 0f), 3);
var sphereNode = new SCNNode();
sphereNode.Geometry = geometry;
sphereNode.RunAction(positionAction);
this.sceneView.Scene.RootNode.AddChildNode(sphereNode);

Listing 5-3Moving a node’s position 0.5 meter in the Y axis over 3 seconds

制作节点位置的动画有助于我们将它们从场景中枯燥的静态对象变成动态移动的对象。

动画定向

想要旋转一个节点?要么降低几度,要么让它旋转?嗯,可以,如清单 5-4 所示。在我们的场景中旋转对象可以帮助显示它们具有一定的自由度,而不是完全静止的。

var material = new SCNMaterial();
material.Diffuse.Contents = UIColor.Green;

var geometry = SCNBox.Create(0.1f, 0.1f, 0.1f, 0);
geometry.Materials = new[] { material };

var rotateAction = SCNAction.RotateBy(
   0, (float)(Math.PI), 0, 3);

var cubeNode = new SCNNode();
cubeNode.RunAction(rotateAction);
this.sceneView.Scene.RootNode.AddChildNode(cubeNode);

Listing 5-4Rotating a node by 360 degrees over 3 seconds

结果是一个缓慢旋转的立方体,如图 5-3 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-3

在 3 秒内将立方体旋转 360 度

重复行为

在前面的例子中,动画默认运行一次。如果你愿意,很容易让它们运行预定的次数,如清单 5-5 所示,或者重复运行,如清单 5-6 所示。

var rotateAction = SCNAction.RotateBy(
   0, (float)(Math.PI), 0, 3);

var repeatRotationForever =
   SCNAction.RepeatActionForever(rotateAction);

sphereNode.RunAction(repeatRotationForever);

Listing 5-6Repeating a rotate action indefinitely

var rotateAction = SCNAction.RotateBy(
   0, (float)(Math.PI), 0, 3);

var repeatRotationFiveTimes =
   SCNAction.RepeatAction(rotateAction, 5);

sphereNode.RunAction(repeatRotationFiveTimes);

Listing 5-5Repeating a rotate action five times

动画放松

我喜欢把放松比作开车时的加速和减速。从静止状态开始,需要一段时间来达到你想要的速度,也需要一段时间来让车减速停下。这是放松。动画在不同的时间以不同的速度播放。缓动的替代方法是线性动画,其中动画的速度从头到尾都是恒定的。清单 5-7 展示了如何在你的动画中使用缓动。

你可能想知道什么时候你可能想使用宽松。就我个人而言,我认为它让动画看起来比默认的线性更“自然”。缓和的选项有EaseInEaseOutEaseInEaseOut,Linear

var opacityAction = SCNAction.FadeOpacityTo(1f, 3);
opacityAction.TimingMode = SCNActionTimingMode.EaseInEaseOut;
sphereNode.Opacity = 0f;
sphereNode.RunAction(opacityAction);

Listing 5-7Easing animations can make them look more natural than their linear counterparts

组合动画

要创建更有趣的动画,您可以用几种方式组合它们。例如,您可以淡入一个节点,同时将它向您移动(沿 Z 轴),同时使它增长(放大)。

您可以组合这些动画,使它们同时发生或按顺序发生,如清单 5-8 所示。

var opacityAction = SCNAction.FadeOpacityTo(1f, 1);
var scaleAction = SCNAction.ScaleBy(1.2f, 1);
var positionAction = SCNAction.MoveBy(
   new SCNVector3(0, 0, -0.1f), 1);

// Would run the actions all at the same time
var simultaneousActions = SCNAction.Group(new SCNAction[] {
       opacityAction, scaleAction, positionAction });

sphereNode.RunAction(simultaneousActions);

// Would run the actions one after another
var sequentialActions = SCNAction.Sequence(new SCNAction[] {
      opacityAction, scaleAction, positionAction });

sphereNode.RunAction(sequentialActions);

Listing 5-8You can group animations to play simultaneously or sequentially

因为 SCNAction。组()和操作。Sequence()返回 SCNAction,您可以继续将这些组和序列分组或排序到“其他”组和序列中。

等待

如果你想在动画之前或动画之间等待一会儿,你可以使用SCNAction.Wait(numberOfSeconds)来延迟你的动画序列。代码很简单,如清单 5-9 所示。

var waitAction = SCNAction.Wait(1);

Listing 5-9You can use wait actions to have even greater control over the timing of your animations

摘要

因此,到现在为止,您的思维应该已经在与移动、缩放和淡化场景中的节点的方式赛跑,以创建引人入胜、动态和有趣的 AR 体验。请记住,虽然巧妙使用动画是强大的,但太多的动画很容易让人不知所措。学会如何取得平衡取决于你。

在下一章中,我们将关注约束,它可以使节点更容易以特定的方式运行。听起来很神秘,对吧?好了,翻到下一页,让我们看看约束能为我们做些什么。

六、约束

在节点上使用约束允许我们以某种方式约束它们的行为。使用它们,您可以使节点,例如,总是面对摄像机或总是面对另一个节点,如果你愿意。

广告牌约束

我推测这种效应是以你作为一名汽车乘客在路过广告牌时看着它的经历命名的。

如果将该约束应用于节点,它将始终面向摄影机。如果你想知道为什么你可能需要它,想象一下如果你有一个标志或标签提供你想让用户总是能够看到的信息。这将是SCNBillboardConstraint的一个很好的用例。从清单 6-1 中可以看出,向节点添加约束非常简单。

var rootNode = new SCNNode
{
    Geometry = CreateGeometry(),
    Constraints = new[] { new SCNBillboardConstraint() }
};

Listing 6-1Have a node always face the camera using a SCNBillboardConstraint

动画

LookAtConstraint在某些方面与BillboardConstraint相似;然而,这个约束告诉节点总是看着(面对)一个特定的节点。

之前,我已经用这个让一些周围的节点“看着”一个中心不可见的节点,效果很好,如图 6-1 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-1

您可以使用 LookAtConstraints 来指向节点以查看其他节点

这个效果是使用清单 6-2 中所示的代码实现的。

var lookAtConstraint = SCNLookAtConstraint.Create(targetNode);
lookAtConstraint.GimbalLockEnabled = true;
imagePlaneNode.Constraints = new SCNConstraint[]
{
   lookAtConstraint
};

Listing 6-2Use “SCNLookAtConstraint” to make nodes always face another node

如果相机旋转,使用GimbalLockEnabled=true停止节点水平旋转。

其他约束

我们可以从 SceneKit 中使用许多其他更高级的约束;然而,它们超出了本入门书的范围。它们包括

  • SCNOrientationConstraint

  • SCNTransformConstraint

  • 约束条件

  • scnavoidoccluderconstrait

  • SCNAccelerationConstraint

  • SCNSliderConstraint

  • SCNReplicatorConstraint

  • SCNIKConstraint

要尝试的事情

玩 LookAtConstraint 。在世界原点放置一个没有几何体(因此不可见)的节点。将多个 2D 平面添加到其节点设置为查看世界原点节点的场景中。

玩广告牌约束。将多个 2D 平面添加到其节点有一个SCNBillboardConstraint的场景中,并注意它们如何总是面向相机。

摘要

SCNBillboardConstraintSCNLookAtConstraint约束是约束节点行为的有效方法,特别有用,因为它们意味着你不需要使用复杂的数学来计算达到相同效果所需的精确角度。

在下一章中,我们将看一下照明,乍看之下,它似乎并不那么重要,但如果不考虑到你的 AR 体验,它实际上可以使 AR 体验变得更好或更差。

七、照明设备

事实证明,在让我们的 AR 场景看起来逼真的时候,照明是极其重要的。例如,如果是一个明亮的日子,但我们在场景中放置了一个黑暗的物体,它看起来非常人工;反过来,同样的道理也适用于将一个非常亮的物体放在黑暗的环境中。因此,在可能的情况下,我们希望在场景中考虑真实世界的光照条件。

创建逼真的 AR 体验的另一个考虑因素是阴影。

如果在场景中将对象放置在光源(如太阳)和表面(如地板)之间,您的大脑会看到阴影。我们可以创建这些假阴影,使我们的场景看起来像在现实世界中一样。

自动添加默认照明

默认情况下,当ARSCNView.AutoenablesDefaultLighting的默认值为真时,“默认”照明会添加到场景视图中。这将在场景中放置一个全向光源,指向与相机相同的方向。这对于您最初的 AR 创建来说可能是好的,但是如果您想要对特定的照明实例进行更多的控制,您可能希望通过设置AutoenablesDefaultLighting=false来关闭它。

自动更新默认照明

我们可以使用ARSCNView.AutomaticallyUpdatesLighting属性将默认照明添加到试图模拟真实世界照明条件的场景中。因此,如果真实世界的光照发生变化,人造光也会发生变化。同样,这在默认情况下是正确的,如果你希望对场景中的照明有更多的控制,你可以设置AutomaticallyUpdatesLighting=false,如清单 7-1 所示。

public ViewController(IntPtr handle) : base(handle)
{
     this.sceneView = new ARSCNView
     {
          AutoenablesDefaultLighting = false,
          AutomaticallyUpdatesLighting = false
     };

     this.View.AddSubview(this.sceneView);
}

Listing 7-1A default light source is added to the scene, but you can turn it off if you want to have more control/add your own light sources

灯光类型

除了完全依赖默认照明,还可以通过向SCNNode添加一个SCNLight实例来在场景中放置一个或多个特定光源。

您可以使用以下不同类型的光源(SCNLight.Type):

  • 环境–向各个方向均匀发光。

  • 方向性–以均匀的强度向某个方向发射光线,因此其发射位置无关紧要。无论放在 10 厘米还是 1 米远的地方,看起来都一样。

  • 泛光灯–类似于定向灯,但是它的位置可以决定光线的强度。如果光源的距离在场景中很重要,请使用此选项。

  • 聚光灯–类似于泛光灯,但光的强度逐渐减弱,形成一个光锥。

在现实世界中,光线从多个表面反射,照亮一个区域。我们能模仿的最接近的方法是添加一个Ambient光源。那么为了更好的表现一些实际的光源,我们可以使用Directional灯。因此,向场景中添加多种类型的光源并不罕见。

清单 7-2 中的例子显示了一个平行光被添加到一个SCNNode中,并指向正下方,有效地照亮了放置在它下面的任何节点的顶部。

var light = SCNLight.Create();
light.LightType = SCNLightType.Directional;
light.Intensity = 2000f;
light.ShadowColor = UIColor.Black.ColorWithAlpha(0.5f);
light.ShadowRadius = 4;
light.ShadowSampleCount = 4;
light.CastsShadow = true;

var lightNode = new SCNNode();
lightNode.Light = light;
lightNode.EulerAngles = new SCNVector3((float)-Math.PI / 2, 0, 0);

Listing 7-2You create a light source and add it to a SCNNode

Note

如果场景中唯一的虚拟光源是平行光,任何平行于光源方向的表面都将是黑色的。

如果我们愿意,我们可以做一些聪明的事情,将这种光放置在场景中的其他节点上,大致模拟太阳,并在地面上的平面上投射阴影,如下一节所示。

添加阴影

让你的物体看起来像在场景中投射阴影一样简单,只需在物体上方添加一个光源(SCNLight)并在物体下方添加一个透明平面,作为阴影投射的表面,如图 7-1 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-1

由虚拟光源投射在虚拟立方体下面的虚拟平面上显示的虚拟阴影

有阴影和没有阴影的体验相差十万八千里。没有阴影,虚拟立方体看起来仍然存在于场景中,但是我们了解它的位置以及它离地面有多高的唯一方法是四处移动。然而,包括阴影在内会立即给我们一个更清晰的指示,告诉我们立方体的位置和它离地面的高度。

在清单 7-3 中,因为我们想要包含平面检测,所以我们使用了一个ARSCNViewDelegate,这一次,我们将让我们的 ViewController 实现它,并将我们的场景视图委托设置为类本身(this)。

ViewDidAppear中,我们在ARWorldTrackingConfiguration中启用水平面检测。我们也在创建一个平行光的实例,设置它的属性,比如强度、方向等等,然后创建一个SCNNode保持光线,然后将包含光线的节点放置在场景中。

然后,我们创建一个立方体形状的物体,并将其添加到场景中,确保将它放置在灯光节点的下方。

然后在DidUpdateNode方法中,我们确保探测到的飞机的光照模型的材质是SCNLightingModel.ShadowOnly,有效地使它对除了投射阴影之外的所有物体透明。

public partial class ViewController : UIViewController, IARSCNViewDelegate
    {
        private readonly ARSCNView sceneView;

        public ViewController(IntPtr handle) : base(handle)
        {
            this.sceneView = new ARSCNView
            {
                AutoenablesDefaultLighting = true,
                AutomaticallyUpdatesLighting = true,
                Delegate = this
            };

            this.View.AddSubview(this.sceneView);
        }

        public override void ViewDidLoad()
        {
            base.ViewDidLoad();
            this.sceneView.Frame = this.View.Frame;
        }

        public override void ViewDidAppear(bool animated)
        {
            base.ViewDidAppear(animated);

            var configuration
               = new ARWorldTrackingConfiguration
            {
                AutoFocusEnabled = true,
                PlaneDetection = ARPlaneDetection.Horizontal,
                LightEstimationEnabled = true,
                WorldAlignment = ARWorldAlignment.Gravity,
                EnvironmentTexturing =
                   AREnvironmentTexturing.Automatic
            };

            this.sceneView.Session.Run(configuration);

            var light = SCNLight.Create();
            light.LightType = SCNLightType.Directional;
            light.Intensity = 2000f;
            light.ShadowColor =
               UIColor.Black.ColorWithAlpha(0.5f);
            light.ShadowRadius = 4;
            light.ShadowSampleCount = 4;
            light.CastsShadow = true;

            var lightNode = new SCNNode();
            lightNode.Light = light;
            lightNode.EulerAngles

               = new SCNVector3((float)-Math.PI / 2, 0, 0);

            var cube = SCNBox.Create(0.1f, 0.1f, 0.1f, 0.02f);
            var metal = SCNMaterial.Create();
            metal.LightingModelName =
               SCNLightingModel.PhysicallyBased;
            metal.Roughness.Contents = new NSNumber(0.1);
            metal.Metalness.Contents = new NSNumber(1);
            cube.FirstMaterial = metal;

            var cubeNode = new SCNNode();
            cubeNode.Geometry = cube;
            cubeNode.CastsShadow = true;

       this.sceneView.Scene.RootNode
          .AddChildNode(lightNode);

       this.sceneView.Scene.RootNode
          .AddChildNode(cubeNode);
        }

        [Export("renderer:didUpdateNode:forAnchor:")]
        public void DidUpdateNode(ISCNSceneRenderer renderer,
           SCNNode node, ARAnchor anchor)
        {
            if (anchor is ARPlaneAnchor planeAnchor)
            {
                var plane =
                  ARSCNPlaneGeometry.Create(sceneView.Device);
                plane.Update(planeAnchor.Geometry);
                plane.FirstMaterial.LightingModelName =
                  SCNLightingModel.ShadowOnly;
                node.Geometry = plane;
                node.CastsShadow = false;
            }
        }
    }

Listing 7-3If you add a light source above other nodes in a scene, you can make them all cast a shadow, making the scene look more realistic

确保如果你在你的 ViewController 类上使用IARSCNViewDelegate而不是一个单独的类,你用[Export("renderer:didUpdateNode:forAnchor:")]来修饰DidUpdateNode方法,如清单 7-3 所示。这很容易忘记,因为我有很多次,想知道为什么我的影子没有出现。

Note

如果看不到任何阴影,请确保场景中的节点的CastsShadow属性设置为true

要尝试的事情

尝试不同的光源类型和照明特性。

尝试添加不同的光源到你的场景中(以及一些不同形状的节点),看看它们对它们有什么影响。尝试不同的光线强度和方向。尝试启用和禁用默认自动照明,以查看场景的效果。

投下阴影。

确保你能得到一个投射阴影的例子,最好是有多个物体,投射多个阴影,因为阴影确实能让场景看起来更真实。

摘要

虽然你可以创建增强现实体验而不考虑照明,事实上让 ARKit 甚至为你的场景添加默认照明,为了获得更真实的体验,你会想要自己手动控制场景中的照明。在指向不同方向的不同位置使用不同强度的不同类型的灯具。

正如我们所见,添加人工阴影为我们的体验增加了额外的可信度,因为我们希望现实世界中的物体能够投射阴影,所以让我们的虚拟物体在可能的情况下投射阴影是有意义的。

在下一章中,我们将会看到更多让用户参与到我们的体验中的方法,这次是使用视频和声音。

八、视频和声音

要为您的增强现实体验添加另一个互动维度,您可以将声音和视频融入到您的场景中。当它们是与场景中的项目交互的结果时,这尤其有效。

播放声音

播放声音是一件非常简单的事情;您只需使用AVAudioPlayer的一个实例,向它提供一个声音文件的位置(确保您已经将它添加到您的项目中),并调用.Play(),如清单 8-1 所示。

NSUrl songURL = new NSUrl($"Sounds/sound.mp3");
NSError err;
AVAudioPlayer player
    = new AVAudioPlayer(songURL, "Song", out err);
player.Volume = 0.5f;
player.FinishedPlaying += delegate {
    player = null;
};
player.Play();

Listing 8-1Playing sound in an AR scene

由于声音是反馈与应用交互的一种很好的方式,例如,如果你愿意,你可以在场景中按下SCNNode时播放声音。或者你可以在应用首次加载时播放声音。

播放视频

你必须看到它才会相信,因为它看起来有点可怕,但你可以在你的增强现实场景中播放视频,这几乎就像虚拟电视屏幕或显示器一样。

在这个例子中,我们需要使用一个SKVideoNodeSKScene来播放视频。

在清单 8-2 中,您可以看到我们使用SCNMaterial将视频放在 2D 的飞机上。由于这是一种材质,您可以在其他地方使用它,例如,在 3D 盒子的侧面。

public override void ViewDidAppear(bool animated)
{
    base.ViewDidAppear(animated);
    this.sceneView.Session.Run(new
       ARWorldTrackingConfiguration {
        LightEstimationEnabled = true,
        WorldAlignment = ARWorldAlignment.GravityAndHeading

       });
    var videoNode
      = new SKVideoNode("Videos/big-buck-bunny-wide.mp4");

    // Without this the video will be inverted upside down and
    // back to front
    videoNode.YScale = -1;
    videoNode.Play();

    var videoScene = new SKScene();
    videoScene.Size = new CoreGraphics.CGSize(640, 360);
    videoScene.ScaleMode = SKSceneScaleMode.AspectFill;
    videoNode.Position
      = new CoreGraphics.CGPoint(videoScene.Size.Width / 2,
         videoScene.Size.Height / 2);
    videoScene.AddChild(videoNode);

    // Set to be the same aspect ratio as the video itself
   //(1.77)
    var width = 0.5f;
    var length = 0.28f;

    var material = new SCNMaterial();
    material.Diffuse.Contents = videoScene;
    material.DoubleSided = true;

    var geometry = SCNPlane.Create(width, length);
    geometry.Materials = new[] { material };

    var planeNode = new SCNNode();
    planeNode.Geometry = geometry;
    planeNode.Position = new SCNVector3(0, 0, -0.5f);

    this.sceneView.Scene.RootNode.AddChildNode(planeNode);
}

Listing 8-2Playing video in an AR scene

在清单 8-2 中,您还会注意到我们必须使用 SceneKit 中的一些东西来播放视频,包括SKSceneSKVideoNode.

在图 8-1 中,你可以看到一架漂浮的 2D 飞机如何显示正在播放的视频。甚至有可能改变它的不透明度,使其半透明或产生阴影,如第 7 “照明”一章所讨论的

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-1

在漂浮的 2D 飞机上播放视频

要尝试的事情

同时播放多个视频。

看看能不能在多个平面节点上同时播放同一个视频文件;然后看看能不能在不同的节点上同时播放不同的视频文件。

看看有多少个节点可以同时完成这项工作。5?50?

在巨大的飞机上播放视频。

梦想过 80 英寸的电视吗?看看你能否通过在一架巨大的 2D 飞机上播放一个电影文件来重现这一场景。

摘要

在增强现实体验中使用声音有助于在用户与你的应用交互时提供听觉反馈,使用视频有助于吸引用户、娱乐用户或与用户交流。两者都为用户提供了更高水平的参与度。

在下一章中,我们将看看平面检测,它可以识别地面和墙壁等表面。一旦我们探测到这些表面,我们就可以用它们做一些有趣的事情。

九、平面检测

检测表面(如地板、墙壁和表面)的能力非常重要,因为这些决定了我们场景环境的约束,也使我们能够在其上放置东西。

这种 AR 功能的商业应用很有趣。一些企业已经使用它来检测墙壁,并将墙纸或图片等产品放在墙上,让客户在购买前预览家中的物品。

探测飞机

可以将平面检测设置为仅检测水平、垂直或水平和垂直平面。

在平面检测期间,随着相机四处移动并检测其环境的更多部分,它可以检测新的平面或更新其对已经检测到的平面的理解。

当检测到一个新的平面时,一个ARPlaneAnchor被放置在检测到的位置。该锚点保存有关检测到的平面的详细信息,如其类型(水平或垂直)、位置、方向、宽度和长度,并自动赋予一个唯一的 ID,以便与其他平面区分开来。

请记住,低光照条件和无特色或反射表面将阻碍 ARKit 探测飞机的能力。例如,ARKit 将很难检测到普通的白墙或灯光昏暗的房间中的墙壁。

记住飞机

在你的应用中跟踪探测到的飞机通常是需要的和有用的。在清单 9-2 以及检测平面的代码中,您将看到将检测到的平面存储在一个变量中的代码,以便于以后检索。

ARSCNViewDelegate(场景视图代理)

一般来说,创建一个专用的类(ARSCNViewDelegate的实例)来处理当不同的锚点被检测到并放置在场景中时触发的事件,例如,当平面、图像或人脸被检测到时。我们将在关于平面和图像检测以及面部跟踪的章节中进一步讨论这一点。

因此,为了启用平面检测,您需要设置您的场景视图的场景视图委托,如清单 9-1 和 9-2 所示。

public ViewController(IntPtr handle) : base(handle)
{
    this.sceneView = new ARSCNView
    {
        AutoenablesDefaultLighting = true,
        Delegate = new SceneViewDelegate()
    };

    this.View.AddSubview(this.sceneView);
}

Listing 9-1Setting a Delegate for the ARSCNView

Note

不使用单独的类作为场景视图委托,可以让 ViewController 类实现IARSCNViewDelegate并将委托设置为this(本身)。

public class SceneViewDelegate : ARSCNViewDelegate
{
    private readonly IDictionary<NSUuid, PlaneNode> planeNodes = new Dictionary<NSUuid, PlaneNode>();

    public override void DidAddNode(
       ISCNSceneRenderer renderer,
        SCNNode node, ARAnchor anchor)
    {
        if (anchor is ARPlaneAnchor planeAnchor)
        {
            UIColor colour;

            if(planeAnchor.Alignment == ARPlaneAnchorAlignment.Vertical) {
                colour = UIColor.Red;
            }
            else {
                colour = UIColor.Blue;
            }

            var planeNode = new PlaneNode(
               planeAnchor, colour);

            var angle = (float)(-Math.PI / 2);
            planeNode.EulerAngles
               = new SCNVector3(angle, 0, 0);

            node.AddChildNode(planeNode);
            this.planeNodes.Add(anchor.Identifier, planeNode);
        }
    }

    public override void DidRemoveNode(
       ISCNSceneRenderer renderer, SCNNode node,
       ARAnchor anchor)
    {
        if (anchor is ARPlaneAnchor planeAnchor) {
            this.planeNodes[anchor.Identifier].RemoveFromParentNode();
            this.planeNodes.Remove(anchor.Identifier);
        }
    }

    public override void DidUpdateNode(ISCNSceneRenderer renderer,
        SCNNode node, ARAnchor anchor)
    {
        if (anchor is ARPlaneAnchor planeAnchor) {
           this.planeNodes[anchor.Identifier]
              .Update(planeAnchor);
        }
    }
}

Listing 9-2The instance of ARSCNViewDelegate will detect and respond to events that are fired when new planes are detected or existing planes are updated

当在场景中检测到一个新的平面时,触发DidAddNode方法(并且相应的ARPlaneAnchor被添加到场景中)。当 ARKit 对现有探测平面的理解改变时,DidUpdateNode方法启动。就是平面比原来想象的要大,或者朝向不一样。我们可以向这些方法中的任何一个添加我们自己的定制代码,用这些信息做一些有趣的事情。

平面检测示例

清单 9-3 中显示了一个 ViewController 类的例子,它检测平面并根据平面是水平还是垂直在检测到的位置放置一个蓝色或红色的SCNPlane

    public partial class ViewController : UIViewController
    {
        private readonly ARSCNView sceneView;

        public ViewController(IntPtr handle) : base(handle)
        {
            this.sceneView = new ARSCNView
            {
                AutoenablesDefaultLighting = true,
                Delegate = new SceneViewDelegate()
            };

            this.View.AddSubview(this.sceneView);
        }

        public override void ViewDidLoad()
        {
            base.ViewDidLoad();
            this.sceneView.Frame = this.View.Frame;
        }

        public override void ViewDidAppear(bool animated)
        {
            base.ViewDidAppear(animated);

            this.sceneView.Session.Run(new ARWorldTrackingConfiguration

            {
                PlaneDetection = ARPlaneDetection.Horizontal | ARPlaneDetection.Vertical,
                LightEstimationEnabled = true,
                WorldAlignment = ARWorldAlignment.GravityAndHeading
            }, ARSessionRunOptions.ResetTracking | ARSessionRunOptions.RemoveExistingAnchors);
        }

        public override void ViewDidDisappear(bool animated)
        {
            base.ViewDidDisappear(animated);
            this.sceneView.Session.Pause();
        }
    }

internal class PlaneNode : SCNNode
    {
        private readonly SCNPlane planeGeometry;

        public PlaneNode(ARPlaneAnchor planeAnchor, UIColor colour)
        {
            Geometry = (planeGeometry = CreateGeometry(planeAnchor, colour));
        }

        public void Update(ARPlaneAnchor planeAnchor)
        {
            planeGeometry.Width = planeAnchor.Extent.X;
            planeGeometry.Height = planeAnchor.Extent.Z;

            Position = new SCNVector3(
                planeAnchor.Center.X,
                planeAnchor.Center.Y,
                planeAnchor.Center.Z);
        }

        private static SCNPlane CreateGeometry(ARPlaneAnchor planeAnchor, UIColor colour)
        {
            var material = new SCNMaterial();
            material.Diffuse.Contents = colour;
            material.DoubleSided = true;
            material.Transparency = 0.8f;

            var geometry = SCNPlane.Create(planeAnchor.Extent.X, planeAnchor.Extent.Z);
            geometry.Materials = new[] { material };

            return geometry;
        }
    }

public class SceneViewDelegate : ARSCNViewDelegate
    {
        private readonly IDictionary<NSUuid, PlaneNode> planeNodes = new Dictionary<NSUuid, PlaneNode>();

        public override void DidAddNode(
           ISCNSceneRenderer renderer, SCNNode node,
           ARAnchor anchor)
        {
            if (anchor is ARPlaneAnchor planeAnchor)
            {
                UIColor colour;

                if(planeAnchor.Alignment == ARPlaneAnchorAlignment.Vertical)
                {
                    colour = UIColor.Red;
                }
                else {
                    colour = UIColor.Blue;
                }

                var planeNode
                   = new PlaneNode(planeAnchor, colour);
                var angle = (float)(-Math.PI / 2);
                planeNode.EulerAngles
                   = new SCNVector3(angle, 0, 0);

                node.AddChildNode(planeNode);
                this.planeNodes.Add(anchor.Identifier, planeNode);
            }
        }

        public override void DidRemoveNode(
           ISCNSceneRenderer renderer, SCNNode node,
           ARAnchor anchor)
        {
            if (anchor is ARPlaneAnchor planeAnchor)
            {
                this.planeNodes[anchor.Identifier]
                   .RemoveFromParentNode();
                this.planeNodes.Remove(anchor.Identifier);
            }
        }

        public override void DidUpdateNode(
           ISCNSceneRenderer renderer, SCNNode node,
            ARAnchor anchor)
        {
            if (anchor is ARPlaneAnchor planeAnchor)
            {
               this.planeNodes[anchor.Identifier]
                  .Update(planeAnchor);
            }
        }
    }

Listing 9-3A full end-to-end example of plane detection

结果如图 9-1 所示。在地板与墙壁相接的地方,您可以看到检测到的垂直和水平平面的材质是如何分别变为红色和蓝色的。使用了不透明度,以便您仍然可以看到平面(墙或地板)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-1

区分检测到的水平面和垂直面

当然,正如第三章“节点、几何体、材质和锚点”中所讨论的,除了纯色,几何体材质也可以是图像。通过使用一个正方形的透明 PNG,并在被检测的平面上重复/平铺图像,可以很容易地实现如下图 9-2 所示的网格效果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-2

检测平面上使用的网格图像

关闭平面检测

平面检测可能是 CPU 密集型的;建议您一旦确定了想要的平面,就关闭平面检测,如清单 9-4 所示。

这可以通过简单地调用现有 SceneView 会话上的.Run()方法来完成,这一次将ARWorldTrackingConfigurationPlaneDetection设置为ARPlaneDetection.None

...

// Turn off plane detection
var configuration = new ARWorldTrackingConfiguration
{
    PlaneDetection = ARPlaneDetection.None,
    LightEstimationEnabled = true,
};

this.sceneView.Session.Run(configuration, ARSessionRunOptions.None)

;

...

Listing 9-4It is recommended to turn off plane detection when no longer needed

可能的应用

平面检测已经被许多企业成功使用。一些主要的家具零售商在其应用中使用它来检测地板,以允许用户在他们的客厅中放置他们家具的 3D 模型。一些壁纸和油漆零售商使用它来允许他们应用的用户预览特定壁纸或油漆在他们墙上的样子。

如果我们想让我们的虚拟物体在真实的表面上投射虚拟的阴影,就像我们在第七章“照明”中看到的那样,能够检测场景中的平面也很有用

就像 AR 的许多方面一样,你只需要发挥你的想象力,你应该有希望能够快速识别许多可能的应用。

要尝试的事情

现在您已经了解了平面检测的理论,您可以尝试以下方法来以不同的方式使用该功能。

识别检测到的垂直和水平平面,并在视觉上区分它们。

将检测到的水平和垂直平面设置为您选择的颜色,并调整不透明度。

使用在检测平面上有图像的材质。

与其给你检测到的平面几何体一个纯色,不如给它一个图像作为材质。我见过一个(平铺的)透明网格图像,用来给探测到的飞机一个有趣的外观。

关闭平面探测。

当不再需要时,练习关闭平面检测。如前所述,它是密集的,在你充分探测到你的飞机后,通常你不需要探测更多。

为您检测到的平面添加触摸交互。

阅读完第十二章“触摸手势和交互”后,回来给你检测到的平面添加触摸手势。可能改变它们的颜色或其他方面?

摘要

平面检测是一个需要理解的重要概念,因为它让你能够做许多有趣的事情,比如将物体放置在被检测的表面上。

继续我们 ARKit 内置检测能力的主题,在下一章,我们将看看图像检测,它允许我们识别场景中的预定义图像,并对它们做一些有趣的事情。

十、图像检测

图像检测是增强现实中最简单、有趣和有用的功能之一,ARKit 使它变得超级容易。

在这一章中,我们将看到如何使用 ARKit 来识别我们希望它检测的预定图像的位置。一旦我们确定了已识别图像的位置,我们就可以做额外的事情,如替换或添加它。以这种方式,图像通常被用作标记来识别 3D 空间中的位置。

将图像添加为应用资源

声明要检测的图像的一种方法是将它们与应用打包在一起。如果您在部署应用之前知道想要检测的图像,这将非常有用。

为此:

  1. Double-click the Assets.xcassets folder in Solution Explorer to see the following screen shown in Figure 10-1.

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    图 10-1

    Assets.xcassets 文件夹

  2. Click the bottom right green plus icon to bring up the “add” context menu and select “New AR Resource Group” to add a new AR Resource Group as shown in Figure 10-2.

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    图 10-2

    添加新的 AR 资源组

  3. Right-click the new AR Resource Group and choose “New AR Reference Image” as shown in Figure 10-3.

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    图 10-3

    添加新的 AR 参考图像

  4. Choose the image, provide its dimensions, and optionally rename it as shown in Figure 10-4.

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    图 10-4

    选择图像并提供尺寸

您在步骤 4 中指定的尺寸是图像在真实世界中显示的近似尺寸。您指定这些来帮助应用检测它。

检测图像

现在我们已经添加了我们想要在现实世界中检测的图像,我们需要编写代码来检测它们,并在我们的应用检测到它们时做一些有趣的事情。

正如你在清单 10-1 的构造函数中看到的,我们告诉我们的场景视图使用场景视图委托。这个类可以在清单 10-3 中看到,它有效地处理了图像检测事件。

public ViewController(IntPtr handle) : base(handle)
{
    this.sceneView = new ARSCNView
    {
        AutoenablesDefaultLighting = true,
        Delegate = new SceneViewDelegate()
    };

    this.View.AddSubview(this.sceneView);
}

Listing 10-1Setting a Scene View Delegate to use in the constructor

在清单 10-2 中,我们正在检索之前添加到图 10-4 中“AR 资源 AR 参考组”的图像,并将它们设置为我们想要检测的图像。

public override void ViewDidAppear(bool animated)
{
    base.ViewDidAppear(animated);

    var detectionImages = ARReferenceImage.GetReferenceImagesInGroup("AR Resources", null);

    this.sceneView.Session.Run(new ARWorldTrackingConfiguration
    {
        LightEstimationEnabled = true,
        WorldAlignment = ARWorldAlignment.GravityAndHeading,
        DetectionImages = detectionImages,
        MaximumNumberOfTrackedImages = 1

    });
}

Listing 10-2Declaring which images we wish to detect in the scene

在清单 10-3 中的 SceneViewDelegate 中,我们首先检查添加到场景中的锚点是否是一个ARImageAnchor。这将是我们的应用在相机视图中检测目标图像的结果。然后我们可以得到我们在图 10-4 中提供的参考图像的相应名称,这样我们就可以识别出检测到了哪张图像。

接下来,在本例中,我们要做的就是确定检测图像的尺寸,创建一个蓝色平面,并将其放置在检测图像的位置,有效地覆盖图像。

值得注意的是,一旦你在这个节点上放置了虚拟的东西,如果你改变了现实世界中检测到的图像的方向,你添加的平面的方向也会发生旋转。

这是一个非常酷的效果,显示了 ARKit 有多聪明;它能够识别检测到的图像的方向正在改变,并可以实时相应地改变虚拟节点的方向。

public class SceneViewDelegate : ARSCNViewDelegate
{
    public override void DidAddNode(
    ISCNSceneRenderer renderer, SCNNode node, ARAnchor anchor)
    {
        if (anchor is ARImageAnchor imageAnchor)
        {
            var detectedImage = imageAnchor.ReferenceImage;

            var width = detectedImage.PhysicalSize.Width;
            var length = detectedImage.PhysicalSize.Height;
            var planeNode = new PlaneNode(width, length, new SCNVector3(0, 0, 0), UIColor.Blue);

            float angle = (float)(-Math.PI / 2);
            planeNode.EulerAngles
               = new SCNVector3(angle, 0, 0);

            node.AddChildNode(planeNode);
        }
    }
}

Listing 10-3Scene View Delegate handles image detection events

在清单 10-4 中,我们可以看到一个简单的类来封装我们在清单 10-3 中使用的一个平面节点。

public class PlaneNode : SCNNode
{
    public PlaneNode(nfloat width, nfloat length,
       SCNVector3 position, UIColor colour)
    {
        var rootNode = new SCNNode
        {
            Geometry = CreateGeometry(width, length, colour),
            Position = position
        };

        AddChildNode(rootNode);
    }

    private static SCNGeometry CreateGeometry(
       nfloat width, nfloat length, UIColor colour)
    {
        var material = new SCNMaterial();
        material.Diffuse.Contents = colour;
        material.DoubleSided = false;

        var geometry = SCNPlane.Create(width, length);
        geometry.Materials = new[] { material };

        return geometry;
    }
}

Listing 10-4Our custom PlaneNode

动态添加要检测的图像

除了将您想要检测的图像与应用打包在一起之外,还可以在运行时动态添加要检测的图像。如果您不知道需要在编译时检测哪些图像,这尤其有用。

例如,您可以调用 Amazon API,返回畅销书籍封面的图像,并将这些图像添加到应用中进行检测。然后,当检测到那些书籍封面时,提供进一步的功能,例如在检测到的书籍旁边的 AR 中检索和显示评论信息。

要尝试的事情

既然您已经知道了如何检测场景中的预期图像,那么您可能希望尝试看看您可以使用该功能做些什么。这里有一些想法。

用另一幅图像替换检测到的图像。

检测到图像后,尝试将另一个图像放在检测到的图像上(显然是替换它)。

用视频替换检测到的图像。

检测到图像后,将视频放在检测到图像的位置并播放。参见第八章“视频和声音”,了解如何将视频添加到场景中。

在检测到图像的位置放置一个 3D 模型。

检测到图像后,在检测到图像的位置放置 3D 模型。参见第十三章“3D 模型”,了解如何将 3D 模型添加到场景中。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-5

将 3D 模型放置在检测到的图像上

图像检测可用于创建一些有趣的效果,如图 10-5 所示,其显示了放置在检测图像顶部的 3D 模型,以及图 10-6 所示,其显示了添加在检测图像顶部的浮动图像。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-6

将浮动图像添加到检测到的图像

摘要

图像检测是增强现实中一个非常有用的功能,营销人员经常使用它来为他们的产品添加 AR 体验。

ARKit 的另一个惊人特性是,它不仅可以跟踪人脸,还可以跟踪面部表情。我们将在下一章“面部跟踪和表情检测”中探讨这个问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值