AR 全称 Augmented Reality(加强现实)是一种在视觉上呈现虚拟物体与现实场景结合的技术。Apple 公司在 2017 年 6 月正式推出了 ARKit,iOS 开发者能够在这个平台上使用简单便捷的 API 来开发 AR 应用程序。为了得到 ARKit 的完整功能,须要 A9 及以上芯片。其实也就是大部分运行 iOS 11 的设备,包括 iPhone 6S。数组
研究过程当中,作了一个卷尺的Demo,如今介绍下项目中用到的技术点。bash
iOS 平台的 AR 应用一般由 ARKit 和渲染引擎两部分构成:微信
下面主要说说ARKit的功能点和渲染部分SceneKit两个方面。session
ARKit 的 ARSession 负责管理每一帧的信息。ARSession 作了两件事:拍摄图像并获取传感器数据;对数据进行分析处理后逐帧输出。以下图:dom
设备追踪确保了虚拟物体的位置不受设备移动的影响。在启动 ARSession 时须要传入一个 ARSessionConfiguration 的子类对象,以区别三种追踪模式:ui
其中 ARFaceTrackingConfiguration 能够识别人脸的位置、方向以及获取拓扑结构。此外,还能够探测到预设的 52 种丰富的面部动做,如眨眼、微笑、皱眉等等。ARFaceTrackingConfiguration 须要调用支持 TrueDepth 的前置摄像头进行追踪。 本项目主要是使用ARWorldTrackingConfiguration进行追踪,获取特征点。spa
// 建立一个 ARSessionConfiguration.
// 暂时无需在乎 ARWorldTrackingSessionConfiguration.
let configuration = ARWorldTrackingSessionConfiguration()
// Create a session.
let session = ARSession()
// Run.
session.run(configuration)
复制代码
从上面的代码看,运行一个 ARSession 的过程是很简单的,那么 ARSession 的底层如何进行世界追踪的呢?3d
AR-World 的坐标系以下,当咱们运行 ARSession 时设备所在的位置就是 AR-World 的坐标系原点。code
在这个 AR-World 坐标系中,ARKit 会追踪如下几个信息:orm
苹果文档中对世界追踪过程是这么解释的:ARKit使用视觉惯性测距技术,对摄像头采集到的图像序列进行计算机视觉分析,而且与设备的运动传感器信息相结合。ARKit 会识别出每一帧图像中的特征点,而且根据特征点在连续的图像帧之间的位置变化,而后与运动传感器提供的信息进行比较,最终获得高精度的设备位置和偏转信息。
ARWorldTrackingConfiguration 提供 6DoF(Six Degree of Freedom)的设备追踪。包括三个姿态角 Yaw(偏航角)、Pitch(俯仰角)和 Roll(翻滚角),以及沿笛卡尔坐标系中 X、Y 和 Z 三轴的偏移量:
不只如此,ARKit 还使用了 VIO(Visual-Inertial Odometry)来提升设备运动追踪的精度。在使用惯性测量单元(IMU)检测运动轨迹的同时,对运动过程当中摄像头拍摄到的图片进行图像处理。将图像中的一些特征点的变化轨迹与传感器的结果进行比对后,输出最终的高精度结果。 从追踪的维度和准确度来看,ARWorldTrackingConfiguration 很是强悍。但如官方文档所言,它也有两个致命的缺点:
因为在追踪过程当中要经过采集图像来提取特征点,因此图像的质量会影响追踪的结果。在光线较差的环境下(好比夜晚或者强光),拍摄的图像没法提供正确的参考,追踪的质量也会随之降低。
追踪过程当中会逐帧比对图像与传感器结果,若是设备在短期内剧烈的移动,会很大程度上干扰追踪结果。
世界追踪有三种状态,咱们能够经过 camera.trackingState 获取当前的追踪状态。
从上图咱们看到有三种追踪状态:
与 TrackingState 关联的一个信息是 ARCamera.TrackingState.Reason,这是一个枚举类型:
咱们能够经过 ARSessionObserver 协议去获取追踪状态的变化,比较简单,能够直接查看接口文档。
ARFrame 中包含有世界追踪过程获取的全部信息,ARFrame 中与世界追踪有关的信息主要是:anchors 和 camera:
var camera: [ARCamera]
复制代码
var anchors: [ARAnchor]
复制代码
每一个 ARFrame 都会包含一个 ARCamera。ARCamera 对象表示虚拟摄像头。虚拟摄像头就表明了设备的角度和位置。
场景解析主要功能是对现实世界的场景进行分析,解析出好比现实世界的平面等信息,可让咱们把一些虚拟物体放在某些实物处。ARKit 提供的场景解析主要有平面检测、场景交互以及光照估计三种,下面逐个分析。
上图中能够看出,ARkit 检测出了两个平面,图中的两个三维坐标系是检测出的平面的本地坐标系,此外,检测出的平面是有一个大小范围的。
开启平面检测很简单,只须要在 run ARSession 以前,将 ARSessionConfiguration 的 planeDetection 属性设为 true 便可。
// Create a world tracking session configuration.
let configuration = ARWorldTrackingSessionConfiguration()
configuration.planeDetection = .horizontal
// Create a session.
let session = ARSession()
// Run.
session.run(configuration)
复制代码
当 ARKit 检测到一个平面时,ARKit 会为该平面自动添加一个 ARPlaneAnchor,这个 ARPlaneAnchor 就表示了一个平面。
新加入了 ARAnchor
func session(_ session: ARSession, didAdd anchors: [ARAnchor])
复制代码
对于平面检测来讲,当新检测到某平面时,咱们会收到该通知,通知中的 ARAnchor 数组会包含新添加的平面,其类型是 ARPlaneAnchor,咱们能够像下面这样使用:
func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
for anchor in anchors {
if let anchor = anchor as? ARPlaneAnchor {
print(anchor.center)
print(anchor.extent)
}
}
}
复制代码
ARAnchor 更新
func session(_ session: ARSession, didUpdate anchors: [ARAnchor])
复制代码
从上面咱们知道当设备移动时,检测到的平面是不断更新的,当平面更新时,会回调这个接口。
删除 ARAnchor
func session(_ session: ARSession, didRemove anchors: [ARAnchor])
复制代码
当手动删除某个 Anchor 时,会回调此方法。此外,对于检测到的平面来讲,若是两个平面进行了合并,则会删除其中一个,此时也会回调此方法。
Hit-testing 是为了获取当前捕捉到的图像中某点击位置有关的信息(包括平面、特征点、ARAnchor 等)。
原理图以下
当点击屏幕时,ARKit 会发射一个射线,假设屏幕平面是三维坐标系中的 xy 平面,那么该射线会沿着 z 轴方向射向屏幕里面,这就是一次 Hit-testing 过程。这次过程会将射线遇到的全部有用信息返回,返回结果以离屏幕距离进行排序,离屏幕最近的排在最前面。
ARFrame 提供了 Hit-testing 的接口:
func hitTest(_ point: CGPoint, types: ARHitTestResult.ResultType) -> [ARHitTestResult]
复制代码
上述接口中有一个 types 参数,该参数表示这次 Hit-testing 过程须要获取的信息类型。ResultType 有如下四种:
表示这次 Hit-testing 过程但愿返回当前图像中 Hit-testing 射线通过的 3D 特征点。以下图:
表示这次 Hit-testing 过程但愿返回当前图像中 Hit-testing 射线通过的预估平面。预估平面表示 ARKit 当前检测到一个多是平面的信息,但当前还没有肯定是平面,因此 ARKit 尚未为此预估平面添加 ARPlaneAnchor。以下图:
表示这次 Hit-testing 过程但愿返回当前图像中 Hit-testing 射线通过的有大小范围的平面。
上图中,若是 Hit-testing 射线通过了有大小范围的绿色平面,则会返回此平面,若是射线落在了绿色平面的外面,则不会返回此平面。
表示这次 Hit-testing 过程但愿返回当前图像中 Hit-testing 射线通过的无限大小的平面。
上图中,平面大小是绿色平面所展现的大小,但 exsitingPlane 选项表示即便 Hit-testing 射线落在了绿色平面外面,也会将此平面返回。换句话说,将全部平面无限延展,只要 Hit-testing 射线通过了无限延展后的平面,就会返回该平面。
示例代码以下
// Adding an ARAnchor based on hit-test
let point = CGPoint(x: 0.5, y: 0.5) // Image center
// Perform hit-test on frame.
let results = frame. hitTest(point, types: [.featurePoint, .estimatedHorizontalPlane])
// Use the first result.
if let closestResult = results.first {
// Create an anchor for it.
anchor = ARAnchor(transform: closestResult.worldTransform)
// Add it to the session.
session.add(anchor: anchor)
}
复制代码
上面代码中,Hit-testing 的 point(0.5, 0.5)表明屏幕的中心,屏幕左上角为(0, 0),右下角为(1, 1)。 对于 featurePoint 和 estimatedHorizontalPlane 的结果,ARKit 没有为其添加 ARAnchor,咱们可使用 Hit-testing 获取信息后本身为 ARSession 添加 ARAnchor,上面代码就显示了此过程。
上图中,一个虚拟物体茶杯被放在了现实世界的桌子上。
当周围环境光线较好时,摄像机捕捉到的图像光照强度也较好,此时,咱们放在桌子上的茶杯看起来就比较贴近于现实效果,如上图最左边的图。可是当周围光线较暗时,摄像机捕捉到的图像也较暗,如上图中间的图,此时茶杯的亮度就显得跟现实世界格格不入。
针对这种状况,ARKit 提供了光照估计,开启光照估计后,咱们能够拿到当前图像的光照强度,从而可以以更天然的光照强度去渲染虚拟物体,如上图最右边的图。
光照估计基于当前捕捉到的图像的曝光等信息,给出一个估计的光照强度值(单位为 lumen,光强单位)。默认的光照强度为 1000lumen,当现实世界较亮时,咱们能够拿到一个高于 1000lumen 的值,相反,当现实世界光照较暗时,咱们会拿到一个低于 1000lumen 的值。
ARKit 的光照估计默认是开启的,固然也能够经过下述方式手动配置:
configuration.isLightEstimationEnabled = true
复制代码
获取光照估计的光照强度也很简单,只须要拿到当前的 ARFrame,经过如下代码便可获取估计的光照强度:
let intensity = frame.lightEstimate?.ambientIntensity
复制代码
渲染是呈现 AR world 的最后一个过程。此过程将建立的虚拟世界、捕捉的真实世界、ARKit 追踪的信息以及 ARKit 场景解析的的信息结合在一块儿,渲染出一个 AR world。渲染过程须要实现如下几点才能渲染出正确的 AR world:
若是咱们本身处理这个过程,能够看到仍是比较复杂的,ARKit 为简化开发者的渲染过程,为开发者提供了简单易用的使用 SceneKit(3D 引擎)以及 SpriteKit(2D 引擎)渲染的视图ARSCNView以及ARSKView。固然开发者也可使用其余引擎进行渲染,只须要将以上几个信息进行处理融合便可。
咱们知道 UIKit 使用一个包含有 x 和 y 信息的 CGPoint 来表示一个点的位置,可是在 3D 系统中,须要一个 z 参数来描述物体在空间中的深度,SceneKit 的坐标系能够参考下图:
这个三维坐标系中,表示一个点的位置须要使用(x,y,z)坐标表示。红色方块位于 x 轴,绿色方块位于 y 轴,蓝色方块位于 z 轴,灰色方块位于原点。在 SceneKit 中咱们能够这样建立一个三维坐标:
let position = SCNVector3(x: 0, y: 5, z: 10)
复制代码
咱们能够将 SceneKit 中的场景(SCNScene)想象为一个虚拟的 3D 空间,而后能够将一个个的节点(SCNNode)添加到场景中。SCNScene 中有惟一一个根节点(坐标是(x:0, y:0, z:0)),除了根节点外,全部添加到 SCNScene 中的节点都须要一个父节点。
下图中位于坐标系中心的就是根节点,此外还有添加的两个节点 NodeA 和 NodeB,其中 NodeA 的父节点是根节点,NodeB 的父节点是 NodeA:
SCNScene 中的节点加入时能够指定一个三维坐标(默认为(x:0, y:0, z:0)),这个坐标是相对于其父节点的位置。这里说明两个概念:
上图中咱们能够看到 NodeA 的坐标是相对于世界坐标系(因为 NodeA 的父节点是根节点)的位置,而 NodeB 的坐标表明了 NodeB 在 NodeA 的本地坐标系位置(NodeB 的父节点是 NodeA)。
有了 SCNScene 和 SCNNode 后,咱们还须要一个摄像机(SCNCamera)来决定咱们能够看到场景中的哪一块区域(就比如现实世界中有了各类物体,但还须要人的眼睛才能看到物体)。摄像机在 SCNScene 的工做模式以下图:
上图中包含如下几点信息:
在 SceneKit 中咱们可使用以下方式建立一个摄像机:
let scene = SCNScene()
let cameraNode = SCNNode()
let camera = SCNCamera()
cameraNode.camera = camera
cameraNode.position = SCNVector3(x: 0, y: 0, z: 0)
scene.rootNode.addChildNode(cameraNode)
复制代码
最后,咱们须要一个 View 来将 SCNScene 中的内容渲染到显示屏幕上,这个工做由 SCNView 完成。这一步其实很简单,只须要建立一个 SCNView 实例,而后将 SCNView 的 scene 属性设置为刚刚建立的 SCNScene,而后将 SCNView 添加到 UIKit 的 view 或 window 上便可。示例代码以下:
let scnView = SCNView()
scnView.scene = scene
vc.view.addSubview(scnView)
scnView.frame = vc.view.bounds
复制代码
欢迎关注公众号:jackyshan,技术干货首发微信,第一时间推送。