【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://www.rhkb.cn/news/264617.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【非比较排序】计算排序算法

目录 CountSort计数排序 整体思想 图解分析 代码实现 时间复杂度&优缺分析 CountSort计数排序 计数排序是一种非比较排序,不需要像前面的排序一样去比较。 计数排序的特性总结: 1. 计数排序在数据范围集中时,效率很高,但…

golang gin单独部署vue3.0前后端分离应用

概述 因为公司最近的项目前端使用vue 3.0,后端api使用golang gin框架。测试通过后,博文记录,用于备忘。 步骤 npm run build,构建出前端项目的dist目录,dist目录的结构具体如下图 将dist目录复制到后端程序同级目录…

Unity中URP下实现水体(水面高光)

文章目录 前言一、实现高光反射原理1、原理:2、公式: 二、实现1、定义 _SpecularColor 作为高光反射的颜色2、定义 _SpecularIntensity 作为反射系数,控制高光反射的强度3、定义 _Smoothness 作为高光指数,用于模型高光范围4、模拟…

紫外-可见吸收光谱法(UV-Vis)是最常用吸收光谱技术 市场持续扩大

紫外-可见吸收光谱法(UV-Vis)是最常用吸收光谱技术 市场持续扩大 紫外-可见吸收光谱法,也称为紫外-可见分光光度法,简称UV-Vis,利用样品分子在紫外和可见光激发下产生电子能级跃迁形成的吸收光谱,对元素进行…

Day 2.exec函数族和线程的基本概念、相关函数接口

exec函数族 extern char **environ; int execl(const char *path, const char *arg, ... /* (char *) NULL */); int execlp(const char *file, const char *arg, ... /* (char *) NULL */); int execle(const…

9.网络游戏逆向分析与漏洞攻防-游戏网络架构逆向分析-接管游戏连接服务器的操作

内容参考于:易道云信息技术研究院VIP课 上一个内容:游戏底层功能对接类GameProc的实现 码云地址(master 分支):https://gitee.com/dye_your_fingers/titan 码云版本号:44c54d30370d3621c1e9ec3d7fa1e2a0…

全球游戏市场回暖,Flat Ads推动海外获客增长

摘要:热门游戏品类分析,解读新兴市场与赛道 近日,中国音数协游戏工委发布了《2023年中国游戏出海研究报告》,据报告数据显示,2023年,全球游戏市场规模11773.79亿元,同比增长6.00%,呈现增长回暖趋势。 图源:伽马数据 1.SLG和RPG游戏热度居高不下,休闲游戏增长势头强劲 目前,S…

Java四大引用详解:强引用、软引用、弱引用、虚引用

在JDK1.2以前的版本中,当一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于可触及状态,程序才能使用它。这就像在商店购买了某样物品后,如果有用就一直保留它,否则就把它扔到…

进行模型测量这种量出来坡面的是平面面积还是真实面积?

斜面面积,不是表面积。 DasViewer是由大势智慧自主研发的免费的实景三维模型浏览器,采用多细节层次模型逐步自适应加载技术,让用户在极低的电脑配置下,也能流畅的加载较大规模实景三维模型,提供方便快捷的数据浏览操作。 #DasViewer##实景三维##三维重建##三维模型…

基于springboot+vue的音乐网站(前后端分离)

博主主页:猫头鹰源码 博主简介:Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战,欢迎高校老师\讲师\同行交流合作 ​主要内容:毕业设计(Javaweb项目|小程序|Pyt…

Java优先级队列--堆

目录 1. 优先级队列 1.1 概念 2.优先级队列的模拟实现 2.1 堆的概念 2.2 堆的存储方式 2.3 堆的创建 2.3.1 堆向下调整 2.3.2 堆的创建 2.3.3 建堆的时间复杂度 2.4 堆的插入与删除 2.4.1 堆的插入 2.4.2 堆的删除 2.5 用堆模拟实现优先级队列 3.常用接口介绍 3…

【Excel PDF 系列】POI + iText 库实现 Excel 转换 PDF

你知道的越多,你不知道的越多 点赞再看,养成习惯 如果您有疑问或者见解,欢迎指教: 企鹅:869192208 文章目录 前言转换前后效果引入 pom 配置代码实现 前言 最近遇到生成 Excel 并转 pdf 的需求,磕磕碰碰总…

初学学习408之数据结构--数据结构基本概念

初学学习408之数据结构我们先来了解一下数据结构的基本概念。 数据结构:是相互之间存在一种或多种特定关系的数据元素的集合。 本内容来源于参考书籍《大话数据结构》与《王道数据结构》。除去书籍中的内容,作为初学者的我会尽力详细直白地介绍数据结构的…

元学习(meta-learning)的通俗解释

目录 1、什么是元学习 2、元学习还可以做什么 3、元学习是如何训练的 1、什么是元学习 meta-learning 的一个很经典的英文解释是 learn to learn,即学会学习。元学习是一个很宽泛的概念,可以有很多实现的方式,下面以目标检测的例子来解释…

JSON简介以及如何在Python中使用JSON

什么是JSON? JSON是"JavaScript Object Notation"的简称,是一种数据交换格式 JSON格式 假设我们有一个对象,这个对象有两个属性:“name”跟“age”。 在JSON中是这样表达的: { "name":"男孩…

基于JAVA springboot+mybatis智慧生活分享平台设计和实现

基于JAVA springbootmybatis智慧生活分享平台设计和实现 博主介绍:5年java开发经验,专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 央顺技术团队 Java毕设项目精品实战案例《1000套》 可定制系统 欢迎点赞 收藏 …

FL Studio Producer Edition2024中文进阶版Win/Mac

FL Studio Producer Edition,特别是其【中文进阶版 Win/Mac】,是数字音乐制作领域中的一款知名软件。它为广大音乐制作人、声音工程师以及音乐爱好者提供了一个从音乐构思到最终作品发布的完整解决方案。这个版本特别为中文用户优化,并兼容W…

Leetcode——hot3最长连续序列

最长连续序列 class Solution {public int longestConsecutive(int[] nums) {if(nums.length 0 || nums.length 1){return nums.length;}Arrays.sort(nums);int count 1;int max 1;for(int i 0; i < nums.length - 1; i){if(nums[i1] - nums[i] 1){count;if(count &…

[Linux]文件基础-如何管理文件

回顾C语言之 - 文件如何被写入 fopen fwrite fread fclose fseek … 这一系列函数都是C语言中对文件进行的操作&#xff1a; int main() {FILE* fpfopen("text","w");char str[20]"write into text";fputs(str,fp);fclose(fp);return 0; }而上…