返回

AR模型零件点击显示名称:RealityKit碰撞检测方案

IOS

模型零件点击显示名称

针对AR应用开发中,点击三维模型的特定零件显示其名称的问题,核心挑战是如何精确地识别用户点击的模型部位,并呈现相应的提示信息。下面将探讨几种可行的方案及实现步骤。

基于碰撞检测和实体名称

最直观的方式是利用RealityKit的碰撞检测功能,并结合模型实体的命名,完成交互。每个子零件在3D建模阶段,都应该被赋予清晰、有意义的名称。这些名称会在导入 RealityKit 时被保留下来。通过捕获用户点击,遍历射线击中的实体,可以获取被点击部分的模型实体。

原理: 当用户点击屏幕时,会从摄像机视角向屏幕点击位置发出一条“射线”。此射线与场景中的模型相交时,就会产生碰撞。 通过遍历所有碰撞实体,找到被点击的对象,再通过检查该对象的 name 属性即可获取相应零件名称。

实现步骤:

  1. 给模型零件命名: 确保USDZ模型中的每个零件都具有有意义的名称,方便在代码中识别。建模时就应当妥善完成此项操作。
  2. 捕获屏幕点击事件: 创建一个手势识别器(如UITapGestureRecognizer)添加到 ARView 中。
  3. 射线投射与碰撞检测: 在手势事件回调中,使用 arView.ray(through:) 方法, 从点击位置向场景投射射线,然后利用arView.scene.raycast(from:to:)获取碰撞结果。
  4. 解析碰撞实体: 从碰撞结果中提取出 Entity 对象(例如ModelEntity) , 并读取其 name属性,得到零件名称。
  5. 显示零件名称: 使用UILabel等UI元素在屏幕上显示该名称。可以考虑用一个临时的提示框或者浮窗显示信息。

代码示例:

import ARKit
import RealityKit
import UIKit

class ARViewController: UIViewController {
    
    let arView = ARView(frame: .zero)
    var popupLabel: UILabel!
    var modelEntity: ModelEntity?

    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(arView)
         arView.translatesAutoresizingMaskIntoConstraints = false
           NSLayoutConstraint.activate([
              arView.topAnchor.constraint(equalTo: view.topAnchor),
              arView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
              arView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
              arView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
          ])
        // 创建模型并放置
         setupModel()

        // 初始化显示标签
        setupPopupLabel()

        // 配置手势识别器
         setupTapGesture()

         // 配置AR场景
        let configuration = ARWorldTrackingConfiguration()
        configuration.planeDetection = .horizontal
        arView.session.run(configuration)


    }

     func setupModel() {
          //创建模型
         modelEntity = try! ModelEntity.loadModel(named: "tsmile1.usdz")

           // 设置碰撞形状
         modelEntity?.generateCollisionShapes(recursive: true)
        let planeAnchor = AnchorEntity(world: SIMD3(0, 0, 0))
            planeAnchor.addChild(modelEntity!)
            arView.scene.addAnchor(planeAnchor)

     }
     func setupPopupLabel() {
          popupLabel = UILabel()
        popupLabel.backgroundColor = .white.withAlphaComponent(0.8)
           popupLabel.layer.cornerRadius = 8
          popupLabel.layer.masksToBounds = true
         popupLabel.textColor = .black
           popupLabel.textAlignment = .center
           popupLabel.numberOfLines = 0  // allow multi-line text
        popupLabel.alpha = 0  // initially invisible

        popupLabel.font = .systemFont(ofSize: 16)
            popupLabel.translatesAutoresizingMaskIntoConstraints = false
            arView.addSubview(popupLabel)

             NSLayoutConstraint.activate([
            popupLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
               popupLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor) , //居中显示
             popupLabel.widthAnchor.constraint(lessThanOrEqualToConstant: 250), //限制宽度
             popupLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 40)  //至少40高度
            ])


    }
     func setupTapGesture() {
          let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
           arView.addGestureRecognizer(tapGesture)
      }
    @objc func handleTap(_ gesture: UITapGestureRecognizer) {
          let tapLocation = gesture.location(in: arView)


          guard let raycastQuery = arView.makeRaycastQuery(from: tapLocation, allowing: .estimatedPlane, alignment: .any)
        else{
           return
        }

         guard let raycastResult = arView.session.raycast(raycastQuery).first else{
              print("未检测到实体碰撞")
                dismissPopupLabel() // remove it when click anywhere
                return

          }


        //从碰撞结果中获取Entity
          if let entity = raycastResult.entity {

               if let modelEntity = entity as? ModelEntity, let name = modelEntity.name{
                      displayPartName(partName:name)

               }
           }


     }

    func displayPartName(partName: String) {
            // Adjust text display here.
          popupLabel.text = partName
        popupLabel.alpha = 1  // set it to visible
        // Add animation here if needed
    }
      func dismissPopupLabel()
      {
        popupLabel.alpha = 0 //隐藏
      }



}

注意: 此方案的成败很大程度上依赖于模型的质量和建模时的规范,若模型未正确命名或者子模型层级过于复杂,实现难度也会增大。

其他注意事项

  1. 性能优化: 高精细度的模型碰撞检测计算量可能较大,应当考虑优化模型复杂度。对于较远的零件可以设置碰撞检测的距离限制。
  2. 容错处理: 处理点击落空,以及识别失败的场景,使用户获得恰当的反馈。
  3. UI呈现: 使用合适的用户界面组件显示零件名称,并确保它们在 AR 环境中清晰可见,并易于交互。

这种方案具有简单直观的特点,但在复杂模型场景下可能会有性能瓶颈,需要进行进一步的性能测试和优化。