iOS 11 ARKit:3D视图中拖拽SCNNode异常,求正确实现方法
修复ARKit中SCNNode拖拽功能的正确实现
你的代码无法正常工作主要有几个关键问题:依赖硬编码的父节点层级(parent?.parent?.parent)获取目标节点、拖拽开始时的命中检测逻辑不够精准,以及拖拽过程中使用featurePoint导致位置跳变不稳定。下面是经过验证的正确实现方案:
第一步:准备必要属性
在视图控制器中添加这些属性,用于跟踪选中节点和拖拽初始状态:
import ARKit class ViewController: UIViewController { @IBOutlet weak var sceneView: ARSCNView! // 记录当前选中的拖拽节点 private var selectedNode: SCNNode? // 记录触摸开始时的屏幕位置与节点初始世界位置 private var initialTouchLocation: CGPoint? private var initialNodeWorldPosition: SCNVector3?
第二步:添加平移手势识别器
在viewDidLoad中添加拖拽用的手势识别器(使用UIPanGestureRecognizer更贴合拖拽场景):
override func viewDidLoad() { super.viewDidLoad() sceneView.delegate = self // 启动ARKit世界跟踪会话 let configuration = ARWorldTrackingConfiguration() configuration.planeDetection = .horizontal // 开启平面检测,可选 sceneView.session.run(configuration) // 添加平移手势识别器 let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) sceneView.addGestureRecognizer(panGesture) }
第三步:实现核心拖拽逻辑
分手势状态处理,确保每个阶段的操作精准稳定:
@objc private func handlePanGesture(_ gesture: UIPanGestureRecognizer) { let touchLocation = gesture.location(in: sceneView) switch gesture.state { case .began: // 1. 精准命中目标节点 let hitTestOptions: [SCNHitTestOption: Any] = [ .searchMode: SCNHitTestSearchMode.all.rawValue, // 可通过categoryBitMask过滤仅检测指定节点,避免误触 .categoryBitMask: 0b1 // 需提前给目标节点设置node.categoryBitMask = 0b1 ] let hitResults = sceneView.hitTest(touchLocation, options: hitTestOptions) guard let hitResult = hitResults.first else { return } // 直接获取命中节点,摒弃不可靠的父节点链依赖 selectedNode = hitResult.node // 记录初始状态 initialTouchLocation = touchLocation initialNodeWorldPosition = selectedNode?.worldPosition case .changed: guard let selectedNode = selectedNode, let initialNodePos = initialNodeWorldPosition else { return } // 2. 将屏幕触摸偏移转换为3D空间位置 // 优先检测已识别的平面,保证拖拽贴合平面更自然 let raycastQuery = sceneView.raycastQuery(from: touchLocation, allowing: .existingPlaneGeometry, alignment: .any) if let raycastResult = sceneView.session.raycast(raycastQuery).first { // 命中平面时,直接使用平面上的位置 selectedNode.worldPosition = SCNVector3( raycastResult.worldTransform.columns.3.x, raycastResult.worldTransform.columns.3.y, raycastResult.worldTransform.columns.3.z ) } else { // 无平面时,基于相机视角做自由空间平移 let camera = sceneView.pointOfView! let cameraForward = SCNVector3(-camera.transform.m31, -camera.transform.m32, -camera.transform.m33) let cameraRight = SCNVector3(camera.transform.m11, camera.transform.m12, camera.transform.m13) // 调整移动速度系数,按需修改 let moveSpeed: Float = 0.002 let delta = gesture.translation(in: sceneView) let offsetX = Float(delta.x) * moveSpeed let offsetY = Float(delta.y) * moveSpeed selectedNode.worldPosition = initialNodePos + cameraRight * offsetX - cameraForward * offsetY } case .ended, .cancelled: // 3. 拖拽结束,清空状态 selectedNode = nil initialTouchLocation = nil initialNodeWorldPosition = nil default: break } }
关键改进点说明
- 精准节点命中:通过
categoryBitMask过滤检测范围,直接使用命中结果的节点,避免依赖不稳定的父节点层级。 - 稳定位置更新:优先使用
raycast检测已识别平面,保证拖拽时节点贴合平面;无平面时基于相机视角平移,避免featurePoint导致的位置跳变。 - 状态管理:记录初始触摸位置和节点世界位置,确保拖拽过程的连贯性。
额外注意事项
- 确保目标节点的
isUserInteractionEnabled属性设为true,否则无法被命中检测到。 - 使用
worldPosition而非position设置节点位置,避免父节点坐标系的影响。
内容的提问来源于stack exchange,提问作者Abhishek Thapliyal
相关产品推荐
相关产品推荐

