ARKIT: перемещение объекта с помощью PanGesture (правильный путь) - PullRequest
0 голосов
/ 02 мая 2018

Я читал множество ответов StackOverflow о том, как перемещать объект, перетаскивая его по экрану. Некоторые используют тесты попаданий для .featurePoints, некоторые используют перевод жестов или просто отслеживают последнюю позицию объекта. Но, честно говоря .. никто не работает так, как все ожидают, что это сработает.

Тестирование удара по .featurePoints просто заставляет объект прыгать повсюду, потому что при перетаскивании пальца вы не всегда попадаете в характерную точку. Я не понимаю, почему все продолжают предлагать это.

Решения, подобные этой, работают: Перетаскивание SCNNode в ARKit с помощью SceneKit

Но объект на самом деле не следует за вашим пальцем, и в тот момент, когда вы делаете несколько шагов или изменяете угол объекта или камеры ... и пытаетесь переместить объект ... все x, z инвертированы ... и имеет смысл сделать это.

Я действительно хочу перемещать объекты так же хорошо, как Apple Demo, но я смотрю на код от Apple ... и это безумно странно и слишком сложно, я даже не могу понять немного. Их техника перемещения объекта так красиво даже не близка к тому, что все предлагают онлайн. https://developer.apple.com/documentation/arkit/handling_3d_interaction_and_ui_controls_in_augmented_reality

Должен быть более простой способ сделать это.

Ответы [ 3 ]

0 голосов
/ 01 апреля 2019

Я добавил некоторые из своих идей к ответу Клэссона. Я заметил некоторую задержку при перетаскивании узла. Я обнаружил, что узел не может следовать за движением пальца.

Чтобы сделать движение узла более плавным, я добавил переменную, которая отслеживает перемещаемый в данный момент узел, и установил положение в месте касания.

    var selectedNode: SCNNode?

Кроме того, я установил значение .categoryBitMask, чтобы указать категорию узлов, которые я хочу редактировать (перемещать). Значение битовой маски по умолчанию равно 1.

Причина, по которой мы устанавливаем битовую маску категории, состоит в том, чтобы различать различные виды узлов и указывать те, которые вы хотите выбрать (для перемещения и т. Д.).

    enum CategoryBitMask: Int {
        case categoryToSelect = 2        // 010
        case otherCategoryToSelect = 4   // 100
        // you can add more bit masks below . . .
    }

Затем я добавил UILongPressGestureRecognizer в viewDidLoad().

        let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressed))
        self.sceneView.addGestureRecognizer(longPressRecognizer)

Ниже приведен UILongPressGestureRecognizer, который я использовал для обнаружения длительного нажатия, которое инициирует перетаскивание узла.

Сначала получите прикосновение location от recognizerView

    @objc func longPressed(recognizer: UILongPressGestureRecognizer) {

       guard let recognizerView = recognizer.view as? ARSCNView else { return }
       let touch = recognizer.location(in: recognizerView)

Следующий код запускается один раз при долгом нажатии.

Здесь мы выполняем hitTest, чтобы выбрать узел, к которому прикоснулись. Обратите внимание, что здесь мы указываем опцию .categoryBitMask для выбора только узлов следующей категории: CategoryBitMask.categoryToSelect

       // Runs once when long press is detected.
       if recognizer.state == .began {
            // perform a hitTest
            let hitTestResult = self.sceneView.hitTest(touch, options: [SCNHitTestOption.categoryBitMask: CategoryBitMask.categoryToSelect])

            guard let hitNode = hitTestResult.first?.node else { return }

            // Set hitNode as selected
            self.selectedNode = hitNode

Следующий код будет периодически запускаться, пока пользователь не отпустит палец. Здесь мы выполняем еще один hitTest, чтобы получить плоскость, по которой должен двигаться узел.

        // Runs periodically after .began
        } else if recognizer.state == .changed {
            // make sure a node has been selected from .began
            guard let hitNode = self.selectedNode else { return }

            // perform a hitTest to obtain the plane 
            let hitTestPlane = self.sceneView.hitTest(touch, types: .existingPlane)
            guard let hitPlane = hitTestPlane.first else { return }
            hitNode.position = SCNVector3(hitPlane.worldTransform.columns.3.x,
                                           hitNode.position.y,
                                           hitPlane.worldTransform.columns.3.z)

Убедитесь, что вы отменили выбор узла, когда палец убран с экрана.

        // Runs when finger is removed from screen. Only once.
        } else if recognizer.state == .ended || recognizer.state == .cancelled || recognizer.state == .failed{

            guard let hitNode = self.selectedNode else { return }

            // Undo selection
            self.selectedNode = nil
        }
    }
0 голосов
/ 21 мая 2019

Краткий ответ: Чтобы получить этот красивый и плавный эффект перетаскивания, как в демонстрационном проекте Apple, вам нужно будет сделать это, как в демонстрационном проекте Apple (Handling 3D Interaction). С другой стороны, я согласен с вами, что код может сбить с толку, если вы посмотрите на него в первый раз. Нелегко вычислить правильное движение для объекта, размещенного на плоскости пола - всегда и из любого места или угла обзора. Это сложная конструкция кода, которая выполняет этот превосходный эффект перетаскивания. Apple проделала большую работу, чтобы добиться этого, но не стала для нас слишком легкой.

Полный ответ: Сокращение шаблона AR Interaction для ваших нуждающихся результатов в кошмаре - но должно работать, если вы потратите достаточно времени. Если вы предпочитаете начинать с нуля, в основном начните использовать обычный шаблон быстрого кода ARKit / SceneKit Xcode (содержащий космический корабль).

Вам также потребуется весь проект AR Interaction Template Project от Apple. (Ссылка включена в вопрос SO) В конце вы должны быть в состоянии перетащить нечто под названием VirtualObject, который на самом деле является специальным SCNNode. Кроме того, у вас будет красивый Квадрат Фокуса, который может быть полезен для любых целей - например, для первоначального размещения объектов или добавления пола или стены. (Некоторый код для эффекта перетаскивания и использования квадрата фокуса можно объединить или связать вместе - сделать это без квадрата фокуса будет на самом деле более сложно)

Начало работы: Скопируйте следующие файлы из шаблона взаимодействия 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 ***

Добавьте код Focus Square

// 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 COPY! вся функция 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 ***
}

И, наконец, добавьте некоторые вспомогательные функции для Focus Square

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
}

Уничтожить или заменить весь этот блок этой единственной строкой кода:

simdPosition = cameraWorldPosition + positionOffsetFromCamera

На этом этапе вы сможете скомпилировать проект и запустить его на устройстве. Вы должны увидеть космический корабль и желтый квадрат фокуса, который уже должен работать.

Чтобы начать размещать объект, который вы можете перетащить, вам нужна какая-то функция для создания так называемого VirtualObject, как я уже говорил в начале.

Используйте этот пример функции для проверки (добавьте ее где-нибудь в контроллер вида):

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, например, для загрузки сцен по имени - полезно при ссылках на импортированные модели)

Веселитесь!

0 голосов
/ 28 февраля 2019

Вроде поздний ответ, но я знаю, что у меня тоже были проблемы с этим. В конце концов я нашел способ сделать это, выполняя два отдельных теста попаданий при каждом вызове моего распознавателя жестов.

Сначала я выполняю тест на удар для своего 3d-объекта, чтобы определить, нажимаю ли я в данный момент на объекте или нет (как вы могли бы получить результаты для нажатия точек, плоскостей и т. Д., Если вы не укажете какие-либо параметры). Я делаю это, используя .categoryBitMask значение SCNHitTestOption. Имейте в виду, что вы должны назначить правильное значение .categoryBitMask вашему объектному узлу и всем его дочерним узлам заранее, чтобы сработал тест попадания. Я объявляю перечисление, которое могу использовать для этого:

enum BodyType : Int {
    case ObjectModel = 2;
}

Как видно из ответа на мой вопрос о .categoryBitMask значениях, которые я разместил здесь , важно учитывать, какие значения вы присваиваете своей битовой маске.

Ниже приведен код, который я использую в сочетании с UILongPressGestureRecognizer в Чтобы выбрать объект, который я сейчас нажимаю:

guard let recognizerView = recognizer.view as? ARSCNView else { return }

let touch = recognizer.location(in: recognizerView)

let hitTestResult = self.sceneView.hitTest(touch, options: [SCNHitTestOption.categoryBitMask: BodyType.ObjectModel.rawValue])
guard let modelNodeHit = hitTestResult.first?.node else { return }

После этого я выполняю 2-й тест на попадание, чтобы найти самолет, на который я нажимаю. Вы можете использовать тип .existingPlaneUsingExtent, если не хотите перемещать свой объект дальше, чем край плоскости, или .existingPlane, если хотите бесконечно перемещать свой объект вдоль обнаруженной плоской поверхности.

 var planeHit : ARHitTestResult!

 if recognizer.state == .changed {

     let hitTestPlane = self.sceneView.hitTest(touch, types: .existingPlane)
     guard hitTestPlane.first != nil else { return }
     planeHit = hitTestPlane.first!
     modelNodeHit.position = SCNVector3(planeHit.worldTransform.columns.3.x,modelNodeHit.position.y,planeHit.worldTransform.columns.3.z)

 }else if recognizer.state == .ended || recognizer.state == .cancelled || recognizer.state == .failed{

     modelNodeHit.position = SCNVector3(planeHit.worldTransform.columns.3.x,modelNodeHit.position.y,planeHit.worldTransform.columns.3.z)

 }

Я сделал GitHub репо , когда попробовал это, также экспериментируя с ARAnchors. Вы можете проверить это, если хотите увидеть мой метод на практике, но я не сделал это с намерением использовать его еще кем-то, так что он совершенно незакончен. Кроме того, ветка разработки должна поддерживать некоторые функциональные возможности для объекта с большим количеством дочерних узлов.

РЕДАКТИРОВАТЬ: ==================================

Для пояснения, если вы хотите использовать объект .scn вместо обычной геометрии, вам нужно перебирать все дочерние узлы объекта при его создании, устанавливая битовую маску для каждого дочернего элемента следующим образом:

 let objectModelScene = SCNScene(named:
        "art.scnassets/object/object.scn")!
 let objectNode =  objectModelScene.rootNode.childNode(
        withName: "theNameOfTheParentNodeOfTheObject", recursively: true)
 objectNode.categoryBitMask = BodyType.ObjectModel.rawValue
 objectNode.enumerateChildNodes { (node, _) in
        node.categoryBitMask = BodyType.ObjectModel.rawValue
    }

Затем в распознавателе жестов после получения hitTestResult

let hitTestResult = self.sceneView.hitTest(touch, options: [SCNHitTestOption.categoryBitMask: BodyType.ObjectModel.rawValue])

вам нужно найти родительский узел, так как в противном случае вы могли бы перемещать отдельный дочерний узел, который вы только что нажали. Сделайте это, выполнив рекурсивный поиск вверх по дереву узлов только что найденного узла.

guard let objectNode = getParentNodeOf(hitTestResult.first?.node) else { return }

где вы объявляете метод getParentNode следующим образом

func getParentNodeOf(_ nodeFound: SCNNode?) -> SCNNode? { 
    if let node = nodeFound {
        if node.name == "theNameOfTheParentNodeOfTheObject" {
            return node
        } else if let parent = node.parent {
            return getParentNodeOf(parent)
        }
    }
    return nil
}

Тогда вы можете выполнять любые операции над objectNode, так как это будет родительский узел вашего объекта .scn, что означает, что любое преобразование, примененное к нему, будет также применяться к дочерним узлам.

...