You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 07:39:08