iOS 18 RealityView 精准3D拖拽:解决位置漂移问题
2025-01-24 13:47:25
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 场景中对应的坐标。
以下步骤介绍具体的操作方法:
-
记录初始位置: 在拖动手势开始时记录实体的初始 3D 位置以及手指的 2D 初始位置。
-
转换坐标: 在拖动手势过程中,不断将新的 2D 手指位置转换到与初始 2D 位置相关的3D 坐标,此 3D 坐标相当于相对偏移量。
-
计算并应用偏移: 计算初始 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物体。
其他考量
- 初始位置 : 上述代码仅仅使用手势第一次接触位置作为转换的基础,如果要支持多次接触操作,你需要额外处理触控状态。比如添加手势状态
@GestureState var isDragging = false
,并在DragGesture().onChanged
的开头增加guard isDragging else { isDragging = true return }
在onEnded
中isDragging = false
. - 深度信息 : 示例代码使用的坐标转换并没有显式利用 3D 场景的深度信息,仅仅依赖相机位置和2D坐标映射。
- 缩放与旋转 : 如果场景中的相机位置、朝向或者视野发生变化,可能会导致触控和实体之间的偏移量出现问题。
通过 convert(point:from:to:)
进行坐标转换,可以让 RealityView 中的 3D 实体能够更精确的跟随 2D 手指拖动,达到用户预期效果,进而带来流畅自然的交互体验。使用这种方式避免了直接修改 3D 位置的不足,提高了交互的精准度和稳定性。 针对特定场景还可以适当调整和优化坐标转换的策略。