【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.mzph.cn/news/704457.shtml

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

相关文章

数据采集设备:安装过程的要点与注意事项

在当今的数据驱动世界,数据采集设备在各行各业中发挥着至关重要的作用。从工业生产到环境监测,再到医疗诊断,数据采集设备为我们提供了大量有价值的信息。然而,要想充分发挥这些设备的潜力,首先需要确保它们被正确地安…

黑马程序员——接口测试——day03

目录: Potman断言 Postman断言简介Postman常用断言 断言响应状态码断言包含某字符串断言JSON数据Postman断言工作原理Postman关联 简介实现步骤核心代码创建环境案例1案例2Postman参数化 简介数据文件简介编写数据文件 CSV文件JSON文件导入数据文件到postman读取数…

啊丢的刷题记录手册(洛谷题单排序篇)

1.洛谷题P1923 求第k小的数 题目描述 输入 n&#xff08;1≤n<5000000 且 n 为奇数&#xff09;个数字ai​&#xff08;1≤ai​<109&#xff09;&#xff0c;输出这些数字的第 k 小的数。最小的数是第 0 小。 请尽量不要使用 nth_element 来写本题&#xff0c;因为本题…

换档控制支架的尺寸优化

前言 本示例使用优化模块&#xff0c;通过在保持原始重量的同时改变壳体厚度&#xff0c;使冲压钣金支架中的应变能之和最小化&#xff08;使刚度最大化&#xff09;。 本页讨论 前言应用描述Abaqus建模方法和仿真技术文件参考 应用描述 本示例说明了汽车变速箱连杆中使用的…

亿道丨三防平板pad丨三防平板是指哪三防丨三防工业级平板电脑

三防工业级平板电脑成为许多行业中的重要工具。本文将介绍三防工业级平板电脑的特点以及其在各个领域中的广泛应用。 三防工业级平板电脑的特点 三防工业级平板电脑是指具备防水、防尘和防震功能的平板电脑。这些特点使得它们能够在恶劣环境中工作&#xff0c;如沙尘飞扬的工地…

sockaddr结构体、sockaddr_in结构体和gethostbyname函数

sockaddr结构体 sockaddr结构体是在网络编程中经常使用的一个数据结构&#xff0c;用来表示套接字地址。它是一个通用的地址结构&#xff0c;可以用于不同的协议&#xff08;如IPv4、IPv6、UNIX等&#xff09;。 在C语言中&#xff0c;sockaddr结构体定义如下&#xff1a; s…

交换两个整数

交换两个整数 题目描述&#xff1a;解法思路&#xff1a;解法代码&#xff1a;运行结果&#xff1a; 题目描述&#xff1a; 写⼀个函数Swap&#xff0c;可以交换两个整数的内容。注意必须实现成函数完成。 输入2个整数&#xff0c;调用自定义函数Swap&#xff0c;完成2个整数的…

【HarmonyOS】鸿蒙开发之Stage模型-基本概念——第4.1章

Stage模型-基本概念 名词解释 AbilityStage:应用组件的“舞台“ UIAbility:包含UI界面的应用组件&#xff0c;是系统调度的基本单元 WindowStage:组件内窗口的“舞台“ Window&#xff1a;用来绘制UI页面的窗口 HAP:Harmony Ability Package(鸿蒙能力类型的包) HSP:Harmony Sh…

C++17之折叠表达式

相关文章系列 深入理解可变参数(va_list、std::initializer_list和可变参数模版) 目录 1.介绍 2.应用 2.1.使用折叠表达式 2.2.支持的运算符 2.3.使用折叠处理类型 3.总结 1.介绍 折叠表达式是C17新引进的语法特性。使用折叠表达式可以简化对C11中引入的参数包的处理&…

合泰杯开发板HT66F2390入门教程(点亮LED灯)——获得成就:点灯大师

前言 前不久报名了合泰杯竞赛项目&#xff0c;然后手上也是有一个HT66F2390的开发板&#xff0c;我就打算先从点灯开始&#xff0c;学习一个新的芯片第一步都是先成为点灯大师。 一开始&#xff0c;我在网上搜寻了许多的代码示例&#xff0c;希望能够顺利实现LED的控制。然而&…

LeetCode第七题: 整数反转

题目描述 给你一个 32 位的有符号整数 x​ &#xff0c;返回将 x​ 中的数字部分反转后的结果。 如果反转后整数超过 32 位的有符号整数的范围 [−2^31, 2^31 − 1]​ &#xff0c;就返回 0。 假设环境不允许存储 64 位整数&#xff08;有符号或无符号&#xff09;。 示例 …

铭瑄科技——为星闪技术发展与应用带来新推力

随着智能化生活逐渐普及&#xff0c;无线通信不仅是不仅是信息时代的重要基础设施&#xff0c;而且是推动社会向智能化发展的核心力量之一&#xff0c;其中短距无线通信更是推动未来智能化发展的关键。 为积极推动未来硬件智能化、产业智能化发展&#xff0c;铭瑄正式宣布成为星…

黑马头条-day10

文章目录 app端文章搜索1、文章搜索1.1 ElasticSearch环境搭建1.2 索引库创建①需求分析②ES导入数据场景分析③创建索引和映射 1.3 索引数据同步①app文章历史数据导入ES②文章实时数据导入ES 1.4 文章搜索多条件复合查询①关键词搜索②搜索接口定义 2、搜索历史记录2.1 需求说…

积分商城管理系统的设计与实现

积分商城管理系统的设计与实现 获取源码——》公主号&#xff1a;计算机专业毕设大全

javascript给对象添加迭代器

迭代器是啥就自行百度了 为啥for…of可以遍历数组&#xff0c;为啥不能遍历对象&#xff0c;就是for…of会调用迭代器&#xff0c;而数组是内置了迭代器了&#xff0c;而对象没有内置&#xff0c;所以直接使用for…of遍历对象会报错&#xff0c;因此只用在对象的原型上面自定义…

YOLO算法改进Backbone系列之:EfficientViT

EfficientViT: Memory Effificient Vision Transformer with Cascaded Group Attention 摘要&#xff1a;视觉transformer由于其高模型能力而取得了巨大的成功。然而&#xff0c;它们卓越的性能伴随着沉重的计算成本&#xff0c;这使得它们不适合实时应用。在这篇论文中&#x…

一般情况下,硬件中使用Repeating Sequence出现波形很奇怪就是数据的周期频率和mcu运行的频率不一致导致的

一般情况下&#xff0c;出现波形很奇怪就是数据的周期频率和mcu运行的频率不一致导致的 把timer values 修改为0 1就好了&#xff0c;如果是0&#xff0c;0.1就不行&#xff0c;不会有下面的波形

YOLOv9中的“RepNCSPELAN4”结构!

RepNCSPELAN4结构出炉啦&#xff0c;收藏起来写论文用&#xff01; 1.代码&#xff1a; 代码路径&#xff1a;yolov9-main->models->common.py&#xff0c;代码如下&#xff1a; class RepNCSPELAN4(nn.Module):# csp-elandef __init__(self, c1, c2, c3, c4, c51): # …

使用EFCore连接SQLite

简介 在使用EFCore连接SQLite之前我们先来了解一下SQLite SQLite是一个轻量级、自包含、无服务器、零配置的事务性SQL数据库引擎&#xff0c;它支持SQL92标准的大多数查询语言并兼容ACID事务。具体如下&#xff1a; 轻量级&#xff1a;SQLite非常轻巧&#xff0c;它的库体积…

UE5 C++ Gas开发 学习记录(三)

添加AuraPlayerState,AuraAbilitySystemComponentBase和AuraAttributeSet 在Build.cs里添加 // Copyright Epic Games, Inc. All Rights Reserved. using UnrealBuildTool; public class MyGas : ModuleRules { public MyGas(ReadOnlyTargetRules Target) : base(Target) { P…