本文是Ray Wenderlich上《ARKit by Tutorials》2.0 版新增章节的读书笔记,主要讲内容概要和读后感
成品效果
本章的成果是做一个 AR 绘图 app,并可以保存并分享给其他人。
世界追踪和场景持久化
ARKit 依靠 ARWorldMap 来提供数据持久化,其中 ARSession 起到了重要作用。
向 ARSession 中添加 ARAnchor 对象时,它既是 app 中的虚拟物体,也是真实世界中的特征。
放置锚点
只有两个步骤:
- 在
renderer(_:willRenderScene:atTime:):
中创建并添加锚点ARLineAnchor
; - 在
renderer(_:didAdd:for:)
中取出锚点,并根据锚点创建节点SCNLineNode
;
// 添加锚点的代码
func addLineAnchorForObject(sourcePoint: SCNVector3?,
destinationPoint: SCNVector3?) {
// 获取命中测试的第一个结果
guard let hitTestResult = sceneView
.hitTest(self.viewCenter!, types: [.existingPlaneUsingGeometry,
.estimatedHorizontalPlane])
.first
else { return }
// 创建 ARLineAnchor 锚点
currentLineAnchorName = "virtualObject\(count)"
count = count+1
let lineAnchor = ARLineAnchor(name: currentLineAnchorName!,
transform: hitTestResult.worldTransform,
sourcePoint: sourcePoint,
destinationPoint: destinationPoint)
// 添加到 session 中
sceneView.session.add(anchor: lineAnchor)
lineObjectAnchors.append(lineAnchor)
}
保存世界地图
世界地图的状态,可以从session(_ session: ARSession, didUpdate frame: ARFrame)
中的frame.worldMappingStatus
来判断。
func session(_ session: ARSession, didUpdate frame: ARFrame) {
switch frame.worldMappingStatus {
case .extending, .mapped:
// 可用状态
default:
}
世界地图的实际获取是从 session 中获取的,同时还会保存一张截图
sceneView.session.getCurrentWorldMap { worldMap, error in
// 取值
guard let map = worldMap else {
return
}
// 配上一张截图
guard let snapshotAnchor = SnapshotAnchor(capturing: self.sceneView) else {
fatalError("Can't take snapshot")
}
map.anchors.append(snapshotAnchor)
do {
// 归档保存
let data = try NSKeyedArchiver.archivedData(withRootObject: map,
requiringSecureCoding: true)
try data.write(to: self.mapSaveURL, options: [.atomic])
} catch {
fatalError("Can't save map: \(error.localizedDescription)")
}
}
加载和恢复
取出保存的世界地图
// 反归档
guard let worldMap =
try NSKeyedUnarchiver.unarchivedObject(ofClass: ARWorldMap.self,
from: data)
else {
fatalError("No ARWorldMap in archive.")
}
// 取出截图
if let snapshotData = worldMap.snapshotAnchor?.imageData,
let snapshot = UIImage(data: snapshotData) {
} else {
print("No snapshot image in world map")
}
// 删除截图
worldMap.anchors.removeAll(where: { $0 is SnapshotAnchor })
// 加载使用
let configuration = self.defaultConfiguration
configuration.initialWorldMap = worldMap
sceneView.session.run(configuration,
options: [.resetTracking, .removeExistingAnchors])
共享 AR 体验
多人 AR 依赖的是苹果的MultipeerConnectivity框架。这个框架使用 Wifi 和蓝牙来传输数据。你需要一个MCSession
对象来广播自己,发现其他设备,及连接到其他 peer 上
import MultipeerConnectivity
class PeerSession: NSObject {
// 用来沟通的 ID,及协议serviceType
private let peerID = MCPeerID(displayName: UIDevice.current.name)
static let serviceType = "arsketchsession"
// MCSession对象,用来支持多方数据传输
private(set) var mcSession: MCSession!
// MCAdvertiserAssistant对象,用来告诉其他 peer,你的 app 可以加入一个多方会话网络中。它还自带了一个标准用户界面,用来接受别人的会话邀请或者主动邀请别人进入会话
private var advertiserAssistant: MCAdvertiserAssistant!
// 处理接收到的数据
private let receivedDataHandler: (Data, MCPeerID) -> Void
}
建立 peer 连接
代理方法
建立连接,传输数据需要在代理方法中处理,虽然MCAdvertiserAssistantDelegate
中的方法不需要我们实现,但MCSessionDelegate
中还是需要处理的:
// 当附近 session 中的 peer 状态发生改变时调用。状态变为 MCSessionState.connected 或者 MCSessionState.notConnected时调用
func session(_ session: MCSession,
peer peerID: MCPeerID,
didChange state: MCSessionState) {
}
// 当自己收到 session 中的其他 peer 发来的`NSData`数据时调用:
func session(_ session: MCSession,
didReceive data: Data,
fromPeer peerID: MCPeerID) {
// 处理收到的数据
receivedDataHandler(data, peerID)
}
// 当附近的 peer 和自己建立byte stream connection时调用,本 app 中不需要这个,出现后直接报错
func session(_ session: MCSession,
didReceive stream: InputStream,
withName streamName: String,
fromPeer peerID: MCPeerID) {
fatalError("This service does not send/receive streams.")
}
// 当自己从附近的 peer 上接收资源时调用,进度由`NSProgress`获取,本 app 中不需要这个,所以提示错误
func session(_ session: MCSession,
didStartReceivingResourceWithName resourceName: String,
fromPeer peerID: MCPeerID,
with progress: Progress) {
fatalError("This service does not send/receive resources.")
}
// 当资源传输完成时调用,本 app 中不需要这个,所以提示错误
func session(_ session: MCSession,
didFinishReceivingResourceWithName resourceName: String,
fromPeer peerID: MCPeerID,
at localURL: URL?,
withError error: Error?) {
fatalError("This service does not send/receive resources.")
}
}
系统自带 UI:MCBrowserViewController
要真正建立连接,还需要发现 peer 并选择要连接的设备。因此,app 需要广播 service type,也就是前面定义的static let serviceType = "arsketchsession"
。我们要做的就是用一个服务浏览器,来查找支持的服务类型service type,一般有两种方式:
- 使用一个
MCNearbyServiceBrowser
对象,它可以让你的 app 使用代码来邀请其他 peer 连接到会话中。并且你可以控制连接的超时时长,以及向其他 peer 发送邀请相关的性质。 - 使用一个
MCBrowserViewController
对象,它提供一个标准用户界面,可以展示出哪里 peer 是可用的,并允许我们建立连接。
这里我们使用第二种方法,直接用自带的标准用户界面,整个过程和 iOS 中相册类似:
// 创建一个浏览控制器
let mcBrowserVC =
MCBrowserViewController(serviceType: PeerSession.serviceType,
session: peerSession.mcSession)
mcBrowserVC.delegate = self
// 弹出控制器
self.present(mcBrowserVC, animated: true, completion: nil)
还需要实现代理:
// 实现代理方法
extension ViewController: MCBrowserViewControllerDelegate {
func browserViewControllerDidFinish(
_ browserViewController: MCBrowserViewController) {
browserViewController.dismiss(animated: true,
completion: nil)
}
func browserViewControllerWasCancelled(
_ browserViewController: MCBrowserViewController) {
browserViewController.dismiss(animated: true,
completion: nil)
}
}
发送数据到 peers
拿到先前归档得到的 data 数据,调用MCSession
类的send(_: toPeers:with:)
方法就可以了
if let data =
try? NSKeyedArchiver.archivedData(withRootObject: lineAnchor,
requiringSecureCoding: true)
{
self.peerSession.sendToAllPeers(data)
}
private(set) var mcSession: MCSession!
func sendToAllPeers(_ data: Data) {
do {
try mcSession.send(data,
toPeers: mcSession.connectedPeers,
with: .reliable)
} catch {
print("""
error sending data to peers:
\(error.localizedDescription)
""")
}
}
接收数据和重定位
接收到的数据在handleReceivedData(_:from:)
代理方法里处理:
do {
// 如果收到的是世界地图
if let worldMap = try NSKeyedUnarchiver.unarchivedObject(ofClass: ARWorldMap.self, from: data) {
DispatchQueue.main.async {
self.displaySnapshotImage(from: worldMap)
}
configureARSession(for: worldMap)
mapProvider = peer
return
}
} catch {
print("can't decode data received from \(peer)")
}
// 如果收到的是线段
if !isRelocalizingMap {
do {
if let anchor = try NSKeyedUnarchiver.unarchivedObject(ofClass: ARLineAnchor.self, from: data) {
sceneView.session.add(anchor: anchor)
}
} catch {
print("unknown data received from \(peer)")
}
}
其他
教程中的其他操作,比如 UI 搭建,重置游戏,文本提示等比较简单,不再赘述。