[ARKit]11-[译]在ARKit中创建一个时空门App:准备开始

2,246 阅读9分钟

说明

ARKit系列文章目录

译者注:本文是Raywenderlich上《ARKit by Tutorials》免费章节的翻译,是原书第7章.原书7~9章完成了一个时空门app.
官网原文地址www.raywenderlich.com/195361/buil…


本文是我们书籍ARKit by Tutorials中的第7章,“创建你的时空门”.这本书向你展示了如何用苹果的增强现实框架ARKit,来构建五个沉浸式的,好看的AR应用.开始吧

通过这一系列教程,你将用ARKit和SceneKit实现一个时空门应用.时空门类的app可以用于教育目的,比如一个太阳系虚拟浏览应用,或者一些休闲活动,比如享受一场虚拟的沙滩假期.

时空门app

在这个应用中,你将在现实世界中的某个水平面上,放置一个通往充满未来感的房间的虚拟门.你可以走进走出这个房间,探索里面有什么.

在该教程中,你将建立时空门应用的基础.在本教程中,你将学会如何:

  • 建立一个ARSession
  • 用ARKit检测并渲染水平面

你准备好建立通往另一个世界的通道了么?

开始

在Xcode中,打开starter工程, Portal.xcodeproj.创建并运行工程,你会看到一个空白屏幕.

啊,是的,一个空白充满机遇的画布!
打开Main.storyboard再展开Portal View Controller Scene

PortalViewController是应用启动后呈现给用户的界面.它包含一个ARSCNView来显示相机预览画面.还包含了两个UILabels来提供说明和反馈给用户.

现在,打开PortalViewController.swift.在这个文件中,你将看到下面的变量,它们代表了storyboard中的元素:

// 1
@IBOutlet var sceneView: ARSCNView?
// 2
@IBOutlet weak var messageLabel: UILabel?
// 3
@IBOutlet weak var sessionStateLabel: UILabel?

让我们看看其中的内容:

  1. sceneView用来显示3D的SceneKit物体在相机视图上.
  2. messageLabel,它是个UILabel,会给用户展示说明性的消息.说明,告诉他们如何与你的app交互.
  3. sessionStateLabel,另一个UILabel,会通知用户session打断情况,例如当app进入后台或环境光不满足条件.

注意:ARKit会处理所有的传感器和相机数据,但它不会实际去渲染任何虚拟内容.要在你的场景中渲染内容,可以使用与ARKit协同的各种渲染器,比如SceneKit or SpriteKit.
ARSCNView是苹果提供的一个框架,你可以轻易将ARKit中数据与SceneKit融合在一起.使用ARSCNView会有很多好处,这就是为什么你要在本教程的项目中用它.

在starter工程中,你将会在Helpers分组下看到很多工具类.你会在app后面的开发中用到它们.

建立ARKit

第一步是用相机来捕捉视频流.为此,你需要使用ARSCNView对象.

打开PortalViewController.swift并添加下面的方法:

func runSession() {
  // 1  
  let configuration = ARWorldTrackingConfiguration.init()
  // 2
  configuration.planeDetection = .horizontal
  // 3
  configuration.isLightEstimationEnabled = true
  // 4
  sceneView?.session.run(configuration)

  // 5
  #if DEBUG
    sceneView?.debugOptions = [ARSCNDebugOptions.showFeaturePoints]
  #endif
}

代码说明:

  1. 首先实例化一个ARWorldTrackingConfiguration对象.它为ARSession定义了配置信息.对ARSession来说可用的配置类型有两种: ARSessionConfigurationARWorldTrackingConfiguration.
    使用ARSessionConfiguration是不推荐的,因为它使用了设备的旋转信息,还没有位置.对于使用A9处理器的设置来说,ARWorldTrackingSessionConfiguration是最佳选择,因为它追踪了设备的所有运动.
  2. configuration.planeDetection是设置为检测水平面.平面的范围可能会改变,并且随着相机的移动多个平面可能会合并成一个.它可以找到任何水平面例如地板,桌子或床.
  3. 它启用了灯光估计计算,可以被渲染框架利用来制造出更真实的虚拟物体.
  4. 使用指定的配置来启动session的AR进程.这将会启动ARKit session和视频捕捉,并显示在sceneView上.
  5. 调试设置,这样会添加可见的特征点;覆盖在相机视图上.

现在,是时候建立labels的默认设置了.用下面的代码替换resetLabels():

func resetLabels() {
  messageLabel?.alpha = 1.0
  messageLabel?.text =
    "Move the phone around and allow the app to find a plane." +
    "You will see a yellow horizontal plane."
  sessionStateLabel?.alpha = 0.0
  sessionStateLabel?.text = ""    
}

这将messageLabelsessionStateLabel设置好透明度和文本.记住,messageLabel是用于展示给用户说明,而sessionStateLabel是用于展示错误信息的,以防出错.

现在,添加runSession()PortalViewController中的viewDidLoad() 里面:

override func viewDidLoad() {
  super.viewDidLoad()    
  resetLabels()
  runSession()
}

这将会在app启动并加载视图时运行ARKit session.

接着,构建并运行app.不要忘记--你需要给app授于相机访问权限.

ARSCNView完成了繁重的相机视频捕捉和显示任务.因为是调试模式,你可以看到渲染出的特征点,它们形成了点云,显示出场景分析的中间结果.

平面探测和渲染

先前,在runSession()中, 你设置了planeDetection.horizontal,这意味着你的app能探测水平面.你能够在ARSCNViewDelegate协议的代理回调方法中获得捕捉到的平面信息.

PortalViewController中添加类扩展,实现ARSCNViewDelegate协议:

extension PortalViewController: ARSCNViewDelegate {

}

runSession() 末尾添加下面代码:

sceneView?.delegate = self

这行代码将PortalViewController设置为sceneView对象的ARSCNViewDelegate代理.

ARPlaneAnchors会被自动添加到ARSession锚点数组中,并且ARSCNView自动将ARPlaneAnchor对象转换为SCNNode节点.

现在,要渲染这些平面,你需要做的是实现ARSCNViewDelegate代理方法:

// 1
func renderer(_ renderer: SCNSceneRenderer,
              didAdd node: SCNNode,
              for anchor: ARAnchor) {
  // 2
  DispatchQueue.main.async {
    // 3
    if let planeAnchor = anchor as? ARPlaneAnchor {
        // 4
      #if DEBUG
        // 5
        let debugPlaneNode = createPlaneNode(
          center: planeAnchor.center,
          extent: planeAnchor.extent)
        // 6  
        node.addChildNode(debugPlaneNode)
      #endif
      // 7
      self.messageLabel?.text =
      "Tap on the detected horizontal plane to place the portal"
    }
  }
}

代码含义:

  1. ARSession探测到新的平面时,renderer(_:didAdd:for:) 方法会被调用,并且ARSCNView自动为平面添加一个ARPlaneAnchor.
  2. 这个回调是在后台线程.这里,你需要派发到主线程,因为更新UI需要在主线程完成.
  3. 检查ARAnchor是否是一个ARPlaneAnchor.
  4. 检查是否在debug模式.
  5. 如果是,用ARKit探测到的planeAnchor的中心点和面积坐标来创建平面SCNNode节点.createPlaneNode() 是个帮助类的方法稍后实现.
  6. node对象是一个空的SCNNode,会被ARSCNView自动添加到场景中;它的坐标对准到ARAnchor的位置上.这里,你添加一个debugPlaneNode作为子节点,这样它就会被放置在节点的位置上.
  7. 最后,不管是否在debug模式,我们都为用户更新说明信息,以提示用户app现在已经准备好放置时空门到场景中了.

现在是时候建立帮助类的方法了.

创建一个新的Swift文件,命名为SCNNodeHelpers.swift.用来盛放渲染SCNNode对象相关的所有工具方法.

导入SceneKit到文件中:

import SceneKit

现在,添加下面的帮助方法:

// 1
func createPlaneNode(center: vector_float3,
                     extent: vector_float3) -> SCNNode {
  // 2
  let plane = SCNPlane(width: CGFloat(extent.x),
                      height: CGFloat(extent.z))
  // 3
  let planeMaterial = SCNMaterial()
  planeMaterial.diffuse.contents = UIColor.yellow.withAlphaComponent(0.4)
  // 4
  plane.materials = [planeMaterial]
  // 5
  let planeNode = SCNNode(geometry: plane)
  // 6
  planeNode.position = SCNVector3Make(center.x, 0, center.z)
  // 7
  planeNode.transform = SCNMatrix4MakeRotation(-Float.pi / 2, 1, 0, 0)
  // 8
  return planeNode
}

代码解读:

  1. createPlaneNode方法有两个参数:要渲染平面的centerextent,类型都是vector_float3.这个类型表示点的坐标.该函数返回一个SCNNode类型的对象.
  2. 用指定的宽度和高度创建一个SCNPlane平面.宽度是extent中的x坐标,高度是z坐标.
  3. 初始化SCNMaterial对象并赋值漫反射内容.漫反射层颜色设置为半透明的黄色.
  4. SCNMaterial对象然后被添加到平面的materials数组中.这定义了平面的纹理和颜色.
  5. 创建一个带有plane几何体的SCNNode节点.SCNPlane继承于SCNGeometry类,它只提供了SceneKit渲染出的可见物体.通过将几何体附加到SCNNode对象来指定它的位置和朝向.多个节点可以引用同一个几何体对象,并允许在一个场景的不同位置出现.
  6. 设置planeNode的位置.注意,节点是根据ARKit上报的ARPlaneAnchor实例对象的信息被平移到坐标点 (center.x, 0, center.z) 处.
  7. SceneKit中的平面默认是竖直的,所以你需要旋转90度以使它呈水平状态.
  8. 该步返回前步创建的planeNode对象.

运行一下app,如果ARKit能探测到合适的平面,你就能看到一个黄色的水平面了.

移动一下设备,你会注意到app有时会显示多个平面.当它发现更多平面时,它将其加到视图中.然而,已经存在的平面,却不会随着ARKit分析出更多特征点而更新或改变尺寸.

ARKit会根据新发现的特征点来持续更新平面的位置和尺寸.要想接收这些更新,在PortalViewController.swift中添加下列的renderer(_:didUpdate:for:) 代理方法:

// 1
func renderer(_ renderer: SCNSceneRenderer,
              didUpdate node: SCNNode,
              for anchor: ARAnchor) {
  // 2              
  DispatchQueue.main.async {
    // 3
    if let planeAnchor = anchor as? ARPlaneAnchor,
      node.childNodes.count > 0 {
      // 4  
      updatePlaneNode(node.childNodes[0],
                      center: planeAnchor.center,
                      extent: planeAnchor.extent)
    }
  }
}

解释:

  1. renderer(_:didUpdate:for:) 将会在相应的ARAnchor更新时被调用.
  2. 在主线程更新UI操作.
  3. 检查ARAnchor,确保是一个ARPlaneAnchor类型,并且它至少有一个子节点对应于平面的SCNNode.
  4. updatePlaneNode(_:center:extent:) 方法将会在稍后实现.它根据ARPlaneAnchor中的信息更新平面的坐标和尺寸.

打开SCNNodeHelpers.swift文件,添加下列代码:

func updatePlaneNode(_ node: SCNNode,
                     center: vector_float3,
                     extent: vector_float3) {
  // 1                    
  let geometry = node.geometry as? SCNPlane
  // 2
  geometry?.width = CGFloat(extent.x)
  geometry?.height = CGFloat(extent.z)
  // 3
  node.position = SCNVector3Make(center.x, 0, center.z)
}

代码解释:

  1. 检查节点是否有SCNPlane几何体.
  2. 使用传递过来的参数更新节点的几何体.用ARPlaneAnchor中的extent或size来更新平面的宽度和高度.
  3. 更新平面节点的位置到新位置上.

现在你可以成功地更新平面的位置了,运行一下app.你会看到平面的尺寸和位置会随着探测到新的特征点而调整.

还有一个问题需要解决.一旦app检测到平面,如果你退出app再重新回来,你会看到前一个探测到的平面还在相机视图上,显示在其他物体前面;它已经不再匹配先前的平面了.

要修复这个问题,你需要在ARSession被打断时移除平面节点.我们会在下一章处理这个问题.

下一步做什么?

你可能还没意识到,但你已经踏上了创建一个时空门app的漫漫长路!是的,还有很多要做的事,但是你已经在进入虚拟空间的路上了.

本章节简单总结:

  • 探索starter项目,并复习了ARKit基础.
  • 配置一个ARSession来在app中展示相机的输出.
  • 添加平面检测和其他函数,以便app能使用ARSCNViewDelegate协议来渲染水平面.

在下一章教程中,你将会学习如何处理session的打断,及在视图中使用SceneKit来渲染3D物体.点击这里来继续本系列教程的第2部分!

如果你喜欢本教程,可以来查看我们的完整版书籍ARKit by Tutorials.

资料下载地址