如何在ARKit中实现不受轴限制的SCNNode手指拖拽?
Absolutely feasible! The issue with your current code is that it’s tied to existing plane hit tests, which restricts movement only to detected surfaces. To drag any child node—whether it’s on a plane or floating freely—you need to calculate 3D movement based on the camera’s perspective and the finger’s screen position, instead of relying on plane detection.
Here’s a Step-by-Step Solution
We’ll use a UIPanGestureRecognizer to track the drag, and calculate the node’s target position by mapping screen touches to 3D space relative to the camera’s viewpoint. This ignores axis constraints and works regardless of the node’s orientation or position.
1. Setup the Gesture Recognizer
First, add the pan gesture to your ARSCNView:
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:))) sceneView.addGestureRecognizer(panGesture)
2. Track Drag State & Calculate Movement
We’ll need to store:
- The selected child node
- The initial world position of the node
- The initial distance from the camera to the node (to maintain depth as we drag)
Here’s the gesture handler code:
private var draggedNode: SCNNode? private var initialNodeWorldPosition: SCNVector3? private var initialDistanceToCamera: Float? @objc private func handlePan(_ gesture: UIPanGestureRecognizer) { let touchLocation = gesture.location(in: sceneView) switch gesture.state { case .began: // Detect the tapped child node (search all nodes, not just surfaces) guard let hitResult = sceneView.hitTest(touchLocation, options: [.searchMode: SCNHitTestSearchMode.all.rawValue]).first else { return } draggedNode = hitResult.node initialNodeWorldPosition = draggedNode?.worldPosition // Calculate initial distance from camera to the node guard let cameraPosition = sceneView.pointOfView?.worldPosition, let nodePosition = initialNodeWorldPosition else { return } initialDistanceToCamera = SCNVector3Distance(cameraPosition, nodePosition) case .changed: guard let draggedNode = draggedNode, let initialDistance = initialDistanceToCamera, let _ = initialNodeWorldPosition else { return } // Create a ray from the camera through the current touch point guard let camera = sceneView.pointOfView else { return } let screenPoint = SCNVector3(touchLocation.x, touchLocation.y, 0) let currentRay = sceneView.unprojectPoint(screenPoint) - camera.worldPosition let normalizedRay = currentRay.normalized() // Calculate target position: move along the ray to maintain initial depth let targetWorldPosition = camera.worldPosition + normalizedRay * initialDistance // Convert world position back to the node's parent space (critical for child nodes) if let parent = draggedNode.parent { draggedNode.position = parent.convertPosition(targetWorldPosition, from: nil) } else { draggedNode.position = targetWorldPosition } case .ended, .cancelled: // Reset tracking variables draggedNode = nil initialNodeWorldPosition = nil initialDistanceToCamera = nil default: break } }
3. Add a Helper Extension for Vector Math
You’ll need this extension to normalize vectors and calculate distance:
extension SCNVector3 { static func - (lhs: SCNVector3, rhs: SCNVector3) -> SCNVector3 { return SCNVector3(lhs.x - rhs.x, lhs.y - rhs.y, lhs.z - rhs.z) } static func + (lhs: SCNVector3, rhs: SCNVector3) -> SCNVector3 { return SCNVector3(lhs.x + rhs.x, lhs.y + rhs.y, lhs.z + rhs.z) } static func * (lhs: SCNVector3, scalar: Float) -> SCNVector3 { return SCNVector3(lhs.x * scalar, lhs.y * scalar, lhs.z * scalar) } func normalized() -> SCNVector3 { let length = sqrt(x*x + y*y + z*z) return SCNVector3(x/length, y/length, z/length) } } func SCNVector3Distance(_ a: SCNVector3, _ b: SCNVector3) -> Float { let dx = a.x - b.x let dy = a.y - b.y let dz = a.z - b.z return sqrt(dx*dx + dy*dy + dz*dz) }
Why This Works
- No Plane Dependence: We’re using the camera’s perspective to map screen touches to 3D space, so the node can be anywhere—on a plane, floating in mid-air, etc.
- Axis-Free Movement: The node follows the exact direction of your finger’s movement in 3D space, ignoring any axis constraints or the node’s own Euler angles.
- Child Node Support: We convert the target world position back to the node’s parent space, which ensures child nodes move correctly relative to their parent hierarchy.
Fixing Your Original Code
Your original code used hitTest(with:types: .existingPlane), which locks movement to planes. By replacing that with camera raycasting and maintaining the node’s initial depth, you avoid this restriction entirely.
内容的提问来源于stack exchange,提问作者Vidhya Sri

