ARKit技术问询:如何像GIPHY World应用一样在空中移动物体
Great question! I’ve built similar AR interaction systems before, so let’s walk through exactly how to implement that free-floating object movement and mode switching like GIPHY World does.
First, let’s clarify the core difference between your existing plane-based drag and the free-floating mode:
- Plane Detection Mode: Raycasts against real-world surfaces detected by ARKit/ARCore to anchor object movement.
- Free-Floating Mode: Ignores real planes entirely, instead using a virtual "target" (either a fixed virtual plane or gesture-based relative tracking) to calculate where the object should move in 3D space.
1. Build the Mode Switching Foundation
Start by creating a state system to manage which mode the app is in, and tie it to a UI control (like a button) for users to toggle:
// Define your mode enum (iOS example; adapt for Android/Unity as needed) enum ARPlacementMode { case planeDetection case freeFloating } var currentMode: ARPlacementMode = .planeDetection // Toggle mode with a button @IBAction func toggleModeTapped(_ sender: UIButton) { currentMode = currentMode == .planeDetection ? .freeFloating : .planeDetection sender.setTitle(currentMode == .planeDetection ? "Switch to Free Mode" : "Switch to Plane Mode", for: .normal) }
2. Implement Free-Floating Object Movement
There are two reliable approaches for this—pick the one that fits your use case:
Option 1: Virtual Plane Raycasting (Most Like GIPHY World)
This method creates an invisible virtual plane fixed in front of the camera (e.g., 1.5 meters away) and raycasts against it, just like you do with real planes. It’s intuitive for users because it mimics placing objects on a "virtual table" in mid-air.
func handlePanGesture(_ gesture: UIPanGestureRecognizer) { guard let sceneView = gesture.view as? ARSCNView else { return } switch currentMode { case .planeDetection: // Your existing plane-based drag logic here let touchLoc = gesture.location(in: sceneView) if let hitResult = sceneView.hitTest(touchLoc, types: .existingPlaneUsingExtent).first { let newPos = hitResult.worldTransform.columns.3 yourObjectNode.position = SCNVector3(newPos.x, newPos.y, newPos.z) } case .freeFloating: let touchLoc = gesture.location(in: sceneView) guard let camera = sceneView.pointOfView else { return } // Define the invisible virtual plane: 1.5m in front of camera, facing the user let planeDistance: Float = 1.5 let planeOrigin = camera.position + camera.worldFront * planeDistance let planeNormal = camera.worldFront // Create a ray from the user's touch point guard let ray = sceneView.ray(fromScreenPoint: touchLoc) else { return } // Calculate where the ray intersects the virtual plane let denominator = ray.direction.dot(planeNormal) guard abs(denominator) > 0.0001 else { return } // Avoid parallel rays let t = (planeOrigin - ray.origin).dot(planeNormal) / denominator guard t > 0 else { return } // Only care about intersections in front of camera let intersectionPoint = ray.origin + ray.direction * t // Smoothly update the object's position SCNTransaction.begin() SCNTransaction.animationDuration = 0.1 yourObjectNode.position = intersectionPoint SCNTransaction.commit() } }
Option 2: Gesture-Based Relative Tracking
This method tracks the user’s pan gesture on the screen and converts that 2D movement into 3D space relative to the camera’s current position. It’s great for letting users "drag" objects freely without a fixed plane.
private var initialObjectPos: SCNVector3? private var initialCameraTransform: SCNMatrix4? func handlePanGesture(_ gesture: UIPanGestureRecognizer) { guard let sceneView = gesture.view as? ARSCNView else { return } switch gesture.state { case .began: if currentMode == .freeFloating { initialObjectPos = yourObjectNode.position initialCameraTransform = sceneView.pointOfView?.transform } case .changed: guard currentMode == .freeFloating, let initialObjPos = initialObjectPos, let initialCamTransform = initialCameraTransform else { return } let currentCamTransform = sceneView.pointOfView?.transform ?? initialCamTransform // Calculate how the camera has moved since the gesture started let camRotationDelta = currentCamTransform.inverse * initialCamTransform // Convert screen pan to 3D movement (adjust sensitivity as needed) let translation = gesture.translation(in: sceneView) let sensitivity: Float = 0.002 let localDelta = SCNVector3(Float(translation.x) * sensitivity, Float(-translation.y) * sensitivity, 0) // Convert local camera-space movement to world space let worldDelta = localDelta.transformed(by: camRotationDelta) // Update object position yourObjectNode.position = initialObjPos + worldDelta case .ended, .cancelled: initialObjectPos = nil initialCameraTransform = nil default: break } }
3. Seamless Mode Switching Details
To make the transition feel natural:
- Preserve Object Position: When switching from plane mode to free mode, keep the object in its current 3D position—don’t reset it to the virtual plane.
- Reset Raycast Targets: Ensure that when in plane mode, your hit tests only check real AR planes; when in free mode, ignore real planes entirely.
- Visual Feedback: Add a small UI indicator (like an icon) to show which mode is active, so users never feel confused.
4. Polish the Experience
- Add Distance Adjustment: Let users pinch to zoom the object closer/farther in free mode, or swipe up/down on the screen to adjust the Z-axis position.
- Collision Detection: If you have multiple objects, add basic collision checks in free mode to prevent objects from overlapping.
- Smooth Animations: Always use short, subtle animations when updating the object’s position—this makes the movement feel fluid instead of jerky.
内容的提问来源于stack exchange,提问作者omarojo

