如何用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也能精准命中它们。
关键注意事项
- 坐标系优先级:操作子节点时优先用
worldPosition和worldEulerAngles,可以完全规避父节点变换带来的坐标偏移问题 - 手势冲突:如果多个手势(点击、平移、旋转)出现冲突,实现
UIGestureRecognizerDelegate的gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:)方法来解决 - 交互开关:确保子节点的
isUserInteractionEnabled属性为true(默认是true,但如果手动关闭过要改回来)
内容的提问来源于stack exchange,提问作者Vidhya Sri
相关产品推荐
相关产品推荐

