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

如何用ARKit与手势识别器自由移动旋转SCNNode子节点

实现ARKit中子节点的自由移动与旋转操作

嘿,我明白你现在的困扰——能操作整个虚拟对象,但子节点只能沿特定轴动,没法像父节点那样随手指自由操控。下面我给你一套完整的解决方案,直接适配你的现有代码,帮你搞定子节点的自由交互:

一、先搞定子节点的正确选中逻辑

你当前的点击检测代码其实已经能选中子节点,但要注意别误取父节点。修改你的点击处理方法,确保精准选中用户点击的那个子节点:

@objc func handleTap(_ gesture: UITapGestureRecognizer) {
    let tapPoint = gesture.location(in: sceneView)
    
    // 优先检测所有3D节点(包括嵌套子节点)
    let nodeHitResults = sceneView.hitTest(tapPoint, options: [
        SCNHitTestOption.searchMode: SCNHitTestSearchMode.all.rawValue
    ])
    
    if let hitResult = nodeHitResults.first {
        // 直接绑定点击到的子节点,不用取parent!
        movedObject = hitResult.node
        print("已选中节点:\(movedObject?.name ?? "未命名子节点")")
    } else {
        // 没选中任何节点时,继续执行原有的平面放置逻辑
        let planeHitResults = sceneView.hitTest(tapPoint, types: .existingPlane)
        guard let hitResult = planeHitResults.last else { return }
        
        // 这里替换成你的根虚拟对象节点
        rootVirtualObject.position = SCNVector3(
            hitResult.worldTransform.columns.3.x,
            hitResult.worldTransform.columns.3.y,
            hitResult.worldTransform.columns.3.z
        )
    }
}

二、实现子节点的自由平移(脱离平面限制)

之前的方案受限于平面检测,现在改用屏幕触摸偏移转3D空间坐标的方式,让子节点随手指自由移动:

1. 先定义几个辅助变量

private var initialTouchPos: CGPoint?
private var initialNodeWorldPos: SCNVector3?

2. 添加平移手势

let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
sceneView.addGestureRecognizer(panGesture)

3. 处理平移逻辑

@objc func handlePan(_ gesture: UIPanGestureRecognizer) {
    guard let targetNode = movedObject else { return }
    
    switch gesture.state {
    case .began:
        initialTouchPos = gesture.location(in: sceneView)
        initialNodeWorldPos = targetNode.worldPosition // 用世界坐标避免父节点层级干扰
    case .changed:
        guard let initialTouch = initialTouchPos, let initialPos = initialNodeWorldPos else { return }
        
        // 计算屏幕触摸的偏移量
        let deltaX = gesture.location(in: sceneView).x - initialTouch.x
        let deltaY = gesture.location(in: sceneView).y - initialTouch.y
        
        // 调整灵敏度,根据你的需求修改数值
        let sensitivity: Float = 0.01
        // 转换为3D空间的位移,屏幕Y轴和3D空间Y轴方向相反,所以取反
        let translation = SCNVector3(
            Float(deltaX) * sensitivity,
            -Float(deltaY) * sensitivity,
            0
        )
        
        // 直接更新世界坐标,无视父节点的变换
        targetNode.worldPosition = SCNVector3(
            initialPos.x + translation.x,
            initialPos.y + translation.y,
            initialPos.z + translation.z
        )
    case .ended, .cancelled:
        initialTouchPos = nil
        initialNodeWorldPos = nil
    default: break
    }
}

三、实现子节点的自由旋转

添加旋转手势,让子节点能像父节点一样随手指旋转:

1. 添加旋转手势

let rotationGesture = UIRotationGestureRecognizer(target: self, action: #selector(handleRotation(_:)))
sceneView.addGestureRecognizer(rotationGesture)

2. 处理旋转逻辑

private var initialNodeWorldRotation: SCNVector3?

@objc func handleRotation(_ gesture: UIRotationGestureRecognizer) {
    guard let targetNode = movedObject else { return }
    
    switch gesture.state {
    case .began:
        initialNodeWorldRotation = targetNode.worldEulerAngles // 记录初始世界旋转角度
    case .changed:
        guard let initialRotation = initialNodeWorldRotation else { return }
        
        // 将手势的旋转角度(弧度)转换为3D空间的旋转,这里默认绕Y轴(垂直屏幕)旋转
        let rotationDelta = Float(gesture.rotation)
        targetNode.worldEulerAngles.y = initialRotation.y - rotationDelta
        
        // 如果需要绕X/Z轴旋转,直接修改对应的欧拉角即可:
        // targetNode.worldEulerAngles.x = initialRotation.x + rotationDelta
        // targetNode.worldEulerAngles.z = initialRotation.z + rotationDelta
    case .ended, .cancelled:
        initialNodeWorldRotation = nil
    default: break
    }
}

四、解决SCNReferenceNode的适配问题

你不用把SCNReferenceNode换成SCNNode!它本身就是SCNNode的子类,只要确保你已经加载了引用内容:

// 加载SCNReferenceNode示例
guard let referenceURL = Bundle.main.url(forResource: "你的虚拟对象", withExtension: "scn") else { return }
let referenceNode = SCNReferenceNode(url: referenceURL)!
referenceNode.load() // 必须调用load()才能访问子节点

sceneView.scene.rootNode.addChildNode(referenceNode)

加载完成后,referenceNode的子节点会被正常识别,hitTest也能精准命中它们。

关键注意事项

  1. 坐标系优先级:操作子节点时优先用worldPosition和worldEulerAngles,可以完全规避父节点变换带来的坐标偏移问题
  2. 手势冲突:如果多个手势(点击、平移、旋转)出现冲突,实现UIGestureRecognizerDelegate的gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:)方法来解决
  3. 交互开关:确保子节点的isUserInteractionEnabled属性为true(默认是true,但如果手动关闭过要改回来)

内容的提问来源于stack exchange,提问作者Vidhya Sri

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 09:24:25