IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> SwiftUI + RealityKit 实现简单AR测距 -> 正文阅读

[移动开发]SwiftUI + RealityKit 实现简单AR测距

Xcode 12.5.1 SwiftUI ARKit RealityKit MacOS 11.5.1

本demo只是为了学习realitykit的使用,实际如果制作AR测距,推荐使用SceneKit来描绘点和线与文字。

效果如图

SwiftUI+RealityKit实现简单AR测距

首先在Xcode新建一个工程,选择Augmented Reality App.建好之后,会有默认的一个3d模型文件和一段默认的ARViewContainer的代码,运行后,当屏幕中心点找到水平面后,会显示一个立方体模型。

我们可以不用这个模型,用我们自己的代码替代

struct ARViewContainer: UIViewRepresentable {

    @EnvironmentObject var vm: ViewModel
    
    func makeUIView(context: Context) -> some UIView {
        return vm.arView
    }

    func updateUIView(_ uiView: UIViewType, context: Context) {
    }
}

还是用熟悉的MVVM模式来设计我们的app,新建一个swift文件,起名为ViewModel.swift

import SwiftUI
import RealityKit
import ARKit

class ViewModel: ObservableObject {
    
  //  static let shared = ViewModel()
    @Published var arView: ARView!
    @Published var position: CGPoint = .zero
    
    // MARK: - 记录屏幕上设置的点的位置
    @Published var simd_position: [simd_float4x4] = []
    // MARK: - 测量的距离
    @Published var distance: Float = 0.0
    
    // MARK: - 移动屏幕时定时发送过来的点,在有新的点时,旧的点要remove(anchor)
    var moveAnchorEntity: AnchorEntity?
    // MARK: - 移动屏幕时画的线,在有新的点时,旧的线要remove(anchor)
    var lineAnchorEntity: AnchorEntity?
    
    var sphere: ModelEntity?
    
    init() {
        arView = ARView(frame: .zero)
        let configuration = ARWorldTrackingConfiguration()
        configuration.planeDetection = [.horizontal, .vertical]//.horizontal
        arView.session.run(configuration)
    }
}

arView是ARKit的主视图,在初始化时初始化为可跟踪镜头的配置模式

测距我采用的方式是,第一个点使用手机屏幕的中心点,设置完第一个点后,启动定时器,实时将屏幕中心点传递给ViewModel,让ARKit计算实际物理世界中的点,并显示在arView中,同时画出两个点的连线。

苹果自带的测距仪App我不知道是不是用这种原理实现,因为我也是初次接触ARKit,所以只能用这种笨方法。

以下是View视图ContentView的源码

import SwiftUI
import RealityKit
import Combine

struct ContentView : View {
    
    @StateObject var vm: ViewModel = ViewModel()
    
    @State var timer = Timer.publish(every: 0.1, on: .main, in: .common)
    @State var cancellable: Cancellable? = nil
    
    var body: some View {
        ZStack {
            ARViewContainer()
                .edgesIgnoringSafeArea(.all)
                .environmentObject(vm)
            
            VStack {
                
                HStack {
                    Spacer()
                    ClearButton.padding()
                }
                Spacer()
                Text("测距: \(vm.distance) 米")
                    .foregroundColor(.white)
                    .font(.title).bold()
              
                AddButton
            }
        }
        .onReceive(timer, perform: timerHandler())
    }
}

extension ContentView {
    
    // MARK: - 清除按钮
    var ClearButton: some View {
        Button(action: {
            cancellable?.cancel()
            cancellable = nil
            vm.clearScreen()
        }, label: {
            Image(systemName: "trash.circle")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: 50, height: 50, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
                .foregroundColor(.white)
                
        })
    }
    
    // MARK: - 加号按钮
    var AddButton: some View {
        Button(action: {
            
            if cancellable == nil {
                // 第一次: 设置起始点,开始计时器
                vm.addStartNode(CGPoint(x: UIScreen.main.bounds.midX, y: UIScreen.main.bounds.midY))
                timer = Timer.publish(every: 0.1, on: .main, in: .common)
                cancellable = timer.connect()
            } else {
                // 第二次: 设置结束点,停止计时器
                cancellable?.cancel()
                cancellable = nil
            }

        }, label: {
            Image(systemName: "plus.circle.fill")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: 40, height: 40)
                .foregroundColor(.white)
        })
    }
    
    // MARK: - 计时器处理方法
    func timerHandler() -> (Timer.TimerPublisher.Output) -> Void {
        return { _ in
            vm.moveNode(CGPoint(x: UIScreen.main.bounds.midX, y: UIScreen.main.bounds.midY))
        }
    }
}

struct ARViewContainer: UIViewRepresentable {

    @EnvironmentObject var vm: ViewModel
    
    func makeUIView(context: Context) -> some UIView {
        return vm.arView
    }

    func updateUIView(_ uiView: UIViewType, context: Context) {
    }
}


#if DEBUG
struct ContentView_Previews : PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
#endif

以下是ViewModel的源码:

import Foundation
import SwiftUI
import RealityKit
import ARKit
import TextEntity

class ViewModel: ObservableObject {
    
  //  static let shared = ViewModel()
    @Published var arView: ARView!
    @Published var position: CGPoint = .zero
    
    // MARK: - 记录屏幕上设置的点的位置
    @Published var simd_position: [simd_float4x4] = []
    // MARK: - 测量的距离
    @Published var distance: Float = 0.0
    
    // MARK: - 移动屏幕时定时发送过来的点,在有新的点时,旧的点要remove(anchor)
    var moveAnchorEntity: AnchorEntity?
    // MARK: - 移动屏幕时画的线,在有新的点时,旧的线要remove(anchor)
    var lineAnchorEntity: AnchorEntity?
    
    var sphere: ModelEntity?
    
    init() {
        arView = ARView(frame: .zero)
        let configuration = ARWorldTrackingConfiguration()
        configuration.planeDetection = [.horizontal, .vertical]//.horizontal
        arView.session.run(configuration)
    }
}

// MARK: - Public Extension
extension ViewModel {
    
    /// 放置初始点
    /// - 放置后开始追踪屏幕中心点,并连线
    /// - Parameters:
    ///   - p: CGPoint 屏幕坐标
    /// - Returns: 无
    public func addStartNode(_ p: CGPoint) {
        
        // MARK: - 设置初始点前先清空可能存在的上一对连接点和线
        clearScreen()
        
        // MARK: - 转换屏幕坐标为物理世界坐标
        let query = arView.makeRaycastQuery(from: p, allowing: .existingPlaneInfinite, alignment: .any)
        if let firstResult = query {
            let results = arView.session.raycast(firstResult)
            if let position = results.first?.worldTransform {
                drawNode(position)
                // MARK: - 存储到simd_position
                simd_position.append(position)
            }
        }
    }
    
    /// 放置结束点
    /// - 在初始点放置后,可以放置结束点,放置后显示两点连线
    /// - Parameters:
    ///   - p: CGPoint 屏幕坐标
    /// - Returns: 无
    public func addEndNode(_ p: CGPoint) {
        
        // MARK: - 转换屏幕坐标为物理世界坐标
        let query = arView.makeRaycastQuery(from: p, allowing: .existingPlaneInfinite, alignment: .any)
        if let firstResult = query {
            let results = arView.session.raycast(firstResult)
            if let position = results.first?.worldTransform {
                drawNode(position)
                // MARK: - 存储到simd_position
                simd_position.append(position)
            }
        }
    }
    
    /// 移动结束点
    /// - 初始点设置完后,如果屏幕中心点变化,在物理世界中追踪这个变化
    /// - Parameters:
    ///   - p: simd_float4x4 物理世界坐标
    /// - Returns: 无
    public func moveNode(_ p: CGPoint) {
        let query = arView.makeRaycastQuery(from: p, allowing: .existingPlaneInfinite, alignment: .any)
        if let firstResult = query {
   
            let results = arView.session.raycast(firstResult)
            
            if let position = results.first?.worldTransform {
           
                let mesh = MeshResource.generatePlane(width: 0.005, depth: 0.005, cornerRadius: 1)
                let material = SimpleMaterial(color: .orange, isMetallic: false)
                
                let modelEntity = ModelEntity(mesh: mesh, materials: [material])
                
                if moveAnchorEntity != nil {
                    arView.scene.removeAnchor(moveAnchorEntity!)
                }
                moveAnchorEntity = AnchorEntity(world: position)
                moveAnchorEntity!.addChild(modelEntity)
                
                arView.scene.addAnchor(moveAnchorEntity!)
             
                self.drawMoveLine(simd_position[0], position)
            }
        }
    }
    
    /// 清除已有的点和线
    /// - 清空对象:simd_position数组 arView.scene.anchors序列
    /// - Returns: 无
    public func clearScreen() {
        simd_position.removeAll()
        arView.scene.anchors.removeAll()
    }
}

// MARK: - Private Extension
extension ViewModel {
    
    /// 在物理世界画一个点
    /// - 用generatePlane + SimpleMaterial方式只能设置为灰色 ???
    /// - Parameters:
    ///   - p: simd_float4x4 物理世界坐标
    /// - Returns: 无
    private func drawNode(_ p: simd_float4x4) {
        let mesh = MeshResource.generatePlane(width: 0.005, depth: 0.005, cornerRadius: 1)
        let material = SimpleMaterial(color: .orange, isMetallic: true)
        
        let modelEntity = ModelEntity(mesh: mesh, materials: [material])
        
        let anchorEntity = AnchorEntity(world: p)
        anchorEntity.addChild(modelEntity)
        
        arView.scene.addAnchor(anchorEntity)
    }
    
  
    /// 根据给定的两点画线
    /// ```
    /// - 屏幕只保留一条线,之前画的线在新画前要清除
    /// -- startPoint: simd_float4x4 起始点
    /// -- endPoint:   simd_float4x4 结束点
    private func drawMoveLine(_ startPoint: simd_float4x4, _ endPoint: simd_float4x4) {
        
        let s = startPoint
        let e = endPoint
        let start = startPoint.columns.3
        let end = endPoint.columns.3
        
        /// 判断当前的anchor集合中是否包含lineAnchorEntity
        /// 如果包含则remove
        let _ = arView.scene.anchors.contains { anchor in
            if anchor == lineAnchorEntity {
                arView.scene.removeAnchor(anchor)
                return true
            }
            return false
        }
        
        // MARK: box的深度,深度为两个点的距离,依次来形成线
        let meters = simd_distance(start, end)
        
        // MARK: 连线,用box形式显示
        let rectangle = ModelEntity(mesh: .generateBox(width: 0.0008, height: 0.0008, depth: meters), materials: [SimpleMaterial(color: UIColor(.orange), isMetallic: false)])
            
        // MARK: 两点的中心点
        let middlePoint : simd_float3 = simd_float3((start.x + end.x)/2, (start.y + end.y)/2, (start.z + end.z)/2)
                
        lineAnchorEntity = AnchorEntity()
        lineAnchorEntity!.position = middlePoint
        
        let startPoint = SIMD3(start.x, start.y, start.z)
        // MARK: Positions and orients the entity to look at a target from a given position.
        //       定位和定向实体以从给定位置看目标。 ( ??? )
        lineAnchorEntity!.look(at: startPoint, from: middlePoint, relativeTo: nil)
        lineAnchorEntity!.addChild(rectangle)
        arView.scene.addAnchor(lineAnchorEntity!)
        
        calcuteMoveDistance(s, e)
    }
    
    private func addLineNode(start: simd_float4, end: simd_float4) {
        // MARK: box的深度,深度为两个点的距离,依次来形成线
        let meters = simd_distance(start, end)
        
        // MARK: 连线,用box形式显示
        let rectangle = ModelEntity(mesh: .generateBox(width: 0.0008, height: 0.0008, depth: meters), materials: [SimpleMaterial(color: UIColor(.orange), isMetallic: false)])
            
        // MARK: 两点的中心点
        let middlePoint : simd_float3 = simd_float3((start.x + end.x)/2, (start.y + end.y)/2, (start.z + end.z)/2)
                
        lineAnchorEntity = AnchorEntity()
        lineAnchorEntity!.position = middlePoint
        
        let startPoint = SIMD3(start.x, start.y, start.z)
        // MARK: Positions and orients the entity to look at a target from a given position.
        //       定位和定向实体以从给定位置看目标。 ( ??? )
        lineAnchorEntity!.look(at: startPoint, from: middlePoint, relativeTo: nil)
        lineAnchorEntity!.addChild(rectangle)
        arView.scene.addAnchor(lineAnchorEntity!)
    }
    
    /// 计算物理世界两点距离
    /// ```
    /// - 无
    /// -- startPoint: simd_float4x4 起始点
    /// -- endPoint:   simd_float4x4 结束点
    func calcuteMoveDistance(_ startPoint: simd_float4x4, _ endPoint: simd_float4x4) {
        
        let start = startPoint
        let end = endPoint
        distance = sqrt(
            pow(end.columns.3.x - start.columns.3.x, 2) +
                pow(end.columns.3.y - start.columns.3.y, 2) +
                pow(end.columns.3.z - start.columns.3.z, 2)
            )
    }
  
    /// 根据给定的两点画线
    /// ```
    /// - startPoint: simd_float4x4 起始点
    /// - endPoint:   simd_float4x4 结束点
    private func drawLine(_ startPoint: simd_float4x4, _ endPoint: simd_float4x4) {
        
        let start = startPoint.columns.3
        let end = endPoint.columns.3
        
        // MARK: box的深度,深度为两个点的距离,依次来形成线
        let meters = simd_distance(start, end)
        
        // MARK: 连线,用box形式显示
        let rectangle = ModelEntity(mesh: .generateBox(width: 0.003, height: 0.003, depth: meters), materials: [SimpleMaterial(color: UIColor(.blue), isMetallic: false)])
            
        // MARK: 两点的中心点
        let middlePoint : simd_float3 = simd_float3((start.x + end.x)/2, (start.y + end.y)/2, (start.z + end.z)/2)
                
        
        let lineAnchor = AnchorEntity()
        lineAnchor.position = middlePoint
      
        let startPoint = SIMD3(start.x, start.y, start.z)
        // MARK: Positions and orients the entity to look at a target from a given position.
        //       定位和定向实体以从给定位置看目标。 ( ??? )
        lineAnchor.look(at: startPoint, from: middlePoint, relativeTo: nil)
        lineAnchor.addChild(rectangle)
        arView.scene.addAnchor(lineAnchor)
    }
    
  
}

需要注意的一点是,我们需要增加一个Launch Screen.storyboard,否则AR无法全屏显示。

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-09-12 13:16:58  更:2021-09-12 13:17:58 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 17:03:23-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码