音视频专辑ARKit教程

ARKit教程07_第五章:表面检测

2019-08-08  本文已影响4人  张芳涛

前言

ARKit感兴趣的同学,可以订阅ARKit教程专题
源代码地址在这里

正文

在本章中,我们将学习如何检测真实世界的曲面以及如何正确管理这些曲面的更新。还将学习如何创建一个焦点光标,通过光线投射将其置于检测到的曲面之上。

可以在Chapter04这个项目上继续开发。你可以拷贝一份代码,也可以新建一个项目,把原来的实现逻辑再写一遍。这一张我们需要一些扩展代码,这些代码:

添加game states

我们接下来需要实现的效果是检测到一个表面,之后再做其他的操作。

定义game states

首先定义此游戏的所有可能游戏状态。

ViewController.swift添加一个枚举:

// MARK: - Game State
enum GameState: Int16 {
case detectSurface  // Scan playable surface (Plane Detection On)
case pointToSurface // Point to surface to see focus point (Plane Detection Off)
case swipeToPlay    // Focus point visible on surface, swipe up to play
}

添加游戏状态信息

现在,你已经定义了一些游戏状态,现在需要一种方法来通知用户他们可以在每个状态下做什么。

首先,添加一些新的属性:

var gameState: GameState = .detectSurface 
var statusMessage: String = ""

上面的代码主要做了如下工作:

至此,我们需要一个更新状态的函数:

func updateStatus() {
// 1
switch gameState {
case .detectSurface:
  statusMessage = "Scan entire table surface...\nHit START when ready!"
case .pointToSurface:
  statusMessage = "Point at designated surface first!"
case .swipeToPlay:
  statusMessage = "Swipe UP to throw!\nTap on dice to collect it again."
}
// 2
self.statusLabel.text = trackingStatus != "" ?
  "\(trackingStatus)" : "\(statusMessage)"
 }

上述代码把实时的gameState状态信息呈现给用户。现在拒用这个更新状态的方法了。

renderer(_:updateAtTime):里面的这一行代码可以注释掉了:

//self.statusLabel.text = self.trackingStatus

状态更新的操作最好放在主线程执行:

 func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
    DispatchQueue.main.async {
      //self.statusLabel.text = self.trackingStatus
      self.updateStatus()
      self.updateFocusNode()
    }
}

锚点

你们对于锚点了解多少?

ARKit 使用附加到 3D 内容的虚拟锚点。其主要目的是在玩家移动设备时保持 3D 内容相对于真实世界的位置。

ARAnchor 对象包含一个实际变换,该变换保持其位置和方向。锚点不是可见元素,它不是可见元素。它只是一个在ARKit场景中维护的对象。默认情况下,ARKit 将每个 ARAnchor 与一个空的 SCNNode 配对。我们所要做的就是将 3D 内容添加为该节点的子节点。

ARPlaneAnchor 对象是一种专用锚点类型,包含真实世界变换(位置和方向),包含其他平面信息,包括中心点、方向和曲面范围。然后,可以使用此信息创建相应的 SceneKit 平面节点。

其实,还有一个 ARFaceAnchor 锚点类型,后面会做介绍。现在,我们将只关注ARPlane锚点。

检测表面

要使 ARKit 检测真实表面需要启用ARConfiguration对象。

要启用该标志,转到初始化部分,并在 sceneView.session.run(config)之前在initARSession() 内添加以下行:

config.planeDetection = .horizontal

ARKit 现在将开始检测水平表面,并为每个检测到的表面自动生成 ARPlaneAnchor 实例。

注意:我们也可以使用.vertical来检测垂直曲面。

创建一个新的平面:

添加新平面锚点时,可以使用下面函数创建相应的可视组件。

func createARPlaneNode( 
    planeAnchor: ARPlaneAnchor, color: UIColor) -> SCNNode { 
    // Add code here
 }

函数传入 ARPlanAnchor 以及 UIColor。现在,我们拥有生成 SceneKit 平面节点所需的所有信息。

首先,生成平面几何体。在createARPlaneNode()函数中添加以下内容:

let planeGeometry = SCNPlane(
width: CGFloat(planeAnchor.extent.x), 
height: CGFloat(planeAnchor.extent.z))

这将使用锚点的范围为平面的宽度和长度生成平面所需的几何体。

创建平面所需材质

现在,我们需要通过创建材质为几何体提供一些纹理。我们需要在createARPlaneNode()函数中添加如下代码:

let planeMaterial = SCNMaterial() 
planeMaterial.diffuse.contents ="ARResource.scnassets/Textures/Surface_diffuse.png" 
planeGeometry.materials = [planeMaterial]

上述代码创建一个新的材质,然后将其漫反射.内容属性设置到 Surface_diffuse.png 中包含的纹理。平面现在将具有纹理而不是平面颜色。

创建平面节点

接下来我们把下面的代码添加到createARPlaneNode()函数中:

// 1 - Create plane node 
let planeNode = SCNNode(geometry: planeGeometry) 
// 2 planeNode.position = SCNVector3Make( 
planeAnchor.center.x, 0, planeAnchor.center.z) 
// 3 
planeNode.transform = SCNMatrix4MakeRotation(-Float.pi / 2, 1, 0, 0) 
// 4 
return planeNode

上面的代码作用如下:

处理新的平面锚点

现在,我们已经拥有了能够创建 SceneKit 平面的帮助器函数,是时候使用它了。

激活平面检测后,ARKit 将自动开始为其检测到的每个水平表面创建 ARPlane锚点。

将调用相应的renderer(_:didAdd:for)代理来通知新添加的锚点。我们只需等待事件触发并为锚点创建相应的 SceneKit 平面。

我们可以在renderer(_:didAdd:for)代理方法中这么处理:

// MARK: - Plane Management
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
    guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
    DispatchQueue.main.async {
        
        let planeNode = self.createARPlaneNode(planeAnchor: planeAnchor,
                                               color: UIColor.yellow.withAlphaComponent(0.5))
        node.addChildNode(planeNode)
    }
}

上述代码作用如下:

更新平面

ARKit 可能最初未检测到整个表面,因此,当用户移动时,我们可能需要使用新信息更新先前检测到的平面。

获取平面几何体

我们需要另一个函数来更新具有新位置、方向和尺寸的现有平面节点。

func updateARPlaneNode( 
  planeNode: SCNNode, planeAchor: ARPlaneAnchor) { // Add code here 
}

我们需要更新平面几何体。在updateARPlaneNode()函数中添加以下代码:

let planeGeometry = planeNode.geometry as! SCNPlane 
planeGeometry.width = CGFloat(planeAchor.extent.x) 
planeGeometry.height = CGFloat(planeAchor.extent.z)

这将从平面节点检索以前生成的平面几何体;然后,它根据提供的平面锚点更新其宽度和高度信息。

更新平面位置信息

接下来需要处理的是平面的位置,在updateARPlaneNode()函数中添加下面的代码:

planeNode.position = SCNVector3Make(planeAchor.center.x, 0, planeAchor.center.z)

这将使用平面锚点提供的位置信息更新平面节点位置。

平面锚点更新的相关处理

最后,我们需要充分利用新的帮助器功能。如果以前检测到的曲面必须使用新信息进行更新,ARKit 将触发renderer(_:didUpdate:for)代理方法。我们可以在代理方法中添加如下的代码:

// 1 
func renderer(_ renderer: SCNSceneRenderer,

    didUpdate node: SCNNode, for anchor: ARAnchor) { 
// 2 
    guard let planeAnchor = anchor as? ARPlaneAnchor else { return } 
      // 3 
      DispatchQueue.main.async { 
    // 4 
    self.updateARPlaneNode(planeNode: node.childNodes[0], planeAchor: planeAnchor) 
     }
  }

上面的代码作用如下:

创建焦点节点

现在,这个应用可以检测表面,之前的一个模型,可以用上了:

image.png

Ray casting

光线投射是从屏幕中心(焦点)将虚拟光线投射到虚拟场景,同时查找与 3D 对象的交集的过程。

在现场。在此特定情况下,要查找场景中的光线和平面节点之间的交点。

光线与平面相交后,该交点位置将用于放置焦点节点。

创建聚焦点

我们首先需要定义用于光线投射测试的屏幕位置;这通常是屏幕的中心。在这种情况下,焦点节点比正常节点大一些。

我们添加一个成员变量保存焦点的位置信息:

var focusPoint:CGPoint!

现在需要初始化该位置。将以下代码行添加到 initSceneView() 的底部:

focusPoint = CGPoint(x: view.center.x, y: view.center.y + view.center.y * 0.25)

这将使用屏幕高度低于视图中心点 25% 的位置初始化对焦点。

方向更改的处理

要监听方向的更改,需要一个通知方法:

NotificationCenter.default.addObserver(self, selector: #selector(ViewController.orientationChanged), name: UIDevice.orientationDidChangeNotification, object: nil)

具体的实现如下:

@objc func orientationChanged() { 
    focusPoint = CGPoint(x: view.center.x, y: view.center.y + view.center.y * 0.25) 
}

上述代码将焦点更新到视图中心点以下 25% 的位置。

更新焦点节点

在焦点准备就绪后,我们还需要另一个函数,该函数将根据屏幕的焦点持续更新焦点节点。

func updateFocusNode() {
    // 1 
    let results = self.sceneView.hitTest(self.focusPoint, types: [.existingPlaneUsingExtent]) 
    // 2 
    if results.count == 1 {
        if let match = results.first {
            // 3
            let t = match.worldTransform
            // 4 
            self.focusNode.position = SCNVector3( x: t.columns.3.x, y: t.columns.3.y, z: t.columns.3.z) self.gameState = .swipeToPlay 
            } 
        } else { 
            // 5 
            self.gameState = .pointToSurface 
        }
    }

上述代码作用如下:

  我们还可以根据其他类型(如featurePoints(要素点)、estimatedHorizontalPlane(估计水平平面)和existingPlane(现有平面))执行光线强制转换。

要完成操作,需要用updateFocusNode()方法来替代renderer(_:updateAtTime)

self.updateFocusNode()

可能前面说这么多有一些不太明白,运行一下程序,看看效果吧:

现在,检测到的表面;焦点节点也应弹出。

现在会有平面重叠的现象。ARKit 有时可能会将多个检测到的平面合并到单个平面中。为此, ARKit 需要在创建新平面之前删除旧平面信息。这些操作,我们可以在renderer(_:didRemove:for)代理方法中做处理。

    func removeARPlaneNode(node: SCNNode) {
    for childNode in node.childNodes {
        childNode.removeFromParentNode()
    }
  }

  func renderer(_ renderer: SCNSceneRenderer, didRemove node: SCNNode, for anchor: ARAnchor) {
    guard anchor is ARPlaneAnchor else { return }
    DispatchQueue.main.async {
        self.removeARPlaneNode(node: node)
    }
   }
上一章 目录 下一章
上一篇下一篇

猜你喜欢

热点阅读