【iOS ARKit】ARWorldMap

      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 完整代码如下所示,稍后我们将对代码中所用技术进行详细解析。

//
//  ARWorldMapSaveAndLoad.swift
//  ARKitDeamo
//
//  Created by zhaoquan du on 2024/2/20.
//import SwiftUI
import ARKit
import RealityKit
import Combinestruct ARWorldMapSaveAndLoad: View {var viewModel: ViewModel = ViewModel()var body: some View {ARWorldMapSaveAndLoadContainer(viewModel: viewModel).overlay(VStack{Spacer()HStack{Button(action: {loadWorldMap()}) {Text("加载信息").frame(width:150,height:50).font(.system(size: 17)).foregroundColor(.black).background(Color.white).opacity(0.6)}.cornerRadius(10)Button(action: {saveWorldMap()}) {Text("保存信息").frame(width:150,height:50).font(.system(size: 17)).foregroundColor(.black).background(Color.white).opacity(0.6)}.cornerRadius(10)}Spacer().frame(height: 40)}).edgesIgnoringSafeArea(.all).navigationTitle("保存与加载ARWorldMap")}var mapSaveURL: URL = {do {return try FileManager.default.url(for: .documentDirectory,in: .userDomainMask,appropriateFor: nil,create: true).appendingPathComponent("arworldmap.arexperience")} catch {fatalError("获取路径出错: \(error.localizedDescription)")}}()func saveWorldMap() {print("save:\(String(describing: viewModel.arView))")self.viewModel.arView?.session.getCurrentWorldMap(completionHandler: { loadWorld, error inguard let worldMap = loadWorld else {print("当前无法获取ARWorldMap:\(error!.localizedDescription)")return}do {let data = try NSKeyedArchiver.archivedData(withRootObject: worldMap, requiringSecureCoding: true)try data.write(to: mapSaveURL, options: [.atomic])print("ARWorldMap保存成功")} catch {fatalError("无法保存ARWorldMap: \(error.localizedDescription)")}})}func loadWorldMap() {print("load:\(String(describing: viewModel.arView))")guard let data = try? Data(contentsOf: mapSaveURL) else {print("load world map faile")return}var worldMap: ARWorldMap?do {worldMap = try NSKeyedUnarchiver.unarchivedObject(ofClass: ARWorldMap.self, from: data)} catch let error {print("ARWorldMap文件格式不正确:\(error)")}guard let worldMap = worldMap else {print("无法解压ARWorldMap")return}let config = ARWorldTrackingConfiguration()config.planeDetection = .horizontalconfig.initialWorldMap = worldMapself.viewModel.arView?.session.run(config,options: [.resetTracking, .removeExistingAnchors])}class ViewModel: NSObject,ARSessionDelegate{var arView: ARView? = nilvar planeEntity : ModelEntity? = nilvar raycastResult : ARRaycastResult?var isPlaced = falsevar robotAnchor: AnchorEntity?let robotAnchorName = "drummerRobot"var planeAnchor = AnchorEntity()func createPlane()  {guard let arView = arView else {return}if let an = arView.scene.anchors.first(where: { an inan.name == "setModelPlane"}){arView.scene.anchors.remove(an)}do {let planeMesh = MeshResource.generatePlane(width: 0.15, depth: 0.15)var planeMaterial = SimpleMaterial(color: SimpleMaterial.Color.red, isMetallic: false)planeMaterial.color =  try SimpleMaterial.BaseColor(tint:UIColor.yellow.withAlphaComponent(0.9999), texture: MaterialParameters.Texture(TextureResource.load(named: "AR_Placement_Indicator")))planeEntity = ModelEntity(mesh: planeMesh, materials: [planeMaterial])planeAnchor = AnchorEntity(plane: .horizontal)planeAnchor.addChild(planeEntity!)planeAnchor.name = "setModelPlane"arView.scene.addAnchor(planeAnchor)} catch let error {print("加载文件失败:\(error)")}}func setupGesture(){let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))self.arView?.addGestureRecognizer(tap)}@objc func handleTap(sender: UITapGestureRecognizer){sender.isEnabled = falsesender.removeTarget(nil, action: nil)isPlaced = truelet anchor = ARAnchor(name: robotAnchorName, transform: raycastResult?.worldTransform ?? simd_float4x4())self.arView?.session.add(anchor: anchor)robotAnchor = AnchorEntity(anchor: anchor)do {let robot =  try ModelEntity.load(named: "toy_drummer")robotAnchor?.addChild(robot)robot.scale = [0.01,0.01,0.01]self.arView?.scene.addAnchor(robotAnchor!)print("Total animation count : \(robot.availableAnimations.count)")robot.playAnimation(robot.availableAnimations[0].repeat())} catch {print("找不到USDZ文件")}//            var cancellable: Cancellable?
//            cancellable = ModelEntity.loadModelAsync(named: "toy_drummer.usdz")
//                .sink(receiveCompletion: { error in
//                    print("laod error:\(error)")
//                    cancellable?.cancel()
//                }, receiveValue: {[weak self] model in
//                    guard let robotAnchor = self?.robotAnchor else {
//                        return
//                    }
//                    robotAnchor.addChild(model)
//                    model.scale = [0.01,0.01,0.01]
//                    self?.arView?.scene.addAnchor(robotAnchor)
//                    //用异步方法加载模型开启骨骼动画会crash,不知到是啥原因
//                    //model.playAnimation(model.availableAnimations[0].repeat())
//                    cancellable?.cancel()
//                })planeEntity?.removeFromParent()planeEntity = nil}func session(_ session: ARSession, didUpdate frame: ARFrame) {guard !isPlaced, let arView = arView else{return}//射线检测guard let result = arView.raycast(from: arView.center, allowing: .estimatedPlane, alignment: .horizontal).first else {return}raycastResult = resultplaneEntity?.setTransformMatrix(result.worldTransform, relativeTo: nil)}func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {guard !anchors.isEmpty,robotAnchor == nil else {return}var panchor: ARAnchor? = nilfor anchor in anchors {if anchor.name == robotAnchorName {panchor = anchorbreak}}guard let pAnchor = panchor else {return}//放置虚拟元素robotAnchor = AnchorEntity(anchor: pAnchor)do {let robot =  try ModelEntity.load(named: "toy_drummer")robotAnchor?.addChild(robot)robot.scale = [0.01,0.01,0.01]self.arView?.scene.addAnchor(robotAnchor!)print("Total animation count : \(robot.availableAnimations.count)")robot.playAnimation(robot.availableAnimations[0].repeat())} catch {print("找不到USDZ文件")}isPlaced = trueplaneEntity?.removeFromParent()planeEntity = nilprint("加载模型成功")}}
}struct ARWorldMapSaveAndLoadContainer: UIViewRepresentable {var viewModel: ARWorldMapSaveAndLoad.ViewModelfunc makeUIView(context: Context) -> some ARView {let arView = ARView(frame: .zero)return arView}func updateUIView(_ uiView: UIViewType, context: Context) {let config = ARWorldTrackingConfiguration()config.planeDetection = .horizontaluiView.session.run(config)viewModel.arView = uiViewuiView.session.delegate = viewModelviewModel.createPlane()viewModel.setupGesture()}}
#Preview {ARWorldMapSaveAndLoad()
}

代码实现的功能如下:

(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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/2812090.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

Wget: 如何批量下载风云卫星和NASA数据?

01 批量下载风云数据 首先进入风云卫星服务官网: https://satellite.nsmc.org.cn/portalsite/default.aspx 假定我们下载火点数据,如下先找到下载界面: 接着依据数据所对应卫星\传感器\产品名称进行检索: 选定需要的数据加入到购物车中: 接着提交订单等待所有数据回调完成就…

TensorFlow2.x 精选笔记(2)自动求导与概率

学习参考: 动手学深度学习2.0Deep-Learning-with-TensorFlow-bookpytorchlightning ①如有冒犯、请联系侵删。 ②已写完的笔记文章会不定时一直修订修改(删、改、增),以达到集多方教程的精华于一文的目的。 ③非常推荐上面(学习参考&#x…

python爬虫实战:获取电子邮件和联系人信息

引言 在数字时代,电子邮件和联系人信息成为了许多企业和个人重要的资源,在本文中,我们将探讨如何使用Python爬虫从网页中提取电子邮件和联系人信息,并附上示例代码。 目录 引言 二、准备工作 你可以使用以下命令来安装这些库&a…

记录一下 Unity团结引擎开发OpenHarmony Next 应用 环境搭建流程

原视频链接 记录环境搭建过程~,本文是图文版本 一、打开团结引擎官网下载对应的 团结引擎版本 官网地址:https://unity.cn/tuanjie/releases 根据各自的开发环境下载对应的软件版本,我是 windwos 环境,我就下载 windows 环境 …

【学习笔记】Vue3源码解析:第二部分-实现响应式(2)

课程地址:【已完结】全网最详细Vue3源码解析!(一行行带你手写Vue3源码) 第二部分-实现响应式(2):(对应课程的第6-9节) 第6节:《实现proxy代理以及解决重复代…

GDB之(1)入门指令参数介绍

GDB之(1)基础入门指令参数介绍 Author:Once Day Date: 2022年7月29日/2024年2月26日 漫漫长路,才刚刚开始… 全系列文章请查看专栏: Linux实践记录_Once-Day的博客-CSDN博客 推荐参考文档: GDB: The GNU Project Debugger (sourceware.o…

32单片机基础:TIM定时中断

STM32中功能最强大,结构最复杂的一个外设——定时器 因为定时器的内容很多,所以本大节总共分为4个部分,8小节。 第一部分:主要讲定时器基本的定时功能,也就是定一个时间,然后让定时器每隔这个时间产生一个中断&#…

C++笔记之执行一个可执行文件时指定动态库所存放的文件夹lib的路径

C++笔记之执行一个可执行文件时指定动态库所存放的文件夹lib的路径 参考博文: 1.C++笔记之执行一个可执行文件时指定动态库所存放的文件夹lib的路径 2.Linux笔记之LD_LIBRARY_PATH详解 3.qt-C++笔记之使用QProcess去执行一个可执行文件时指定动态库所存放的文件夹lib的路径 c…

适合新手博主站长使用的免费响应式WordPress博客主题JianYue

这款JianYue主题之所以命名为 JianYue,意思就是简单而不简约的。是根据Blogs主题优化而成,剔除了一些不必要的功能及排版,仅保留一种博客布局,让新手站长能够快速手上WordPress。可以说这款主题比较适合新手博主站长使用&#xff…

GDB动态调试学习-2-【断点 观察点】

文章目录 在程序地址上打断点在程序入口处打断点获取程序入口地址 在命名空间设置断点命名空间给命名空间的函数下断电 在文件行号上打断点保存已经设置的断点设置临时断点设置条件断点忽略断点 在程序地址上打断点 当调试汇编程序,或者没有调试信息的程序时&#…

【Rust】简介、安装和编译

文章目录 一、Rust简介二、Rust 安装三、Rust 程序结构3.1 模块(Modules):3.2 函数(Functions):3.3 变量(Variables):3.4 控制流(Control Flow)&a…

Verilog中向量的位截取、拼接

1、位截取: 当索引均为常数 例如:men[4:1]men[4-:4] men[1:4] 索引是变量 语法为:men[base:width] or men[base-:width] 例如:if cnt8, men[cnt:4] 等于 men[11:8]; men[cnt-:4] 等于men[8:5]. 其中,base可变&am…

el-table样式问题:如何修改element-ui表格中按钮悬浮显示但是被el-table溢出隐藏的问题?

最近在写elment-ui样式表格中遇到了溢出隐藏的问题 修改前 修改后 是由于el-table__body-wrapper为 overflow:hidden导致的 解决方式: .el-table__body-wrapper {overflow: visible !important; } //或者 /deep/.el-table__body-wrapper {overflow: v…

ONLYOFFICE 桌面编辑器 v8.0 更新内容详细攻略

文章目录 引言PDF 表单RTL 支持电子表格中的新增功能Moodle 集成用密码保护 PDF 文件从“开始”菜单快速创建文档本地界面主题下载安装桌面编辑工具总结 引言 官网链接: ONLYOFFICE 官方网址 ONLYOFFICE 桌面编辑器是一款免费的文档处理软件,适用于 Li…

主机字节序与网络字节序

大端序和小端序 大端序(Big Endian)和小端序(Little Endian)是两种计算机存储数据的方式。 大端序指的是将数据的高位字节存储在内存的低地址处,而将低位字节存储在内存的高地址处。这类似于我们阅读多位数时从左往右…

【电子书】研发管理

资料 wx:1945423050 整理了一些互联网电子书,推荐给大家 研发管理 ABAQUS 6.14中文版有限元分析与实例详解.epubAkka入门与实践.epubAltium Designer 16电路设计与仿真从入门到精通.epubAltium Designer17电子设计速成实战宝典.epubApache Kafka源码剖…

ChatGPT带火的HBM是什么?

“ChatGPT是人工智能领域的iPhone时刻,也是计算领域有史以来最伟大的技术之一。” 英伟达创始人兼CEO黄仁勋此前这样盛赞ChatGPT。 ChatGPT突然爆火,对大算力芯片提出了更高更多的要求。近日,据韩国经济日报报道,受惠于ChatGPT&am…

ZYNQ:串口-CAN协议转换

前言 目前已经实现zynq的PS-CAN和PL-CAN功能。串口-CAN协议转换是实现以太网-CAN功能的过渡,通过这个流程能够减少后期以太网工程出现问题的频率。阶段性功能目标如下: 实现数据在CAN调试助手和串口调试助手之间的来回转换,从而了解中断机制…

CMU15445实验总结(Spring 2023)

CMU15445实验总结(Spring 2023) 背景 菜鸟博主是2024届毕业生,学历背景太差,导致23年秋招无果,准备奋战春招。此前有读过LevelDB源码的经历,对数据库的了解也仅限于LevelDB。奔着”有对比才能学的深“的理念,以及缓解…

MySQL基础(二)

文章目录 MySQL基础(二)1. 数据库操作-DQL1.1 介绍1.2 语法1.3 基本查询1.4 条件查询1.5 聚合函数1.6 分组查询1.7 排序查询1.8 分页查询1.9 案例1.9.1 案例一1.9.2 案例二 2. 多表设计2.1 一对多2.1.1 表设计2.1.2 外键约束 2.2 一对一2.3 多对多2.4 案…