ARKit 世界地图 – 如何 Save/Access 使用自定义 UTI 文件类型?

ARKit World Maps – How To Save/Access Using A Custom UTI File Type?

我找不到任何专门列出 ARKit 将其世界地图保存为的文件类型的文档。据我所知,它是一个 .arexperience 文件。本质上,我正在尝试修改文档浏览器,以便能够有选择地选择要加载的 .arexperience 文件。

我尝试启用支持的文档类型,特别是 public.arexperience,其中 LSHandlerRank 为 String 类型,Value 为 Alternate,CFBundleTypeRole 为 Type String,Value 为 Viewer。

我还在 info.plist 中将 Supports Document Browser 设置为 YES Additional document type properties.

9.7.18 编辑:我现在收到以下错误。对此有什么想法吗?我已经清理了构建文件夹,关闭了 XCode,卸载并重新安装了项目,然后重新启动了我的计算机,但没有任何变化。

9.13.18 编辑:此错误是由 XCode 10 Beta(包括 Swift 4.2)和您的示例项目的全新安装引起的。知道发生了什么事吗?研究表明 SIGABRT 错误是未使用插座的结果,但使用让我查看连接的助理编辑器没有显示明显的问题...

这似乎是一个有趣的问题,所以我想我会着手回答并有一个完整的示例,可以作为您和其他人未来开发的基础。

这可能不是正确的方法,但希望它有用。

话虽如此,我不完全确定您为什么要使用文档浏览器来允许用户选择 ARWorldMaps。一种更简单的方法可能是将它们简单地存储在 CoreData 中,并允许 selection 在 UITableView 中。或者将下面的逻辑合并到类似的东西中,例如打开自定义文件时,将其保存到 CoreData,并以这种方式显示所有收到的文件。

无论如何,这里有一些东西可以让您更详细地探索这个主题。虽然请注意,这不是优化的方式,但它应该足以为您指明正确的方向 ^______^.

供您参考:

ARWorldMap conforms to the NSSecureCoding protocol, so you can convert a world map to or from a binary data representation using the NSKeyedArchiver and NSKeyedUnarchiver classes.

因为我们想使用 Custom UTI 来保存我们的 ARWorldMap 我们首先需要在我们的 info.plist 文件中设置它,我们将 UTI 类型设置为public.data.

在项目编辑器中看起来像这样:

有关执行此操作的更多信息,请参阅 Ray Wenderlich 中的一个很好的教程。

完成此操作后,我们当然需要保存 ARWorldMap 并允许导出。我创建了一个 typealias 这就是我们保存数据的方式,例如String 的键值和 Data 的值(我们的 ARWorldMap):

typealias BMWorlMapItem = [String: Data]

/// Saves An ARWorldMap To The Documents Directory And Allows It To Be Sent As A Custom FileType
@IBAction func saveWorldMap(){

    //1. Attempt To Get The World Map From Our ARSession
    augmentedRealitySession.getCurrentWorldMap { worldMap, error in

        guard let mapToShare = worldMap else { print("Error: \(error!.localizedDescription)"); return }

        //2. We Have A Valid ARWorldMap So Save It To The Documents Directory
        guard let data = try? NSKeyedArchiver.archivedData(withRootObject: mapToShare, requiringSecureCoding: true) else { fatalError("Can't Encode Map") }

        do {

            //a. Create An Identifier For Our Map
            let mapIdentifier = "BlackMirrorzMap"

            //b. Create An Object To Save The Name And WorldMap
            var contentsToSave = BMWorlMapItem()

            //c. Get The Documents Directory
            let documentDirectory = try self.fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor:nil, create:false)

            //d. Create The File Name
            let savedFileURL = documentDirectory.appendingPathComponent("/\(mapIdentifier).bmarwp")

            //e. Set The Data & Save It To The Documents Directory
            contentsToSave[mapIdentifier] = data

            do{
                let archive = try NSKeyedArchiver.archivedData(withRootObject: contentsToSave, requiringSecureCoding: true)
                try archive.write(to: savedFileURL)

                //f. Show An Alert Controller To Share The Item
                let activityController = UIActivityViewController(activityItems: ["Check Out My Custom ARWorldMap", savedFileURL], applicationActivities: [])
                self.present(activityController, animated: true)

                print("Succesfully Saved Custom ARWorldMap")

            }catch{

                print("Error Generating WorldMap Object == \(error)")
            }

        } catch {

            print("Error Saving Custom WorldMap Object == \(error)")
        }

    }
}

这还会将数据保存到用户设备上的 Documents Directory,这样我们就可以检查是否一切正常,例如:

保存数据后,我们会向用户显示 UIActivityAlertController,以便用户可以将文件发送给他们的 email

因为我们现在可以导出数据,所以我们需要处理我们接收数据的方式 select 如何使用我们的自定义处理程序打开它:

这在我们的 AppDelegate 中是这样处理的:

//---------------------------
//MARK: - Custom File Sharing
//---------------------------

func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {

    //1. List Our Custom File Type Which Will Hold Our ARWorldMap
    guard url.pathExtension == "bmarwp" else { return false }

    //2. Post Our Data
    NotificationCenter.default.post(name: NSNotification.Name(rawValue: "MapReceived"), object: nil, userInfo: ["MapData" : url])

    return true
}

如您所见,当通过 AppDelegate 收到我们的自定义文件时,将发送一个 Notification,我们将在 ViewController 的 viewDidLoad 中注册例如:

 NotificationCenter.default.addObserver(self, selector: #selector(importWorldMap(_:)), name: NSNotification.Name(rawValue: "MapReceived"), object: nil)

现在我们已经完成了所有这些设置,我们当然需要提取数据以便使用。这是这样实现的:

/// Imports A WorldMap From A Custom File Type
///
/// - Parameter notification: NSNotification)
@objc public func importWorldMap(_ notification: NSNotification){

    //1. Remove All Our Content From The Hierachy
    self.augmentedRealityView.scene.rootNode.enumerateChildNodes { (existingNode, _) in existingNode.removeFromParentNode() }

    //2. Check That Our UserInfo Is A Valid URL
    if let url = notification.userInfo?["MapData"] as? URL{

        //3. Convert Our URL To Data
        do{
            let data = try Data(contentsOf: url)

            //4. Unarchive Our Data Which Is Of Type [String: Data] A.K.A BMWorlMapItem
            if let mapItem = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as! BMWorlMapItem,
               let archiveName = mapItem.keys.first,
               let mapData = mapItem[archiveName] {

                //5. Get The Map Data & Log The Anchors To See If It Includes Our BMAnchor Which We Saved Earlier
                if  let unarchivedMap = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [ARWorldMap.classForKeyedUnarchiver()], from: mapData),
                    let worldMap = unarchivedMap as? ARWorldMap {

                    print("Extracted BMWorldMap Item Named = \(archiveName)")

                    worldMap.anchors.forEach { (anchor) in if let name = anchor.name { print ("Anchor Name == \(name)") } }

                    //5. Restart Our Session
                    let configuration = ARWorldTrackingConfiguration()
                    configuration.planeDetection = .horizontal
                    configuration.initialWorldMap = worldMap
                    self.augmentedRealityView.session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
                }

            }

        }catch{

            print("Error Extracting Data == \(error)")
        }
    }
}

现在我们的数据已提取,我们只需要重新配置 Session 并加载地图。

您会注意到我正在记录 AnchorNames,作为检查过程是否成功的一种方式,因为我创建了一个名为 BMAnchor 的自定义 ARAnchor,我使用UITapGestureRecognizer 像这样:

//------------------------
//MARK: - User Interaction
//------------------------

/// Allows The User To Create An ARAnchor
///
/// - Parameter gesture: UITapGestureRecognizer
@objc func placeAnchor(_ gesture: UITapGestureRecognizer){

    //1. Get The Current Touch Location
    let currentTouchLocation = gesture.location(in: self.augmentedRealityView)

    //2. Perform An ARSCNHiteTest For Any Feature Points
    guard let hitTest = self.augmentedRealityView.hitTest(currentTouchLocation, types: .featurePoint).first else { return }

    //3. Create Our Anchor & Add It To The Scene
    let validAnchor = ARAnchor(name: "BMAnchor", transform: hitTest.worldTransform)
    self.augmentedRealitySession.add(anchor: validAnchor)

}

提取后,我通过 ARSCNViewDelegate 生成一个模型,这再次有助于检查我们的过程是否成功:

//-------------------------
//MARK: - ARSCNViewDelegate
//-------------------------

extension ViewController: ARSCNViewDelegate{

    func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {

        //1. Check We Have Our BMAnchor
        if let name = anchor.name, name == "BMAnchor" {

            //2. Create Our Model Node & Add It To The Hierachy
            let modelNode = SCNNode()

            guard let sceneURL = SCNScene(named: "art.scnassets/wavingState.dae") else { return nil }

            for childNode in sceneURL.rootNode.childNodes { modelNode.addChildNode(childNode) }

            return modelNode

        }else{

            return SCNNode()
        }

    }

}

希望这会为您指明正确的方向...

这里有一个完整的工作示例,供您和其他人试验并适应您的需求:Sharing ARWorldMaps

您需要做的就是等待会话开始 运行,放置您的模型,然后按保存。当出现警报时,将其通过电子邮件发送给自己,然后检查您的电子邮件,然后单击 .bmarwp 文件,该文件将自动加载到应用程序中 ^_________^,

Document Based App 中,您可以很容易地使用自定义文件类型。

以下是您 info.plist 的要求:

(a) 文档类型:

(b) 导出类型 UTI:

项目的信息部分如下所示:

因此,您最终会得到如下不同的屏幕:

它既适用于我创建的基于文档的基本应用程序,也适用于 Apple Files 应用程序。

希望对您有所帮助...

ARWorldMap不是文件格式,甚至也不是统一的数据格式。当 ARKit 创建世界地图时,它包含会话中存在的所有 ARAnchors,包括由 运行 ARKit 的任何应用程序定义的自定义锚点子类。您可以在 Apple 的示例代码中看到这一点:

  • Creating a Persistent AR Experience 将图像快照保存在锚点中,以帮助您重新调整自己的方向,以便稍后从世界地图恢复会话。
  • SwiftShot 在将世界地图发送给另一个玩家之前将游戏板位置保存在锚点中,以便两个玩家在开始游戏之前都知道板在哪里。

最重要的是,ARKit 甚至不保存世界地图 文件 — 你负责将 ARWorldMap 个实例(以及它们包含的任何内容)序列化为二进制数据并将其保存到文件中。 (或者用这些数据做其他事情,比如通过网络发送它。或者序列化一些对象图 包含 ARWorldMap 而不仅仅是地图本身。等等。)

因此,没有一种 "ARKit World Map" 格式——每个使用 ARKit 的应用程序都将自己的自定义数据保存在世界地图中,并且该数据通常对其他应用程序没有意义。 (有 SwiftShot 地图、ThisApp 地图、ThatApp 地图等)


现在,您 可以 定义您自己的文件格式 — 只需声明,当您的应用 NSKeyedArchiver-ing 一个 ARWorldMap 并写入结果Data 到一个文件,这是一个 .myappworldmap 文件,它有 UTI com.example.myapp,等等。您 可以 继续将其声明为用户可见的文档类型并实现文档浏览器支持。 (正如@BlackMirrorz 的回答中所描述的那样。)

但是你应该吗?

实际上,ARKit 世界地图(大部分)是瞬态 数据。为了能够重新定位到以前保存的世界地图,加载地图的设备需要与保存地图的设备处于非常相似的真实环境中——例如在同一个房间里,具有相同的照明条件,房间里通常有相同的东西(昨天凌乱的桌子并不总是像今天一样凌乱),等等。世界地图越旧,真实的可能性越大-它描述的世界环境不再足够相似。

您可以将 ARWorldMap 用于有限形式的持久性,但将它们视为用户文档并没有真正意义,例如 Word/Pages 文档或照片 — 如果您在设备之间或将它们复制到世界其他地方的其他人时,它们在接收端没有多大用处。

对于许多应用程序来说,从世界地图恢复会话的唯一有意义的方法是只保存一张地图——应用程序使用的最后一个环境——并在启动时尝试从该地图恢复。 (否则,假设用户处于不同的环境并重新开始。)

如果对您的应用来说记住多个过去的环境是有意义的——例如,一个放置虚拟家具的室内设计应用可能能够记住一个家的不同房间— 使 UI 用于选择保存的地图与您的应用程序的体验更加集成,而不是将它们踢出到通用文档浏览器可能对您的用户更有帮助。