使用ARKit做一个最简单的入门小案例,点击屏幕添加一个立方体

332 阅读10分钟

刚入门ARKit的朋友可能刚开始有点不知所措,不知道怎么入门,甚至连门都找不到,这里就使用一个最小的Demo来做一个案例展示,我也是慢慢研究一天搞出来的。当检测到平面的时候,会在这个平面上显示一个黑色的立方体,并且当你点击屏幕的时候,就会在对应的位置增加一个立方体。

刚开始使用的时候,可能会找不到平面,所以加了检测到平面的日志输出,当有检测到平面的时候,就会输出log。并且当点击屏幕的时候也会检测是否有平面。

完整代码:

import SwiftUI
import RealityKit
import ARKit

struct ContentView: View {
    var body: some View {
        ARViewContainer().edgesIgnoringSafeArea(.all)
    }
}

extension ARViewContainer.Coordinator: ARSessionDelegate {
    func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
        // 当新的平面被检测到时执行
        for anchor in anchors {
            if let planeAnchor = anchor as? ARPlaneAnchor {
                print("Detected a plane: \(planeAnchor)")
                
                // 可以在这里更新 UI 或做其他反馈
            }
        }
    }
}

struct ARViewContainer: UIViewRepresentable {
    
    func makeUIView(context: Context) -> ARView {
        let arView = ARView(frame: .zero)
        
        // 配置 AR 会话
        let configuration = ARWorldTrackingConfiguration()
        configuration.planeDetection = [.horizontal] // 启用水平面检测
        arView.session.run(configuration)

        arView.session.delegate = context.coordinator
        
        // 创建一个立方体模型
        let mesh = MeshResource.generateBox(size: 0.1, cornerRadius: 0.005)
        let material = SimpleMaterial(color: .gray, roughness: 0.15, isMetallic: true)
        let model = ModelEntity(mesh: mesh, materials: [material])
        
        // 创建一个水平面锚点
        let anchor = AnchorEntity(.plane(.horizontal, classification: .any, minimumBounds: SIMD2<Float>(0.2, 0.2)))
        anchor.children.append(model)
        
        // 将锚点添加到场景中
        arView.scene.anchors.append(anchor)

        // 添加触摸事件识别器
        let tapGestureRecognizer = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleTap(_:)))
        arView.addGestureRecognizer(tapGestureRecognizer)
        
        return arView
    }
    
    func updateUIView(_ uiView: ARView, context: Context) {}
    
    // 创建一个 coordinator 来管理 AR 会话和触摸事件
    func makeCoordinator() -> Coordinator {
        return Coordinator()
    }
    
    class Coordinator: NSObject {
        
        // 处理触摸点击事件
        @objc func handleTap(_ sender: UITapGestureRecognizer) {
            guard let arView = sender.view as? ARView else { return }
            
            // 获取点击位置的 raycast 结果
            let tapLocation = sender.location(in: arView)
            let results = arView.raycast(from: tapLocation, allowing: .estimatedPlane, alignment: .horizontal)
            
            // 调试信息:检查 raycast 是否有结果
            print("Raycast results: \(results)")
            
            // 如果有有效的 raycast 结果
            if let firstResult = results.first {
                // 创建一个新的立方体
                let mesh = MeshResource.generateBox(size: 0.1, cornerRadius: 0.005)
                let material = SimpleMaterial(color: .gray, roughness: 0.15, isMetallic: true)
                let model = ModelEntity(mesh: mesh, materials: [material])
                
                // 将模型放置到触摸的位置
                let modelAnchor = AnchorEntity(world: firstResult.worldTransform)
                modelAnchor.addChild(model)
                
                // 将模型锚点添加到 AR 视图
                arView.scene.anchors.append(modelAnchor)
                
                // 调试信息:输出放置位置
                print("Placed object at: \(firstResult.worldTransform)")
            } else {
                // 如果没有 raycast 结果,打印调试信息
                print("No valid raycast result.")
            }
        }
    }
}

代码解释:

import SwiftUI

  • 作用:导入 SwiftUI 框架,用于构建 iOS、iPadOS、macOS、watchOS 和 tvOS 上的用户界面,允许以声明式的方式创建视图层级结构。

import RealityKit

  • 作用:引入 RealityKit 框架,该框架用于创建增强现实(AR)和虚拟现实(VR)场景,处理 3D 模型、动画以及与虚拟内容的交互等功能。

import ARKit

  • 作用:导入 ARKit 框架,它是苹果用于在 iOS 和 iPadOS 设备上构建增强现实体验的框架,提供了诸如追踪设备运动、识别现实世界平面等功能,为创建 AR 应用提供基础支持。

struct ContentView: View {... }

  • 整体作用:定义了一个遵循 View 协议的结构体 ContentView,代表应用的主视图结构,在 SwiftUI 中用于构建用户界面视图层级。
  • var body: some View {... }:视图结构体中必须实现的计算属性,用于描述视图的内容和布局,这里返回一个 ARViewContainer 视图,并设置忽略安全区域边缘,使得 ARViewContainer 可以占据整个屏幕空间来展示增强现实内容。

extension ARViewContainer.Coordinator: ARSessionDelegate {... }

  • 整体作用:为 ARViewContainer 内部的 Coordinator 类添加扩展,使其遵循 ARSessionDelegate 协议,用于处理与 ARSession(ARKit 中的增强现实会话)相关的事件回调。
  • func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {... }:实现 ARSessionDelegate 协议中的方法,当 ARSession 检测到新的锚点(ARAnchor)被添加时会触发该方法。在方法内部,会遍历新添加的锚点,判断如果是平面锚点(ARPlaneAnchor)类型,就打印出相关信息,并且可以在这里进一步添加更新 UI 或者其他反馈相关的逻辑,用于响应平面检测事件。

struct ARViewContainer: UIViewRepresentable {... }

  • 整体作用:定义了一个遵循 UIViewRepresentable 协议的结构体 ARViewContainer,用于在 SwiftUI 视图层级中嵌入 UIKit 的 ARView(ARKit 中的用于展示增强现实内容的视图),实现 SwiftUI 和 ARKit 的结合使用。
  • func makeUIView(context: Context) -> ARView {... }
    • 整体作用:遵循 UIViewRepresentable 协议要求实现的方法,用于创建并配置要嵌入到 SwiftUI 中的 UIKit 的 ARView
    • let arView = ARView(frame:.zero):创建一个初始帧大小为零的 ARView 实例,后续会根据设备屏幕等情况进行调整布局。
    • let configuration = ARWorldTrackingConfiguration():创建一个 ARWorldTrackingConfiguration 实例,用于配置 AR 会话的追踪模式,它可以实现对设备在现实世界中的位置、方向以及平面等的追踪。
    • configuration.planeDetection = [.horizontal]:设置 AR 会话配置的平面检测模式,这里只启用了对水平面的检测,意味着 ARKit 会尝试识别现实世界中的水平平面,例如桌面、地面等,方便后续在这些平面上放置虚拟物体。
    • arView.session.run(configuration):启动 ARView 的 AR 会话,并传入前面配置好的 ARWorldTrackingConfiguration,使得 AR 功能开始运作,开始追踪现实世界环境等。
    • arView.session.delegate = context.coordinator:将 ARView 的 AR 会话的代理设置为通过 context 获取的 coordinator,这样 coordinator 就能接收并处理 AR 会话相关的各种事件回调了,例如平面检测到等情况。
    • let mesh = MeshResource.generateBox(size: 0.1, cornerRadius: 0.005):使用 RealityKit 中的 MeshResource 创建一个立方体形状的网格资源,指定立方体的大小为边长 0.1(单位应该根据项目整体设置,通常是米之类的长度单位),以及角落的圆角半径为 0.005,用于定义虚拟立方体模型的几何形状。
    • let material = SimpleMaterial(color:.gray, roughness: 0.15, isMetallic: true):创建一个简单材质实例,设置颜色为灰色,粗糙度为 0.15,金属度为 true,用于给前面创建的立方体网格赋予外观材质属性,使其看起来有相应的视觉效果。
    • let model = ModelEntity(mesh: mesh, materials: [material]):通过前面创建的网格资源和材质创建一个 ModelEntity,它代表一个完整的 3D 模型实体,将网格和材质组合起来形成一个可在场景中展示的虚拟立方体模型。
    • let anchor = AnchorEntity(.plane(.horizontal, classification:.any, minimumBounds: SIMD2<Float>(0.2, 0.2))):创建一个锚点实体,将其关联到水平平面上,并且设置了平面的分类为任意(可以是桌子、地面等各种水平平面),以及最小边界尺寸(表示检测到的平面需要达到一定大小才会被认为有效等情况),用于确定虚拟物体放置的参考平面。
    • anchor.children.append(model):将前面创建的立方体模型添加到平面锚点实体的子节点列表中,这样立方体模型就会跟随这个平面锚点在现实世界对应的平面位置上显示。
    • arView.scene.anchors.append(anchor):将包含立方体模型的平面锚点添加到 ARView 的场景的锚点列表中,使得整个虚拟内容能够在 AR 场景里展示出来,基于前面配置的追踪等功能与现实世界对应起来。
    • let tapGestureRecognizer = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleTap(_:))):创建一个 UITapGestureRecognizer(用于识别触摸点击手势),并设置其目标为 context 中的 coordinator,以及对应的触发方法为 Coordinator 类中的 handleTap(_:) 方法,用于后续响应屏幕触摸点击操作。
    • arView.addGestureRecognizer(tapGestureRecognizer):将创建好的触摸点击手势识别器添加到 ARView 上,使得 ARView 能够检测用户的触摸点击动作并做出相应响应。
  • func updateUIView(_ uiView: ARView, context: Context) {}:遵循 UIViewRepresentable 协议要求实现的方法,用于在视图需要更新时进行相关操作,这里暂时没有具体更新逻辑实现,为空方法。
  • func makeCoordinator() -> Coordinator {... }:遵循 UIViewRepresentable 协议要求实现的方法,用于创建并返回一个 Coordinator 实例,这个 Coordinator 类主要用于管理 AR 会话相关事件以及处理触摸事件等,作为 ARView 和相关交互逻辑的协调管理对象。

class Coordinator: NSObject {... }

  • 整体作用:定义了一个继承自 NSObject 的内部类 Coordinator,它作为 ARViewContainer 的协调管理类,用于处理 AR 相关的交互逻辑以及事件回调等。
  • @objc func handleTap(_ sender: UITapGestureRecognizer) {... }
    • 整体作用:一个标记为 @objc(可被 Objective-C 运行时调用)的方法,用于处理前面添加的触摸点击手势识别器触发的点击事件。
    • guard let arView = sender.view as? ARView else { return }:从手势发送者(即触发触摸点击的视图)中尝试获取对应的 ARView,如果无法转换则直接返回,确保后续操作是在有效的 ARView 上进行。
    • let tapLocation = sender.location(in: arView):获取触摸点击在 ARView 中的位置坐标,用于后续基于点击位置进行相关操作,比如判断点击位置对应的现实世界位置等。
    • let results = arView.raycast(from: tapLocation, allowing:.estimatedPlane, alignment:.horizontal):在 ARView 上从点击位置发起射线投射(raycast)操作,允许投射到预估的平面上(基于 ARKit 对平面的追踪和预估),并且指定投射方向为水平方向,获取射线投射的结果列表,用于判断点击位置对应的现实世界平面等信息。
    • print("Raycast results: \(results)"):打印射线投射的结果信息,用于调试查看是否获取到了有效的结果以及对应的详细内容等情况。
    • if let firstResult = results.first {... }:判断射线投射结果列表中是否有第一个有效的结果,如果有则执行后续在点击位置放置虚拟物体的相关逻辑。
    • 在 if 分支内:
      • let mesh = MeshResource.generateBox(size: 0.1, cornerRadius: 0.005):再次创建一个和之前类似的立方体网格资源,用于创建新的要放置的虚拟立方体模型。
      • let material = SimpleMaterial(color:.gray, roughness: 0.15, isMetallic: true):同样创建一个相应的材质实例用于新模型。
      • let model = ModelEntity(mesh: mesh, materials: [material]):基于新的网格和材质创建一个新的 ModelEntity,即新的立方体模型实体。
      • let modelAnchor = AnchorEntity(world: firstResult.worldTransform):创建一个新的锚点实体,其位置基于射线投射得到的第一个有效结果对应的世界变换矩阵(表示在现实世界中的位置和方向等信息),用于准确地将新模型放置到点击对应的现实世界位置上。
      • modelAnchor.addChild(model):将新创建的立方体模型添加到新的锚点实体的子节点中,使其与锚点关联起来,能在对应位置展示。
      • arView.scene.anchors.append(modelAnchor):将包含新模型的锚点添加到 ARView 的场景锚点列表中,使得新模型能够在 AR 场景里基于点击位置显示出来。
      • print("Placed object at: \(firstResult.worldTransform)"):打印出放置物体的位置信息(通过世界变换矩阵表示),用于调试查看放置的具体位置情况。
    • 在 else 分支内:
      • print("No valid raycast result."):如果没有有效的射线投射结果,打印相应提示信息,用于调试查看为什么没有成功放置物体等情况。