ARKit 是苹果 WWDC2017 中发布的用于开发iOS平台 AR 功能的框架。AR 全称 Augmented Reality(增强现实),是一种在摄像机捕捉到的真实世界中加入计算机程序创造的虚拟世界的技术。
AR 系统由以下几个基础部分组成:
这是 SceneKit 主视图 SCNView 的子类,帮我们用 SceneKit 渲染的 3D 内容来增强实时摄像头视图,将设备摄像头的实时视频流渲染为场景背景,并会自动匹配 SceneKit 空间和真实世界。这个类做了下面几件事:
ARSCNView 本身不会做 AR 处理,但它需要 AR session 对象来管理设备摄像头和运动处理。负责综合虚拟世界(SceneKit)的信息和现实世界的信息(由ARSession 类负责采集),然后将它们综合渲染呈现出一个 AR 世界。
每个增强现实会话都都需要有一个 ARSession 实例。它负责控制摄像头、聚合所有来自设备的传感器数据等等以构建无缝体验(负责采集现实世界的信息)。ARSCNView 实例已经有 ARSession 实例,只需要在开始的时候配置一下。
ARFrame 包含了两部分信息:ARAnchor 和 ARCamera。
其中,ARCamera 指的是当前摄像机的位置和旋转信息。这一部分 ARKit 已经为我们配置好,不用特别配置。
指的是现实世界中的锚点,具体解释如下:可以把 ARAnchor(锚点)理解为真实世界中的某个点或平面,anchor 中包含位置信息和旋转信息。拿到 anchor 后,可以在该 anchor 处放置一些虚拟物体。也可以与 SCNNode 可以绑定。它有一个子类:ARPlaneAnchor,专门指的是一个代表水平面的锚点。
这个类会告诉 ARSession,在真实世界中追踪用户时需要使用六个自由度,roll、pitch、yaw 以及 X轴、Y轴、Z轴上的变换。如果不用这个类,就只能创建在同一个点旋转查看增强内容的 AR 体验。有了这个类,就可以在 3D 空间里绕着物体移动了。如果你不需要在 X轴、Y轴、Z轴上的变换,用户就会在投影增强内容时保持在固定位置,这时可以用 ARSessionConfiguration 类替代此类来初始化 ARSession 实例。
ARSession 是整个ARKit系统的核心,ARSession 实现了世界追踪、场景解析等重要功能。而 ARFrame 中包含有 ARSession 输出的所有信息,是渲染的关键数据来源。
ARKit 本身并不提供创建虚拟世界的引擎,而是使用其他 3D/2D 引擎进行创建虚拟世界。iOS 系统上可使用的引擎主要有:
ARKit 需要看向能检测出许多有用特征点的内容。可能检测不出特征点的情况如下:
先介绍 ARSCNView 的代理:ARSCNViewDelegate,他有以下几个回调方法。
func renderer(SCNSceneRenderer, nodeFor: ARAnchor)
当 ARSession 检测到一个锚点时,可以在这个回调方法中决定是否给它返回一个 SCNNode。默认是返回一个空的 SCNNode(),我们可以根据自己的需要将它改成只在检测到平面锚点(ARPlaneAnchor)时返回一个锚点,诸如此类。
func renderer(SCNSceneRenderer, didAdd: SCNNode, for: ARAnchor)
func renderer(SCNSceneRenderer, willUpdate: SCNNode, for: ARAnchor)
func renderer(SCNSceneRenderer, didUpdate: SCNNode, for: ARAnchor)
func renderer(SCNSceneRenderer, didRemove: SCNNode, for: ARAnchor)
以上方法会在为一个锚点已经添加、将要更新、已经更新、已经移除一个虚拟锚点时进行回调。
ARSession 类也有自己的代理:ARSessionDelegate
func session(ARSession, didUpdate: ARFrame)
在 ARKit 中,当用户移动手机时,会实时更新很多 ARFrame。这个方法会在更新了 ARFrame 时,进行回调。它可以用于类似于始终想维持一个虚拟物体在屏幕中间的场景,只需要在这个方法中将该节点的位置更新为最新的 ARFrame 的中心点即可。
func session(ARSession, didAdd: [ARAnchor])
func session(ARSession, didUpdate: [ARAnchor])
func session(ARSession, didRemove: [ARAnchor])
如果使用了 ARSCNViewDelegate 或 ARSKViewDelegate,那上面三个方法不必实现。因为另外的 Delegate 的方法中除了锚点以外,还包含节点信息,这可以让我们有更多的信息进行处理。
import ARKit
private lazy var sceneView: ARSCNView = {
let tmpView = ARSCNView()
tmpView.frame = CGRect(x: 0, y: kNaviBarMaxY, width: kScreenWidth, height: kScreenHeight - kNaviBarMaxY)
// 添加光源
tmpView.autoenablesDefaultLighting = true
tmpView.automaticallyUpdatesLighting = true
return tmpView
}()
我们的AR应用程序是通过摄像头观察世界和周围的环境。所以接下来我们需要设置Camera:
配置ARKit SceneKit View。在ViewController类中插入以下代码:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let configuration = ARWorldTrackingConfiguration()
sceneView.session.run(configuration)
}
The World Tracking配置跟踪设备的方向和位置,它还能通过设备的Camera探测真实世界的表面。
设置sceneView’s AR session来运行我们刚刚初始化的配置。AR session管理视图内容的运动跟踪和camera图像处理。
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
sceneView.session.pause()
}
停止tracking和视图内容的图像。
private func addBox() {
let box = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0)
let boxNode = SCNNode()
boxNode.geometry = box
boxNode.position = SCNVector3(0, 0, -0.2)
let scene = SCNScene()
scene.rootNode.addChildNode(boxNode)
sceneView.scene = scene
}
private func addTapGestureToSceneView() {
let tap = UITapGestureRecognizer(target: self, action: #selector(didTap(withGestureRecognizer:)))
sceneView.addGestureRecognizer(tap)
}
@objc func didTap(withGestureRecognizer recognizer: UIGestureRecognizer) {
let tapLocation = recognizer.location(in: sceneView)
let hitTestResults = sceneView.hitTest(tapLocation)
guard let node = hitTestResults.first?.node else { return }
node.removeFromParentNode()
}
extension float4x4 {
var translation: float3 {
let translation = self.columns.3
return float3(translation.x, translation.y, translation.z)
}
}
extension将矩阵转换为float3。修改addBox()方法:
func addBox(x: Float = 0, y: Float = 0, z: Float = -0.2) {
let box = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0)
let boxNode = SCNNode()
boxNode.geometry = box
boxNode.position = SCNVector3(x, y, z)
sceneView.scene.rootNode.addChildNode(boxNode)
}
修改didTap(使用gesturerecognizer:)方法,在guard let语句内部和return语句之前。添加以下代码:
let hitTestResultsWithFeaturePoints = sceneView.hitTest(tapLocation, types: .featurePoint)
if let hitTestResultWithFeaturePoints = hitTestResultsWithFeaturePoints.first {
let translation = hitTestResultWithFeaturePoints.worldTransform.translation
addBox(x: translation.x, y: translation.y, z: translation.z)
}
接下来实现使用x、y和z在检测到的点击时添加一个新的box。didTap(withGestureRecognizer:)方法的代码如下:
@objc func didTap(withGestureRecognizer recognizer: UIGestureRecognizer) {
let tapLocation = recognizer.location(in: sceneView)
let hitTestResults = sceneView.hitTest(tapLocation)
guard let node = hitTestResults.first?.node else {
let hitTestResultsWithFeaturePoints = sceneView.hitTest(tapLocation, types: .featurePoint)
if let hitTestResultWithFeaturePoints = hitTestResultsWithFeaturePoints.first {
let translation = hitTestResultWithFeaturePoints.worldTransform.translation
addBox(x: translation.x, y: translation.y, z: translation.z)
}
return
}
node.removeFromParentNode()
}