【iOS ARKit】网络传输 ARWorldMap

    ARKit 可以利用 ARWorldMap 在应用中断后进行状态恢复、继续AR 进程。一个用户也可以将ARWorldMap 发送给其他用户,当其他用户接收并加载 ARWorldMap 后,就可以在相同的物理环境看到同样的虚拟元素,达到共享 AR体验的目的。

     在ARKit 中,ARWorldMap 可以保存的 ARSession状态包括对物理环境的感知(特征点信息)、地标信息、用户添加到场景中的ARAnchor,但不包括虚拟元素本身。ARWorldMap 并不会存储虚拟元素,理解这点对我们还原场景非常重要,由于虚拟元素都依赖于 ARAnchor,因此 ARAnchor 就成为最重要的桥梁,有通过ARAnchor,我们才能再度恢复场景。

      除了可以将 ARWorldMap 存储到本地文件系统中供本机应用稍后加载继续 AR体验,也可以通过网络将 ARWorldMap 传输到其他移动设备上供其他设备共享 AR 体验。我们通过网络传输使用ARWorldMap,网络传输采用 Multipeer Connectivity 通信框架,Multipeer Connectivity 点对点通信框架特别适合物理距离很近的设备通过 WiFi、蓝牙直连。

     为便于代码的理解,这里对 Multipeer Connectivity 框架进行必要简述,更详细的信息需读者查阅相关资料。Multipeer Connectivity 框架是点对点通信框架,即任何一方既可以作为主机也可以作为客户机参与通信,进行通信时,该框架使用 MCNearbyServiceAdvertiser 向外广播自身服务,使用 MCNearbyServiceBrowser搜索发现(Discovering)可用的服务。

     根据通信进程,该框架的使用可分成两个阶段:发现阶段与会话通信阶段。假设有两台设备A和B,A先作为主机广播自身服务,B作为客户机搜索可用服务,一旦B发现了A就尝试与其建立连接,在经过A同意后二者建立连接。当连接建立后即可进行数据通信,进入会话通信阶段。

      在应用程序转到后台时,Multipeer Connectivity 框架会暂停广播与搜索发现并断开已连接的会话,在回到前台后,该框架会自动恢复广播与发现,但会话还需要重新建立连接。利用网络传输 ARWorldMap的代码如下所示。

//
//  ARWorldMapShare.swift
//  ARKitDeamo
//
//  Created by zhaoquan du on 2024/2/22.
//

import SwiftUI
import RealityKit
import ARKit
import Combine
import MultipeerConnectivity

struct ARWorldMapShare: View {
    var viewModel: ViewModel = ViewModel()
    
    var body: some View {
        ARWorldMapShareContainer(viewModel: viewModel)
            .overlay(
                VStack{
                    Spacer()
                    
                    Button(action: {viewModel.saveWorldMap()}) {
                        Text("发送AR环境信息")
                            .frame(width:250,height:50)
                            .font(.system(size: 17))
                            .foregroundColor(.black)
                            .background(Color.white)
                        
                            .opacity(0.6)
                    }
                    .cornerRadius(10)
                    Spacer().frame(height: 40)
                }
            ).edgesIgnoringSafeArea(.all).navigationTitle("共享ARWorldMap")
    }
  

    
    
    
    
    class ViewModel: NSObject,ARSessionDelegate{
        var arView: ARView? = nil
        var multipeerSession: MultipeerSession? = nil
        
      
        var planeEntity : ModelEntity? = nil
        var raycastResult : ARRaycastResult?
        var isPlaced = false
        var robotAnchor: AnchorEntity?
        let robotAnchorName = "drummerRobot"
        
        
        func createPlane()  {
            if multipeerSession == nil {
                multipeerSession = MultipeerSession(receivedDataHandler: reciveData(_:from:), peerJoinedHandler: peerJoined(_:), peerLeftHandler: peerLeft(_:), peerDiscoveredHandler: peerDiscovery(_:))
            }
            guard let arView = arView else {
                return
            }
          
            if let an = arView.scene.anchors.first(where: { an in
                an.name == "setModelPlane"
            }){
                arView.scene.anchors.remove(an)
            }
            
            let planeMesh = MeshResource.generatePlane(width: 0.15, depth: 0.15)
            var planeMaterial = SimpleMaterial(color:.white,isMetallic: false)
            planeEntity = ModelEntity(mesh: planeMesh, materials: [planeMaterial])
            let planeAnchor = AnchorEntity(plane: .horizontal)
            
            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.addChild(planeEntity!)
                planeAnchor.name = "setModelPlane"
                
                arView.scene.addAnchor(planeAnchor)
            } catch let error {
                print("加载文件失败:\(error)")
            }
        }
        func saveWorldMap() {
            print("save:\(String(describing: arView))")
            
            self.arView?.session.getCurrentWorldMap(completionHandler: {[weak self] loadWorld, error in
                guard let worldMap = loadWorld else {
                    print("当前无法获取ARWorldMap:\(error!.localizedDescription)")
                    return
                }
               
                do {
                    let data = try NSKeyedArchiver.archivedData(withRootObject: worldMap, requiringSecureCoding: true)
                    self?.multipeerSession?.sendToAllPeers(data, reliably: true)
                    print("ARWorldMap已发送")
                } catch {
                    fatalError("无法序列化ARWorldMap: \(error.localizedDescription)")
                }
            })
        }
        
        func reciveData(_ data: Data,from peer: MCPeerID) {
            
            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
            }
            print("收到ARWorldMap")
            let config = ARWorldTrackingConfiguration()
            config.planeDetection = .horizontal
            config.initialWorldMap = worldMap
            
            self.arView?.session.run(config,options: [.resetTracking, .removeExistingAnchors])
                 
        }
        
        func peerDiscovery(_ peer: MCPeerID) -> Bool{
            guard let multipeerSession = multipeerSession else {
                return false
            }
            if multipeerSession.connectedPeers.count > 3{
                return false
            }
            return true
        }
        func peerJoined(_ peer: MCPeerID) {
        }
        func peerLeft(_ peer: MCPeerID) {
        }
        
        func setupGesture(){
            let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
            self.arView?.addGestureRecognizer(tap)
        }
        @objc func handleTap(sender: UITapGestureRecognizer){
            sender.isEnabled = false
            sender.removeTarget(nil, action: nil)
            isPlaced = true
            let 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文件")
            }
            
           
            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 = result
            planeEntity?.setTransformMatrix(result.worldTransform, relativeTo: nil)
        }
        
        func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
            guard !anchors.isEmpty,robotAnchor == nil else {
               
                return
            }
            var panchor: ARAnchor? = nil
            for anchor in anchors {
                if anchor.name == robotAnchorName {
                    panchor = anchor
                    break
                }
            }
            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 = true
            planeEntity?.removeFromParent()
            planeEntity = nil
            print("加载模型成功")
        }
        
    }
}
struct ARWorldMapShareContainer: UIViewRepresentable {
    var viewModel: ARWorldMapShare.ViewModel
    func makeUIView(context: Context) -> some ARView {
        let arView = ARView(frame: .zero)
        return arView
    }
    
    func updateUIView(_ uiView: UIViewType, context: Context) {
        let config = ARWorldTrackingConfiguration()
        config.planeDetection = .horizontal
        uiView.session.run(config)
        uiView.session.delegate = viewModel
        viewModel.arView = uiView
        viewModel.createPlane()
        viewModel.setupGesture()
    }
    
}
#Preview {
    ARWorldMapShare()
}

     在使用 Multipeer Connectivity 框架时,每个服务都需要一个类型标识符(serviceType),该标识符用于在多服务主机的情况下区分不同主机,这个标识符由 ASCII 字符、数字和“”组成,最多15个字符,且至少包含一个ASCII 字符,不得以“”开头或结尾,也不得两个“”连用。在进行通信前可以将 sessionType设置为 host(主机)、peer(客户机)、both(既可以是主机也可以是客户机)三者之一,用于设置设备的通信类型。

      同时在两台设备A和B上运行本案例(确保两台设备连接到同一个WiFi 网络或者都打开蓝牙),在A设备检测到的平面上添加机器人模型后单击“发送地图”按钮,在A、B 连接顺畅的情况下可以看到B设备的ARSession 会重启,当环境匹配成功后虚拟机器人模型会出现在B设备中,并且其所在物理世界中的位置与A设备中的一致,如图所示。

     

      直接使用 ARAnchor 名字进行虚拟元素关联的方式不会附带更多的自定义信息,在某些场景下,可能需要更多的场景状态数据,这时我们可以通过继承 ARAnchor,自定义 ARAnchor 子类来实现这一目标,通过自定义的 ARAnchor 于类可以携带更多关于应用运行时的状态信息。

      在 ARKit 中,每生成一个 ARFrame 对象就会更新一次所有与当前 ARSession 关联的ARAnchor,并且ARAnchor 对象是不可变的,这意味着,ARKit 会将所有的ARAnchor 从一个 ARFrame 对象复制到另一个ARFrame 对象。当创建继承自 ARAnchor 的子类时,为确保这些子类在保存与加载 ARWorldMap 时正常工作,子类创建应当遵循以下原则:

(1)子类应当完全遵循ARAnchorCopying 协议,必须提供 init(anchor:)方法,ARKit 需要调用 init(anchor:)方法将其从一个 ARFrame 复制到另一个 ARFrame。同时,在该构造方法中,需要确保自定义的变量值被正确地复制。

(2) 子类应当完全遵循 NSSecureCoding 协议,重写 encode(with:)和 init(coder:)方法,确保自定义变量能正确地被序列化和反序列化。

(3) 子类判等的条件是其identifier 值相等。

(4) 只有那些不遵循 ARTrackable 协议的ARAnchor 才能被保存进ARWorldMap,即类似 ARFaceAnchor、ARBodyAnchor 这类反映实时变化的 ARAnchor 不会通过 ARWorldMap 共享。当使用 getCurrent WorldMap(completion Handler:)方法创建 AR WorldMap 时,所有的非可跟踪(Trackable)的ARAnchor 都将自动被保存。

      除此之外,使用 getCurrentWorldMap(completion Handler:)方法获取的当前场景 ARSession 运行状态数据的可用性与当前 ARFrame 的状态有关,ARFrame.WorldMappingStatus  为 mapped 时获取的 ARWorldMap 数据最可信,反之则可能不准确,从而影响场景恢复,所以在获取 ARWorldMap 时最好选择 ARFrame状态 ARFrame. WorldMappingStatus 为 mapped 时进行。

具体代码地址:GitHub - duzhaoquan/ARkitDemo

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【c语言】内存函数

欢迎关注个人主页:逸狼 创造不易,可以点点赞吗~ 如有错误,欢迎指出~ 目录 memcpy函数的使用和模拟实现 memcpy函数的使用 memcpy函数的模拟实现 memmove的使用和模拟实现 memmove的使用 memmove的模拟实现 memset函数的使用 memcmp函数…

java中容器继承体系

首先上图 源码解析 打开Collection接口源码&#xff0c;能够看到Collection接口是继承了Iterable接口。 public interface Collection<E> extends Iterable<E> { /** * ...... */ } 以下是Iterable接口源码及注释 /** * Implementing this inte…

【UE 材质】水晶材质

效果 步骤 1. 先在Quixel Bridge上下载冰纹理 2. 新建一个材质&#xff0c;这里命名为“M_Ice”并打开&#xff0c;添加如下纹理采样节点 继续添加如下节点 此时效果如下&#xff1a; 可以看到此时的材质颜色比较浅&#xff0c;如果希望颜色深一点可以继续添加如下节点 此时效…

Java学习--学生管理系统(残破版)

代码 Main.java import java.util.ArrayList; import java.util.Scanner;public class Main {public static void main(String[] args) {ArrayList<Student> list new ArrayList<>();loop:while (true) {System.out.println("-----欢迎来到阿宝院校学生管理系…

OSI模型

OSI模型 TCP/IP参考模型 TCP/IP常见协议 应用层 FTP&#xff08;用于文件的下载和上传&#xff0c;采用C/S结构&#xff09; Telnet&#xff08;用于远程登陆服务&#xff09; DNS&#xff08;域名解析&#xff09; HTTP&#xff08;接收和发布Html页面&#xff09; 传输层…

React Switch用法及手写Switch实现

问&#xff1a;如果注册的路由特别多&#xff0c;找到一个匹配项以后还会一直往下找&#xff0c;我们想让react找到一个匹配项以后不再继续了&#xff0c;怎么处理&#xff1f;答&#xff1a;<Switch>独特之处在于它只绘制子元素中第一个匹配的路由元素。 如果没有<Sw…

Vue项目构建优化

本文作者为 360 奇舞团前端开发工程师 宁航 在开发大型前端项目时&#xff0c;往往是一个需求对应一个分支&#xff0c;当完成需求后&#xff0c;就需要将代码打包、部署。代码通常需要部署到多个环境中&#xff0c;这些环境包括&#xff1a;日常环境、测试环境、回归环境和生产…

Linux Centos7配置SSH免密登录

Linux Centos7配置SSH免密登录 配置SSH免密登录说明&#xff1a; 分两步 第一步、给Server A生成密钥对 第二步、给Server B授权 生成密钥对之后&#xff0c;我们可以看看它保存的目录下的文件。 接下来我们就要把Server A&#xff08;10.1.1.74&#xff09;的公钥拷贝到Se…

Linux运维-Web服务器的配置与管理(Apache+tomcat)(没成功,最后有失败经验)

Web服务器的配置与管理(Apachetomcat) 项目场景 公司业务经过长期发展&#xff0c;有了很大突破&#xff0c;已经实现盈利&#xff0c;现公司要求加强技术架构应用功能和安全性以及开始向企业应用、移动APP等领域延伸&#xff0c;此时原来开发web服务的php语言已经不适应新的…

Rust使用calamine读取excel文件,Rust使用rust_xlsxwriter写入excel文件

Rust使用calamine读取已存在的test.xlsx文件全部数据&#xff0c;还读取指定单元格数据&#xff1b;Rust使用rust_xlsxwriter创建新的output.xlsx文件&#xff0c;并写入数据到指定单元格&#xff0c;然后再保存工作簿。 Cargo.toml main.rs /*rust读取excel文件*/ use cala…

13.云原生之常用研发中间件部署

云原生专栏大纲 文章目录 mysql主从集群部署mysql高可用集群高可用互为主从架构互为主从架构如何实现主主复制中若是两台master上同时出现写操作可能会出现的问题该架构是否存在问题&#xff1f; heml部署mysql高可用集群 nacos集群部署官网文档部署nacoshelm部署nacos redis集…

像用Excel一样用Python:pandasGUI

文章目录 启动数据导入绘图 启动 众所周知&#xff0c;pandas是Python中著名的数据挖掘模块&#xff0c;以处理表格数据著称&#xff0c;并且具备一定的可视化能力。而pandasGUI则为pandas打造了一个友好的交互窗口&#xff0c;有了这个&#xff0c;就可以像使用Excel一样使用…

EtherCAT主站转Ethernet/IP网关

产品功能 1 YC-ECTM-EIP工业级Profinet 网关 2 EtherCAT转 EtherNet/IP 3 支持EtherNet/IP从站 4 即插即用 无需编程 轻松组态 ,即实现数据交互 5 导轨安装 支持提供EDS文件 6 EtherNET/IP与EtherCAT互转数据透明传输可接入PLC组态 支持CodeSys/欧姆龙PLC&#xff0c;西门…

Python 实现Excel自动化办公(上)

在Python 中你要针对某个对象进行操作&#xff0c;是需要安装与其对应的第三方库的&#xff0c;这里对于Excel 也不例外&#xff0c;它也有对应的第三方库&#xff0c;即xlrd 库。 什么是xlrd库 Python 操作Excel 主要用到xlrd和xlwt这两个库&#xff0c;即xlrd是读Excel &am…

《opencv实用探索·二十二》支持向量机SVM用法

1、概述 在了解支持向量机SVM用法之前先了解一些概念&#xff1a; &#xff08;1&#xff09;线性可分和线性不可分 如果在一个二维空间有一堆样本&#xff0c;如下图所示&#xff0c;如果能找到一条线把这两类样本分开至线的两侧&#xff0c;那么这个样本集就是线性可分&#…

Day03:Web架构OSS存储负载均衡CDN加速反向代理WAF防护

目录 WAF CDN OSS 反向代理 负载均衡 思维导图 章节知识点&#xff1a; 应用架构&#xff1a;Web/APP/云应用/三方服务/负载均衡等 安全产品&#xff1a;CDN/WAF/IDS/IPS/蜜罐/防火墙/杀毒等 渗透命令&#xff1a;文件上传下载/端口服务/Shell反弹等 抓包技术&#xff1a…

腾讯云4核8G服务器收费贵不贵?

腾讯云4核8G服务器多少钱&#xff1f;轻量应用服务器4核8G12M带宽一年446元、646元15个月&#xff0c;云服务器CVM标准型S5实例4核8G配置价格15个月1437.3元&#xff0c;5年6490.44元&#xff0c;标准型SA2服务器1444.8元一年&#xff0c;在txy.wiki可以查询详细配置和精准报价…

ky10-server docker 离线安装包、离线安装

离线安装脚本 # ---------------离线安装docker------------------- rpm -Uvh --force --nodeps *.rpm# 修改docker拉取源为国内 rm -rf /etc/docker mkdir -p /etc/docker touch /etc/docker/daemon.json cat >/etc/docker/daemon.json<<EOF{"registry-mirro…

蓝桥杯刷题2

1. 修建灌木 import java.util.Scanner;public class Main {public static void main(String[] args) {Scanner scan new Scanner(System.in);int n scan.nextInt();for (int i 1;i < n1;i){int distance Math.max(i-1,n-i);System.out.println(distance*2);}scan.close…

Android13 Audio框架

一、Android 13音频代码结构 1、framework: android/frameworks/base 1.AudioManager.java &#xff1a;音频管理器&#xff0c;音量调节、音量UI、设置和获取参数等控制流的对外API 2.AudioService.java &#xff1a;音频系统服务&#xff08;java层&#xff09;&#xff0c…