【iOS ARKit】ARWorldMap

打印 上一主题 下一主题

主题 1002|帖子 1002|积分 3008

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
      ARWorldMap 用于存储 ARSession 检测扫描到的空间信息数据,包罗地标(Landmark)、特征点(Feature Point)、平面(Plane)等,以及利用者的利用信息,如利用者添加的 ARAnchor 和开发者自定义的一些信息。ARWorldMap 可以看作 ARSession 运行时的一次状态快照。
      在技能上,每个具备世界跟踪的 ARSession 都会时刻维护一个内部的世界舆图(internal world map),ARKit 正是利用这个舆图定位跟踪用户设备的姿态,利用 getCurrent WorldMap(completionHandler:)方法获取的 ARWroldMap只是特定时刻内部世界舆图的一个快照。    
ARWorldMap 概述

     长期化地存储应用进程数据,ARKit 提供了 ARWorldMap 功能,ARWorldMap 本质是将 AR 场景状态信息转换为可存储可传输的情势(即序列化)保存到文件系统或者数据库中,当利用者再次加载这些景状态信息后即可规复应用进程。ARWorldMap 不仅保存了应用进程状态信息,还保存了场景特征点云息,在利用者再次加载这些状态数据后,ARKit 可通过保存的特征点云信息与当前用户摄像头获取的特点云信息进行对比匹配从而更新当前用户的坐标,确保两个坐标系的匹配。
存储与加载 ARWorldMap

     存储 ARWorldMap最重要的是从 ARSession 中获取场景的 ARWorldMap 并序列化之,然后保件系统中。加载 ARWorldMap 则首先要从文件系统中获取ARWorldMap 并反序列化之,然后利用这个ARWorldMap 重启 ARSession。存储与加载 ARWorldMap 完整代码如下所示,稍后我们将对代码中所用技能进行详细分析。
  1. //
  2. //  ARWorldMapSaveAndLoad.swift
  3. //  ARKitDeamo
  4. //
  5. //  Created by zhaoquan du on 2024/2/20.
  6. //
  7. import SwiftUI
  8. import ARKit
  9. import RealityKit
  10. import Combine
  11. struct ARWorldMapSaveAndLoad: View {
  12.     var viewModel: ViewModel = ViewModel()
  13.    
  14.     var body: some View {
  15.         ARWorldMapSaveAndLoadContainer(viewModel: viewModel)
  16.             .overlay(
  17.                 VStack{
  18.                     Spacer()
  19.                     
  20.                     HStack{
  21.                         Button(action: {loadWorldMap()}) {
  22.                             Text("加载信息")
  23.                                 .frame(width:150,height:50)
  24.                                 .font(.system(size: 17))
  25.                                 .foregroundColor(.black)
  26.                                 .background(Color.white)
  27.                            
  28.                                 .opacity(0.6)
  29.                         }
  30.                         .cornerRadius(10)
  31.                         
  32.                         Button(action: {saveWorldMap()}) {
  33.                             Text("保存信息")
  34.                                 .frame(width:150,height:50)
  35.                                 .font(.system(size: 17))
  36.                                 .foregroundColor(.black)
  37.                                 .background(Color.white)
  38.                            
  39.                                 .opacity(0.6)
  40.                         }
  41.                         .cornerRadius(10)
  42.                     }
  43.                     Spacer().frame(height: 40)
  44.                 }
  45.             ).edgesIgnoringSafeArea(.all).navigationTitle("保存与加载ARWorldMap")
  46.     }
  47.     var mapSaveURL: URL = {
  48.         do {
  49.             return try FileManager.default
  50.                 .url(for: .documentDirectory,
  51.                      in: .userDomainMask,
  52.                      appropriateFor: nil,
  53.                      create: true)
  54.                 .appendingPathComponent("arworldmap.arexperience")
  55.         } catch {
  56.             fatalError("获取路径出错: \(error.localizedDescription)")
  57.         }
  58.     }()
  59.     func saveWorldMap() {
  60.         print("save:\(String(describing: viewModel.arView))")
  61.         
  62.         self.viewModel.arView?.session.getCurrentWorldMap(completionHandler: { loadWorld, error in
  63.             guard let worldMap = loadWorld else {
  64.                 print("当前无法获取ARWorldMap:\(error!.localizedDescription)")
  65.                 return
  66.             }
  67.            
  68.             do {
  69.                 let data = try NSKeyedArchiver.archivedData(withRootObject: worldMap, requiringSecureCoding: true)
  70.                 try data.write(to: mapSaveURL, options: [.atomic])
  71.                 print("ARWorldMap保存成功")
  72.             } catch {
  73.                 fatalError("无法保存ARWorldMap: \(error.localizedDescription)")
  74.             }
  75.         })
  76.     }
  77.     func loadWorldMap() {
  78.         print("load:\(String(describing: viewModel.arView))")
  79.         guard let data = try? Data(contentsOf: mapSaveURL) else {
  80.             print("load world map faile")
  81.             return
  82.         }
  83.         var worldMap: ARWorldMap?
  84.         do {
  85.             worldMap = try NSKeyedUnarchiver.unarchivedObject(ofClass: ARWorldMap.self, from: data)
  86.         } catch let error {
  87.             print("ARWorldMap文件格式不正确:\(error)")
  88.         }
  89.         guard let worldMap = worldMap else {
  90.             print("无法解压ARWorldMap")
  91.             return
  92.         }
  93.         let config = ARWorldTrackingConfiguration()
  94.         config.planeDetection = .horizontal
  95.         config.initialWorldMap = worldMap
  96.         
  97.         self.viewModel.arView?.session.run(config,options: [.resetTracking, .removeExistingAnchors])
  98.             
  99.     }
  100.    
  101.     class ViewModel: NSObject,ARSessionDelegate{
  102.         var arView: ARView? = nil
  103.         
  104.       
  105.         var planeEntity : ModelEntity? = nil
  106.         var raycastResult : ARRaycastResult?
  107.         var isPlaced = false
  108.         var robotAnchor: AnchorEntity?
  109.         let robotAnchorName = "drummerRobot"
  110.         var planeAnchor = AnchorEntity()
  111.         
  112.         func createPlane()  {
  113.            
  114.             guard let arView = arView else {
  115.                 return
  116.             }
  117.          
  118.             if let an = arView.scene.anchors.first(where: { an in
  119.                 an.name == "setModelPlane"
  120.             }){
  121.                 arView.scene.anchors.remove(an)
  122.             }
  123.             do {
  124.                 let planeMesh = MeshResource.generatePlane(width: 0.15, depth: 0.15)
  125.                 var planeMaterial = SimpleMaterial(color: SimpleMaterial.Color.red, isMetallic: false)
  126.                 planeMaterial.color =  try SimpleMaterial.BaseColor(tint:UIColor.yellow.withAlphaComponent(0.9999), texture: MaterialParameters.Texture(TextureResource.load(named: "AR_Placement_Indicator")))
  127.                 planeEntity = ModelEntity(mesh: planeMesh, materials: [planeMaterial])
  128.                
  129.                 planeAnchor = AnchorEntity(plane: .horizontal)
  130.                 planeAnchor.addChild(planeEntity!)
  131.                 planeAnchor.name = "setModelPlane"
  132.                
  133.                 arView.scene.addAnchor(planeAnchor)
  134.             } catch let error {
  135.                 print("加载文件失败:\(error)")
  136.             }
  137.         }
  138.         
  139.         func setupGesture(){
  140.             
  141.             let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
  142.             self.arView?.addGestureRecognizer(tap)
  143.         }
  144.         @objc func handleTap(sender: UITapGestureRecognizer){
  145.             sender.isEnabled = false
  146.             sender.removeTarget(nil, action: nil)
  147.             isPlaced = true
  148.             let anchor = ARAnchor(name: robotAnchorName, transform: raycastResult?.worldTransform ?? simd_float4x4())
  149.             self.arView?.session.add(anchor: anchor)
  150.             
  151.             robotAnchor = AnchorEntity(anchor: anchor)
  152.             
  153.             
  154.             do {
  155.                 let robot =  try ModelEntity.load(named: "toy_drummer")
  156.                 robotAnchor?.addChild(robot)
  157.                 robot.scale = [0.01,0.01,0.01]
  158.                 self.arView?.scene.addAnchor(robotAnchor!)
  159.                 print("Total animation count : \(robot.availableAnimations.count)")
  160.                 robot.playAnimation(robot.availableAnimations[0].repeat())
  161.             } catch {
  162.                 print("找不到USDZ文件")
  163.             }
  164.             
  165. //            var cancellable: Cancellable?
  166. //            cancellable = ModelEntity.loadModelAsync(named: "toy_drummer.usdz")
  167. //                .sink(receiveCompletion: { error in
  168. //                    print("laod error:\(error)")
  169. //                    cancellable?.cancel()
  170. //                }, receiveValue: {[weak self] model in
  171. //                    guard let robotAnchor = self?.robotAnchor else {
  172. //                        return
  173. //                    }
  174. //                    robotAnchor.addChild(model)
  175. //                    model.scale = [0.01,0.01,0.01]
  176. //                    self?.arView?.scene.addAnchor(robotAnchor)
  177. //                    //用异步方法加载模型开启骨骼动画会crash,不知到是啥原因
  178. //                    //model.playAnimation(model.availableAnimations[0].repeat())
  179. //                    cancellable?.cancel()
  180. //                })
  181.             planeEntity?.removeFromParent()
  182.             planeEntity = nil
  183.         }
  184.         
  185.         func session(_ session: ARSession, didUpdate frame: ARFrame) {
  186.             guard !isPlaced, let arView = arView else{
  187.                 return
  188.             }
  189.             //射线检测
  190.             guard let result = arView.raycast(from: arView.center, allowing: .estimatedPlane, alignment: .horizontal).first else {
  191.                 return
  192.             }
  193.             raycastResult = result
  194.             planeEntity?.setTransformMatrix(result.worldTransform, relativeTo: nil)
  195.         }
  196.         
  197.         func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
  198.             guard !anchors.isEmpty,robotAnchor == nil else {
  199.                
  200.                 return
  201.             }
  202.             var panchor: ARAnchor? = nil
  203.             for anchor in anchors {
  204.                 if anchor.name == robotAnchorName {
  205.                     panchor = anchor
  206.                     break
  207.                 }
  208.             }
  209.             guard let pAnchor = panchor else {
  210.                 return
  211.             }
  212.             //放置虚拟元素
  213.             robotAnchor = AnchorEntity(anchor: pAnchor)
  214.             do {
  215.                 let robot =  try ModelEntity.load(named: "toy_drummer")
  216.                 robotAnchor?.addChild(robot)
  217.                 robot.scale = [0.01,0.01,0.01]
  218.                 self.arView?.scene.addAnchor(robotAnchor!)
  219.                 print("Total animation count : \(robot.availableAnimations.count)")
  220.                 robot.playAnimation(robot.availableAnimations[0].repeat())
  221.             } catch {
  222.                 print("找不到USDZ文件")
  223.             }
  224.             isPlaced = true
  225.             planeEntity?.removeFromParent()
  226.             planeEntity = nil
  227.             print("加载模型成功")
  228.         }
  229.         
  230.     }
  231. }
  232. struct ARWorldMapSaveAndLoadContainer: UIViewRepresentable {
  233.     var viewModel: ARWorldMapSaveAndLoad.ViewModel
  234.    
  235.     func makeUIView(context: Context) -> some ARView {
  236.         let arView = ARView(frame: .zero)
  237.       
  238.         
  239.         return arView
  240.     }
  241.    
  242.     func updateUIView(_ uiView: UIViewType, context: Context) {
  243.         
  244.         let config = ARWorldTrackingConfiguration()
  245.         config.planeDetection = .horizontal
  246.         uiView.session.run(config)
  247.         
  248.         viewModel.arView = uiView
  249.         uiView.session.delegate = viewModel
  250.         
  251.         viewModel.createPlane()
  252.         viewModel.setupGesture()
  253.         
  254.     }
  255.    
  256. }
  257. #Preview {
  258.     ARWorldMapSaveAndLoad()
  259. }
复制代码
代码实现的功能如下:
(1)进行平面检测,在检测到可用平面时实例化一个指示图标用于指示放置位置。
(2)添加屏幕单击手势,在平面可用时通过单击屏幕会在指示图标位置放置虛拟呆板人模型。
(3) 当用户单击“保存AR信息”按钮时会从当前 ARSesion 中获取 ARWorldMap 并序列化之,然后保存到文件系统中。
(4)当用户单击“加载AR信息”按钮时会从文件系统中加载ARWorldMap 并反序列化之,然后利用该ARWorldMap 重启 ARSession。
        在第(2)项功能中,即 bandleTap()方法中的代码,我们首先将屏幕单击手势禁用,以防止添加多个呆板人模型,然后禁止表现指示图标。随后利用命申点的坐标天生了一个ARAnchor,并将其添加到 ARSession中,注意这里设置了 ARAnchor 的名字(name)属性,这步很关键,因为后续我们须要利用该ARAnchor 的名字来规复虚拟元素。后续代码是利用异步方式加载呆板人模型,不赘述。
       在第(3)项功能中,即 saveAR WorldMap()方法中代码,首先利用 getCurrent WorldMap()方法从 ARSession中获取 ARWorldVap,在闭包中,利用 let data = try NSKeyedArchiver. archivedData (with RootObject: map,requiringSecureCoding: true)语句对获取的ARWorldMap 进行序列化,然后利用 try data. write (to:mapSaveURL., options: [.atomic])方法将序列化后的 ARWorldMap 写人到文件系统中。
       在第(4)项功能中,即 loadARWorldMap()方法中代码,首先从文件系统中读取存储的 ARWorldMap文件,并利用 let worldMap = try NSKeyedUnarchiver. unarchivedObject (ofClass: AR WorldMap. self,from:data)语句将其反序列化。在得到反序列化后的ARWorldMap 后,就可以利用其作为配置文件的initialWorldMap 属性重启 ARSession,当用户设备所在的物理情况与 ARWorldMap 保存时的物理情况一致时(即情况特征点信息匹配时),ARKit 就会校正用户设备坐标信息,将当前用户设备的坐标信息与ARWorldMap 中存储的用户设备坐标信息关联起来,并规复相应的ARAnchor 信息,这时规复的ARAnchor 姿态与ARWorldMap 中存储的姿态就是一致的,即ARAnchor 在物理情况中的位置与方向是一致的,这就达到了应用进程数据存储与加载的目的。
      正如前文所述,ARWorldMap 并不会存储虚拟元素本身,因此,须要手动规复虚拟元素,因为虚拟元素总是与ARAnchor 关联,利用 ARAnchor 的名字(name)属性我们就可以规复关联的虚拟元素。在代码中,session(_:didAdd:)方法就用于规复关联的虚拟元素,在该方法中,通过ARAnchor.name 进行ARAnchor的对比,假如名字一样且当前没有加载呆板人模型则利用异步方法加载之。通过这种方式,我们就可以逐一地规复全部的虚拟元素,从而规复整个场景。
       运行案例,在检测到的平面上添加虚拟元素后单击“保存舆图”按钮保存 AR WorldMap,稍后单击“加载舆图”按钮,或者关闭应用,在重新运行后单击“加载舆图”按钮,可以看到虚拟呆板人模型会出如今物理世界中的固定位置,如图所示。

      究竟上,在将 ARWorldMap 设置 ARWorldTrackingConfiguration. initialWorldMap 属性启动ARSession 时,ARKit 会进入重定位(relocalize)过程,在这个过程中,ARKit 会尝试将当前设备摄像头采集的情况信息与 ARWorldMap 中存储的情况特征信息进行匹配。因此,保持当前设备姿态与存储ARWorldMap 时的设备姿态一致时(即提高情况特征点匹配成功率)可以更快速地重定位。   
具体代码地点:GitHub - duzhaoquan/ARkitDemo

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

南七星之家

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表