ios - ARKIT:使用 PanGesture 移动对象(正确方式)

标签 ios xcode scenekit arkit

我一直在阅读大量关于如何通过在屏幕上拖动对象来移动对象的 StackOverflow 答案。有些使用针对 .featurePoints 的 HitTest ,有些使用手势平移或仅跟踪对象的 lastPosition。但老实说..没有人能像每个人期望的那样工作。

针对 .featurePoints 的 HitTest 只会让对象四处跳跃,因为拖动手指时并不总是命中特征点。我不明白为什么每个人都在建议这个。

像这样的解决方案工作:Dragging SCNNode in ARKit Using SceneKit

但是物体并没有真正跟随你的手指,当你走几步或改变物体或相机的角度时..并尝试移动物体..x,z都倒置了..完全有意义要做到这一点。

我真的很想像 Apple Demo 一样移动对象,但我查看了 Apple 的代码......而且非常奇怪和过于复杂,我什至无法理解。他们将物体移动得如此漂亮的技术甚至与每个人在网上提出的建议都不接近。
https://developer.apple.com/documentation/arkit/handling_3d_interaction_and_ui_controls_in_augmented_reality

必须有一种更简单的方法来做到这一点。

最佳答案

简短的回答:
要获得 Apple 演示项目中这种漂亮流畅的拖动效果,您必须像 Apple 演示项目(处理 3D 交互)中那样进行操作。另一方面,我同意你的观点,如果你第一次看代码可能会让人困惑。计算放置在地板上的物体的正确运动一点也不容易——总是从每个位置或视角。这是一个复杂的代码结构,正在做这种极好的拖动效果。苹果在实现这一目标方面做得很好,但对我们来说并不容易。

完整答案:
为您的需要删除 AR 交互模板会导致一场噩梦 - 但如果您投入足够的时间,也应该可以工作。如果您喜欢从头开始,基本上可以使用通用的 swift ARKit/SceneKit Xcode 模板(包含 spaceship 的模板)。

您还需要 Apple 提供的整个 AR 交互模板项目。 (链接包含在 SO 问题中)
最后你应该可以拖动一个叫做 VirtualObject 的东西,它实际上是一个特殊的 SCNNode。此外,您将拥有一个漂亮的焦点方 block ,它可以用于任何目的 - 例如最初放置物体或添加地板或墙壁。 (一些用于拖动效果和焦点方 block 使用的代码是合并或链接在一起的——没有焦点方 block 的情况下实际上会更复杂)

开始:
将以下文件从 AR 交互模板复制到您的空项目:

  • Utilities.swift(通常我将此文件命名为 Extensions.swift,它包含一些必需的基本扩展)
  • FocusSquare.swift
  • FocusSquareSegment.swift
  • ThresholdPanGesture.swift
  • VirtualObject.swift
  • VirtualObjectLoader.swift
  • VirtualObjectARView.swift

  • 将 UIGestureRecognizerDelegate 添加到 ViewController 类定义中,如下所示:
    class ViewController: UIViewController, ARSCNViewDelegate, UIGestureRecognizerDelegate {
    

    将此代码添加到 ViewController.swift 中的定义部分,就在 viewDidLoad 之前:
    // MARK: for the Focus Square
    // SUPER IMPORTANT: the screenCenter must be defined this way
    var focusSquare = FocusSquare()
    var screenCenter: CGPoint {
        let bounds = sceneView.bounds
        return CGPoint(x: bounds.midX, y: bounds.midY)
    }
    var isFocusSquareEnabled : Bool = true
    
    
    // *** FOR OBJECT DRAGGING PAN GESTURE - APPLE ***
    /// The tracked screen position used to update the `trackedObject`'s position in `updateObjectToCurrentTrackingPosition()`.
    private var currentTrackingPosition: CGPoint?
    
    /**
     The object that has been most recently intereacted with.
     The `selectedObject` can be moved at any time with the tap gesture.
     */
    var selectedObject: VirtualObject?
    
    /// The object that is tracked for use by the pan and rotation gestures.
    private var trackedObject: VirtualObject? {
        didSet {
            guard trackedObject != nil else { return }
            selectedObject = trackedObject
        }
    }
    
    /// Developer setting to translate assuming the detected plane extends infinitely.
    let translateAssumingInfinitePlane = true
    // *** FOR OBJECT DRAGGING PAN GESTURE - APPLE ***
    

    在 viewDidLoad 中,在设置场景之前添加以下代码:
    // *** FOR OBJECT DRAGGING PAN GESTURE - APPLE ***
    let panGesture = ThresholdPanGesture(target: self, action: #selector(didPan(_:)))
    panGesture.delegate = self
    
    // Add gestures to the `sceneView`.
    sceneView.addGestureRecognizer(panGesture)
    // *** FOR OBJECT DRAGGING PAN GESTURE - APPLE ***
    

    在 ViewController.swift 的最后添加以下代码:
    // MARK: - Pan Gesture Block
    // *** FOR OBJECT DRAGGING PAN GESTURE - APPLE ***
    @objc
    func didPan(_ gesture: ThresholdPanGesture) {
        switch gesture.state {
        case .began:
            // Check for interaction with a new object.
            if let object = objectInteracting(with: gesture, in: sceneView) {
                trackedObject = object // as? VirtualObject
            }
    
        case .changed where gesture.isThresholdExceeded:
            guard let object = trackedObject else { return }
            let translation = gesture.translation(in: sceneView)
    
            let currentPosition = currentTrackingPosition ?? CGPoint(sceneView.projectPoint(object.position))
    
            // The `currentTrackingPosition` is used to update the `selectedObject` in `updateObjectToCurrentTrackingPosition()`.
            currentTrackingPosition = CGPoint(x: currentPosition.x + translation.x, y: currentPosition.y + translation.y)
    
            gesture.setTranslation(.zero, in: sceneView)
    
        case .changed:
            // Ignore changes to the pan gesture until the threshold for displacment has been exceeded.
            break
    
        case .ended:
            // Update the object's anchor when the gesture ended.
            guard let existingTrackedObject = trackedObject else { break }
            addOrUpdateAnchor(for: existingTrackedObject)
            fallthrough
    
        default:
            // Clear the current position tracking.
            currentTrackingPosition = nil
            trackedObject = nil
        }
    }
    
    // - MARK: Object anchors
    /// - Tag: AddOrUpdateAnchor
    func addOrUpdateAnchor(for object: VirtualObject) {
        // If the anchor is not nil, remove it from the session.
        if let anchor = object.anchor {
            sceneView.session.remove(anchor: anchor)
        }
    
        // Create a new anchor with the object's current transform and add it to the session
        let newAnchor = ARAnchor(transform: object.simdWorldTransform)
        object.anchor = newAnchor
        sceneView.session.add(anchor: newAnchor)
    }
    
    
    private func objectInteracting(with gesture: UIGestureRecognizer, in view: ARSCNView) -> VirtualObject? {
        for index in 0..<gesture.numberOfTouches {
            let touchLocation = gesture.location(ofTouch: index, in: view)
    
            // Look for an object directly under the `touchLocation`.
            if let object = virtualObject(at: touchLocation) {
                return object
            }
        }
    
        // As a last resort look for an object under the center of the touches.
        // return virtualObject(at: gesture.center(in: view))
        return virtualObject(at: (gesture.view?.center)!)
    }
    
    
    /// Hit tests against the `sceneView` to find an object at the provided point.
    func virtualObject(at point: CGPoint) -> VirtualObject? {
    
        // let hitTestOptions: [SCNHitTestOption: Any] = [.boundingBoxOnly: true]
        let hitTestResults = sceneView.hitTest(point, options: [SCNHitTestOption.categoryBitMask: 0b00000010, SCNHitTestOption.searchMode: SCNHitTestSearchMode.any.rawValue as NSNumber])
        // let hitTestOptions: [SCNHitTestOption: Any] = [.boundingBoxOnly: true]
        // let hitTestResults = sceneView.hitTest(point, options: hitTestOptions)
    
        return hitTestResults.lazy.compactMap { result in
            return VirtualObject.existingObjectContainingNode(result.node)
            }.first
    }
    
    /**
     If a drag gesture is in progress, update the tracked object's position by
     converting the 2D touch location on screen (`currentTrackingPosition`) to
     3D world space.
     This method is called per frame (via `SCNSceneRendererDelegate` callbacks),
     allowing drag gestures to move virtual objects regardless of whether one
     drags a finger across the screen or moves the device through space.
     - Tag: updateObjectToCurrentTrackingPosition
     */
    @objc
    func updateObjectToCurrentTrackingPosition() {
        guard let object = trackedObject, let position = currentTrackingPosition else { return }
        translate(object, basedOn: position, infinitePlane: translateAssumingInfinitePlane, allowAnimation: true)
    }
    
    /// - Tag: DragVirtualObject
    func translate(_ object: VirtualObject, basedOn screenPos: CGPoint, infinitePlane: Bool, allowAnimation: Bool) {
        guard let cameraTransform = sceneView.session.currentFrame?.camera.transform,
            let result = smartHitTest(screenPos,
                                      infinitePlane: infinitePlane,
                                      objectPosition: object.simdWorldPosition,
                                      allowedAlignments: [ARPlaneAnchor.Alignment.horizontal]) else { return }
    
        let planeAlignment: ARPlaneAnchor.Alignment
        if let planeAnchor = result.anchor as? ARPlaneAnchor {
            planeAlignment = planeAnchor.alignment
        } else if result.type == .estimatedHorizontalPlane {
            planeAlignment = .horizontal
        } else if result.type == .estimatedVerticalPlane {
            planeAlignment = .vertical
        } else {
            return
        }
    
        /*
         Plane hit test results are generally smooth. If we did *not* hit a plane,
         smooth the movement to prevent large jumps.
         */
        let transform = result.worldTransform
        let isOnPlane = result.anchor is ARPlaneAnchor
        object.setTransform(transform,
                            relativeTo: cameraTransform,
                            smoothMovement: !isOnPlane,
                            alignment: planeAlignment,
                            allowAnimation: allowAnimation)
    }
    // *** FOR OBJECT DRAGGING PAN GESTURE - APPLE ***
    

    添加一些焦点平方代码
    // MARK: - Focus Square (code by Apple, some by me)
    func updateFocusSquare(isObjectVisible: Bool) {
        if isObjectVisible {
            focusSquare.hide()
        } else {
            focusSquare.unhide()
        }
    
        // Perform hit testing only when ARKit tracking is in a good state.
        if let camera = sceneView.session.currentFrame?.camera, case .normal = camera.trackingState,
            let result = smartHitTest(screenCenter) {
            DispatchQueue.main.async {
                self.sceneView.scene.rootNode.addChildNode(self.focusSquare)
                self.focusSquare.state = .detecting(hitTestResult: result, camera: camera)
            }
        } else {
            DispatchQueue.main.async {
                self.focusSquare.state = .initializing
                self.sceneView.pointOfView?.addChildNode(self.focusSquare)
            }
        }
    }
    

    并添加一些控制功能:
    func hideFocusSquare()  { DispatchQueue.main.async { self.updateFocusSquare(isObjectVisible: true) } }  // to hide the focus square
    func showFocusSquare()  { DispatchQueue.main.async { self.updateFocusSquare(isObjectVisible: false) } } // to show the focus square
    

    从 VirtualObjectARView.swift 复制!整个函数 smartHitTest 到 ViewController.swift (所以它们存在两次)
    func smartHitTest(_ point: CGPoint,
                      infinitePlane: Bool = false,
                      objectPosition: float3? = nil,
                      allowedAlignments: [ARPlaneAnchor.Alignment] = [.horizontal, .vertical]) -> ARHitTestResult? {
    
        // Perform the hit test.
        let results = sceneView.hitTest(point, types: [.existingPlaneUsingGeometry, .estimatedVerticalPlane, .estimatedHorizontalPlane])
    
        // 1. Check for a result on an existing plane using geometry.
        if let existingPlaneUsingGeometryResult = results.first(where: { $0.type == .existingPlaneUsingGeometry }),
            let planeAnchor = existingPlaneUsingGeometryResult.anchor as? ARPlaneAnchor, allowedAlignments.contains(planeAnchor.alignment) {
            return existingPlaneUsingGeometryResult
        }
    
        if infinitePlane {
    
            // 2. Check for a result on an existing plane, assuming its dimensions are infinite.
            //    Loop through all hits against infinite existing planes and either return the
            //    nearest one (vertical planes) or return the nearest one which is within 5 cm
            //    of the object's position.
            let infinitePlaneResults = sceneView.hitTest(point, types: .existingPlane)
    
            for infinitePlaneResult in infinitePlaneResults {
                if let planeAnchor = infinitePlaneResult.anchor as? ARPlaneAnchor, allowedAlignments.contains(planeAnchor.alignment) {
                    if planeAnchor.alignment == .vertical {
                        // Return the first vertical plane hit test result.
                        return infinitePlaneResult
                    } else {
                        // For horizontal planes we only want to return a hit test result
                        // if it is close to the current object's position.
                        if let objectY = objectPosition?.y {
                            let planeY = infinitePlaneResult.worldTransform.translation.y
                            if objectY > planeY - 0.05 && objectY < planeY + 0.05 {
                                return infinitePlaneResult
                            }
                        } else {
                            return infinitePlaneResult
                        }
                    }
                }
            }
        }
    
        // 3. As a final fallback, check for a result on estimated planes.
        let vResult = results.first(where: { $0.type == .estimatedVerticalPlane })
        let hResult = results.first(where: { $0.type == .estimatedHorizontalPlane })
        switch (allowedAlignments.contains(.horizontal), allowedAlignments.contains(.vertical)) {
        case (true, false):
            return hResult
        case (false, true):
            // Allow fallback to horizontal because we assume that objects meant for vertical placement
            // (like a picture) can always be placed on a horizontal surface, too.
            return vResult ?? hResult
        case (true, true):
            if hResult != nil && vResult != nil {
                return hResult!.distance < vResult!.distance ? hResult! : vResult!
            } else {
                return hResult ?? vResult
            }
        default:
            return nil
        }
    }
    

    您可能会在所复制的函数中看到有关 hitTest 的一些错误。像这样更正它:
    hitTest... // which gives an Error
    sceneView.hitTest... // this should correct it
    

    实现渲染器 updateAtTime 函数并添加以下行:
    func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
        // For the Focus Square
        if isFocusSquareEnabled { showFocusSquare() }
    
        self.updateObjectToCurrentTrackingPosition() // *** FOR OBJECT DRAGGING PAN GESTURE - APPLE ***
    }
    

    最后为焦点广场添加一些辅助功能
    func hideFocusSquare() { DispatchQueue.main.async { self.updateFocusSquare(isObjectVisible: true) } }  // to hide the focus square
    func showFocusSquare() { DispatchQueue.main.async { self.updateFocusSquare(isObjectVisible: false) } } // to show the focus square
    

    此时,您可能仍会在导入的文件中看到十几个错误和警告,这可能会在 Swift 5 中执行此操作并且您有一些 Swift 4 文件时发生。只需让 Xcode 纠正错误。 (这都是关于重命名一些代码语句,Xcode 最清楚)

    进入 VirtualObject.swift 并搜索此代码块:
    if smoothMovement {
        let hitTestResultDistance = simd_length(positionOffsetFromCamera)
    
        // Add the latest position and keep up to 10 recent distances to smooth with.
        recentVirtualObjectDistances.append(hitTestResultDistance)
        recentVirtualObjectDistances = Array(recentVirtualObjectDistances.suffix(10))
    
        let averageDistance = recentVirtualObjectDistances.average!
        let averagedDistancePosition = simd_normalize(positionOffsetFromCamera) * averageDistance
        simdPosition = cameraWorldPosition + averagedDistancePosition
    } else {
        simdPosition = cameraWorldPosition + positionOffsetFromCamera
    }
    

    用这行代码注释或替换整个 block :
    simdPosition = cameraWorldPosition + positionOffsetFromCamera
    

    此时,您应该能够编译项目并在设备上运行它。你应该看到宇宙飞船和一个应该已经工作的黄色焦点方 block 。

    要开始放置一个可以拖动的对象,您需要一些函数来创建一个所谓的 VirtualObject,正如我在开头所说的那样。

    使用此示例函数进行测试(将其添加到 View Controller 中的某处):
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    
        if focusSquare.state != .initializing {
            let position = SCNVector3(focusSquare.lastPosition!)
    
            // *** FOR OBJECT DRAGGING PAN GESTURE - APPLE ***
            let testObject = VirtualObject() // give it some name, when you dont have anything to load
            testObject.geometry = SCNCone(topRadius: 0.0, bottomRadius: 0.2, height: 0.5)
            testObject.geometry?.firstMaterial?.diffuse.contents = UIColor.red
            testObject.categoryBitMask = 0b00000010
            testObject.name = "test"
            testObject.castsShadow = true
            testObject.position = position
    
            sceneView.scene.rootNode.addChildNode(testObject)
        }
    }
    

    注意:您要在平面上拖动的所有内容都必须使用 VirtualObject() 而不是 SCNNode() 进行设置。关于 VirtualObject 的所有其他内容都与 SCNNode 相同

    (您还可以添加一些常见的 SCNNode 扩展,例如按名称加载场景的扩展 - 在引用导入的模型时很有用)

    玩得开心!

    关于ios - ARKIT:使用 PanGesture 移动对象(正确方式),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50131675/

    相关文章:

    ios - 在后台 3 秒时间间隔后重复调用方法

    ios - iOS6 上的推特库

    ios - 场景套件: Use multiple SCNView instances side by side

    ios - 如何在 ARKit 和 SceneKit 中正确缩放 DAE 模型?

    iphone - 如何获取目标名称?

    swift - "Cannot assign to the result of this expression"变量 (var) 引起的错误

    ios - libgdx 和 robovm,打开 facebook 共享对话框

    iphone - iOS 5 : Is it possible to write a struct to a plist file?

    ios - 深色模式比浅色模式需要更多时间加载图片

    iphone - 为 iOS 5 构建的 pjsip,不会