返回

iOS 18 RealityView 精准3D拖拽:解决位置漂移问题

IOS

RealityView 中 3D 实体拖拽的精确移动

在 iOS 18 中, RealityView 的引入简化了 3D 内容的创建。不过,使用 2D 触控拖拽手势来移动 3D 实体时,可能会遇到无法精确跟随手指的问题。比如,3D 实体移动不贴合手指位置,用户体验不够好。

问题分析

上述代码提供的实现方式直接将 DragGesture 的 2D 平移值 value.translation 乘以一个缩放比例来改变 3D 实体的位置,这种方式忽略了透视和深度信息,不能准确地映射 2D 手指移动到 3D 场景中。 因为屏幕上的 2D 坐标并不能直接与 3D 空间中的位置一一对应,单纯通过增量的方式会造成不一致的视觉效果,实体位置跟不上手指的移动,就像实体在”飘”而不是被拖拽。 在传统的 SceneKit 或 ARKit 中,利用投影 (project) 与反投影 (unproject) 或光线投射(raycast) 等方法,可以把 2D 坐标转换成精确的 3D 位置,而直接修改 3D 坐标的方法就显得简单粗暴。

解决方案:利用 convert(point:from:to:)进行坐标转换

RealityView 为我们提供了一个方便的方法 convert(point:from:to:) 来进行坐标转换,可以有效的解决这个问题。 convert(point:from:to:) 用于将 2D 点从指定的坐标空间转换到目标坐标空间。 借助这个方法,可以将触控点从屏幕坐标转换成 3D 场景中对应的坐标。

以下步骤介绍具体的操作方法:

  1. 记录初始位置: 在拖动手势开始时记录实体的初始 3D 位置以及手指的 2D 初始位置。

  2. 转换坐标: 在拖动手势过程中,不断将新的 2D 手指位置转换到与初始 2D 位置相关的3D 坐标,此 3D 坐标相当于相对偏移量。

  3. 计算并应用偏移: 计算初始 3D 位置加上转换后得到的相对偏移量。 此和既为当前 3D 实体的新位置。

import SwiftUI
import RealityKit

struct ContentView: View {
  @State var box = Entity()
  @State var initialBoxPosition: SIMD3<Float> = .zero
  @State var lastPanTouchPosition: CGPoint = .zero
  @Environment(\.realityKitContent) var realityKitContent
    
    

  var body: some View {
      RealityView{ content in
          let item = ModelEntity(mesh: .generateBox(size: .init(0.25, 0.25, 0.25)), materials: [SimpleMaterial(color: .blue, isMetallic: true)])
          box.addChild(item)
          content.add(box)
      }
      .gesture(dragThis)
  }

    
  var dragThis: some Gesture {
    DragGesture()
          .onChanged { value in
              
            if lastPanTouchPosition == .zero {
                  lastPanTouchPosition = value.location
                  initialBoxPosition = box.position
              }
              
                if let new3DLocation = convert2DPointTo3D(touchPoint: value.location, oldPoint: lastPanTouchPosition){
                    
                    box.position =  initialBoxPosition + new3DLocation
                   
                  }
              
                lastPanTouchPosition = value.location
             
        }
          .onEnded{ _ in
              lastPanTouchPosition = .zero
             
          }
    }
    
    func convert2DPointTo3D(touchPoint:CGPoint, oldPoint:CGPoint ) -> SIMD3<Float>? {

      guard let camera = realityKitContent.camera else{
         return nil
        }
         // 从屏幕坐标系 转换到 3d 坐标系
        let oldResult =  realityKitContent.convert(point: oldPoint, from: .local, to: camera )

          let newResult =  realityKitContent.convert(point: touchPoint, from: .local, to: camera )


           let locationChange:SIMD3<Float> =  newResult - oldResult
      
        return locationChange
     }
}

  • 代码逻辑更新:首先,我们在DragGesture().onChanged 中添加逻辑。当lastPanTouchPosition为默认值zero时, 我们将其设置为触碰的第一个坐标。 记录 box 的初始位置到initialBoxPosition. 随后我们使用转换坐标的方法 convert2DPointTo3D, 将旧的坐标和新的坐标转换为3D 坐标, 我们将得到新的3D偏移量, 将initialBoxPosition加上新的偏移量就可以实现3D entity 贴合手指的移动。最后我们将新的 touch 坐标更新为 lastPanTouchPosition 以便下次计算。 DragGesture().onEnded 中重置 lastPanTouchPosition ,结束拖动后不再移动3D物体。

其他考量

  1. 初始位置 : 上述代码仅仅使用手势第一次接触位置作为转换的基础,如果要支持多次接触操作,你需要额外处理触控状态。比如添加手势状态 @GestureState var isDragging = false,并在 DragGesture().onChanged 的开头增加 guard isDragging else { isDragging = true return } onEndedisDragging = false.
  2. 深度信息 : 示例代码使用的坐标转换并没有显式利用 3D 场景的深度信息,仅仅依赖相机位置和2D坐标映射。
  3. 缩放与旋转 : 如果场景中的相机位置、朝向或者视野发生变化,可能会导致触控和实体之间的偏移量出现问题。

通过 convert(point:from:to:) 进行坐标转换,可以让 RealityView 中的 3D 实体能够更精确的跟随 2D 手指拖动,达到用户预期效果,进而带来流畅自然的交互体验。使用这种方式避免了直接修改 3D 位置的不足,提高了交互的精准度和稳定性。 针对特定场景还可以适当调整和优化坐标转换的策略。