Как программно перемещать ARAnchor?

Я пытаюсь использовать новый ARKit для замены другого подобного решения. Это очень здорово! Но я не могу понять, как программно перемещать ARAnchor. Я хочу медленно перемещать якорь слева от пользователя.

Создание якоря на 2 метра перед пользователем:

        var translation = matrix_identity_float4x4
        translation.columns.3.z = -2.0
        let transform = simd_mul(currentFrame.camera.transform, translation)

        let anchor = ARAnchor(transform: transform)
        sceneView.session.add(anchor: anchor)

позже, перемещая объект влево/вправо пользователя (по оси x)...

anchor.transform.columns.3.x = anchor.transform.columns.3.x + 0.1

повторяется каждые 50 миллисекунд (или что-то еще).

Вышеприведенное не работает, потому что transform является свойством get-only.

Мне нужен способ изменить положение AR-объекта в пространстве относительно пользователя таким образом, чтобы сохранить AR-образность неповрежденным - это означает, что если вы перемещаете свое устройство, объект AR будет перемещаться, но также не будет "застрять" на фотоаппарате, как будто он просто нарисован, но движется, как будто вы увидите, как человек движется во время ходьбы - они движутся, и вы двигаетесь, и это выглядит естественно.

Обратите внимание, что область действия этого вопроса связана только с тем, как перемещать объект в пространстве по отношению к пользователю (ARAnchor), а не по отношению к плоскости (ARPlaneAnchor) или к другой обнаруженной поверхности (ARHitTestResult).

Спасибо!

Ответ 1

Вам не нужно перемещать якоря. (ручная волна) Это не тот API, который вы ищете...

Добавление объектов ARAnchor к сеансу эффективно касается "маркировки" точки в пространстве реального мира, чтобы вы могли ссылаться на нее позже. Точкой (1,1,1) (например) всегда является точка (1,1,1) - вы не можете ее перемещать где-то еще, потому что тогда это не точка (1,1,1).

Сделать двумерную аналогию: привязки - это ориентиры, подобные границам вида. Система (или другая часть вашего кода) сообщает о том, где находятся ее границы, и представление рисует его содержимое относительно этих границ. Якоря в AR дает вам ориентиры, которые вы можете использовать для рисования контента в 3D.

То, о чем вы спрашиваете, действительно касается перемещения (и анимации движения) виртуального контента между двумя точками. И сама ARKit действительно не касается отображения или анимации виртуального контента - там много отличных графических движков, поэтому ARKit не нужно изобретать это колесо. То, что ARKit делает, обеспечивает реальную систему отсчета для отображения или анимации контента с использованием существующих графических технологий, таких как SceneKit или SpriteKit (или Unity или Unreal, или настраиваемый движок, созданный с помощью Metal или GL).


Поскольку вы упомянули о попытке сделать это с помощью SpriteKit... будьте осторожны, это становится беспорядочным. SpriteKit - это 2D-движок, и в то время как ARSKView предоставляет некоторые способы подгонки третьего измерения, эти способы имеют свои пределы.

ARSKView автоматически обновляет xScale, yScale и zRotation каждого спрайта, связанного с ARAnchor, обеспечивая иллюзию трехмерной перспективы. Но это относится только к узлам, прикрепленным к якорям, и, как отмечено выше, привязки статичны.

Однако вы можете добавить другие узлы в свою сцену и использовать те же самые свойства, чтобы эти узлы соответствовали узлам ARSKView. Вот какой код вы можете добавить/заменить в проекте шаблона ARKit/SpriteKit Xcode для этого. Мы начнем с некоторой базовой логики, чтобы запустить анимацию подпрыгивания на третьем кране (после использования первых двух кранов для размещения якорей).

var anchors: [ARAnchor] = []
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

    // Start bouncing on touch after placing 2 anchors (don't allow more)
    if anchors.count > 1 {
        startBouncing(time: 1)
        return
    }
    // Create anchor using the camera current position
    guard let sceneView = self.view as? ARSKView else { return }
    if let currentFrame = sceneView.session.currentFrame {

        // Create a transform with a translation of 30 cm in front of the camera
        var translation = matrix_identity_float4x4
        translation.columns.3.z = -0.3
        let transform = simd_mul(currentFrame.camera.transform, translation)

        // Add a new anchor to the session
        let anchor = ARAnchor(transform: transform)
        sceneView.session.add(anchor: anchor)
        anchors.append(anchor)
    }
}

Затем некоторые эмоции SpriteKit для создания этой анимации:

var ballNode: SKLabelNode = {
    let labelNode  = SKLabelNode(text: "🏀")
    labelNode.horizontalAlignmentMode = .center
    labelNode.verticalAlignmentMode = .center
    return labelNode
}()
func startBouncing(time: TimeInterval) {
    guard
        let sceneView = self.view as? ARSKView,
        let first = anchors.first, let start = sceneView.node(for: first),
        let last = anchors.last, let end = sceneView.node(for: last)
        else { return }

    if ballNode.parent == nil {
        addChild(ballNode)
    }
    ballNode.setScale(start.xScale)
    ballNode.zRotation = start.zRotation
    ballNode.position = start.position

    let scale = SKAction.scale(to: end.xScale, duration: time)
    let rotate = SKAction.rotate(toAngle: end.zRotation, duration: time)
    let move = SKAction.move(to: end.position, duration: time)

    let scaleBack = SKAction.scale(to: start.xScale, duration: time)
    let rotateBack = SKAction.rotate(toAngle: start.zRotation, duration: time)
    let moveBack = SKAction.move(to: start.position, duration: time)

    let action = SKAction.repeatForever(.sequence([
        .group([scale, rotate, move]),
        .group([scaleBack, rotateBack, moveBack])
        ]))
    ballNode.removeAllActions()
    ballNode.run(action)
}

Здесь видео, чтобы вы могли видеть этот код в действии. Вы заметите, что иллюзия работает только до тех пор, пока вы не перемещаете камеру - это не так хорошо для AR. При использовании SKAction мы не можем корректировать начальные/конечные состояния анимации во время анимации, поэтому мяч продолжает прыгать назад и вперед между его исходными (экранными) позициями/вращениями/масштабами.

Вы могли бы добиться большего, просто используя анимацию шара, но это очень много. Вам нужно в каждом кадре (или каждом view(_:didUpdate:for:) делегировать обратный вызов):

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

  • Определить положение, поворот и значения шкалы для анимированного node путем интерполяции между двумя значениями конечных точек на основе текущего времени.

  • Установите новые атрибуты в node. (Или, возможно, оживить эти атрибуты в течение очень короткой продолжительности, чтобы он не перескакивал слишком много в одном кадре?)

Такая работа по созданию поддельной 3D-иллюзии в 2D-графическом инструменталитете - поэтому мои комментарии о SpriteKit не являются отличным первым шагом в ARKit.


Если вам нужны 3D-позиционирование и анимация для ваших AR-оверлей, гораздо проще использовать набор 3D-графики. Здесь повторение предыдущего примера, но вместо этого используется SceneKit. Начните с шаблона ARKit/SceneKit Xcode, вытащите космический корабль и вставьте ту же самую функцию touchesBegan сверху в ViewController. (Смените as ARSKView на as ARSCNView).

Затем, некоторый быстрый код для размещения 2D-афишированных спрайтов, сопоставляющий SceneKit с поведением шаблона ARKit/SpriteKit:

// in global scope
func makeBillboardNode(image: UIImage) -> SCNNode {
    let plane = SCNPlane(width: 0.1, height: 0.1)
    plane.firstMaterial!.diffuse.contents = image
    let node = SCNNode(geometry: plane)
    node.constraints = [SCNBillboardConstraint()]
    return node
}

// inside ViewController
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
    // emoji to image based on https://stackoverflow.com/a/41021662/957768
    let billboard = makeBillboardNode(image: "⛹️".image())
    node.addChildNode(billboard)
}

Наконец, добавив анимацию для прыгающего шара:

let ballNode = makeBillboardNode(image: "🏀".image())
func startBouncing(time: TimeInterval) {
    guard
        let sceneView = self.view as? ARSCNView,
        let first = anchors.first, let start = sceneView.node(for: first),
        let last = anchors.last, let end = sceneView.node(for: last)
        else { return }

    if ballNode.parent == nil {
        sceneView.scene.rootNode.addChildNode(ballNode)
    }

    let animation = CABasicAnimation(keyPath: #keyPath(SCNNode.transform))
    animation.fromValue = start.transform
    animation.toValue = end.transform
    animation.duration = time
    animation.autoreverses = true
    animation.repeatCount = .infinity
    ballNode.removeAllAnimations()
    ballNode.addAnimation(animation, forKey: nil)
}

На этот раз код анимации намного короче, чем версия SpriteKit. Вот как это выглядит в действии.

Поскольку мы работаем в 3D для начала, мы фактически анимации между двумя 3D-позициями - в отличие от версии SpriteKit анимация остается там, где она должна была. (И без дополнительной работы для непосредственного интерполяции и анимации атрибутов.)