web-dev-qa-db-fra.com

Comment déplacer par programme un ARAnchor?

J'essaie le nouvel ARKit pour remplacer une autre solution similaire que j'ai. C'est assez génial! Mais je n'arrive pas à comprendre comment déplacer un ARAnchor par programme. Je veux déplacer lentement l'ancre vers la gauche de l'utilisateur.

Création de l'ancre à 2 mètres devant l'utilisateur:

        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)

plus tard, déplacer l'objet vers la gauche/droite de l'utilisateur (axe x) ...

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

répétée toutes les 50 millisecondes (ou autre).

Ce qui précède ne fonctionne pas car la transformation est une propriété en lecture seule.

J'ai besoin d'un moyen de changer la position d'un objet AR dans l'espace par rapport à l'utilisateur de manière à garder l'expérience AR intacte - ce qui signifie que si vous déplacez votre appareil, l'objet AR se déplacera mais ne sera pas "coincé" "à la caméra comme si elle était simplement peinte, mais bouge comme si vous voyiez une personne bouger pendant que vous passiez - elle bouge et vous bougez et ça a l'air naturel.

Veuillez noter que la portée de cette question ne concerne que la façon de déplacer un objet dans l'espace par rapport à l'utilisateur (ARAnchor), pas par rapport à un avion (ARPlaneAnchor) ou à une autre surface détectée (ARHitTestResult).

Merci!

13
Ryan Pfister

Vous n'avez pas besoin de déplacer les ancres. (vague de main) Ce n'est pas l'API que vous recherchez ...

Ajouter des objets ARAnchor à une session revient à "étiqueter" un point dans l'espace réel afin que vous puissiez vous y référer plus tard. Le point (1,1,1) (Par exemple) est toujours le point (1,1,1) - vous ne pouvez plus le déplacer ailleurs car ce n'est plus le point (1,1,1).

Pour faire une analogie 2D: les ancres sont des points de référence un peu comme les limites d'une vue. Le système (ou une autre partie de votre code) indique à la vue où se trouvent ses limites, et la vue dessine son contenu par rapport à ces limites. Les ancres dans AR vous donnent des points de référence que vous pouvez utiliser pour dessiner du contenu en 3D.

Ce que vous demandez, c'est vraiment de déplacer (et d'animer le mouvement) du contenu virtuel entre deux points. Et ARKit lui-même ne consiste pas vraiment à afficher ou à animer du contenu virtuel - il existe de nombreux excellents moteurs graphiques, donc ARKit n'a pas besoin de réinventer cette roue. Ce que fait ARKit, c'est de fournir un cadre de référence du monde réel pour afficher ou animer du contenu en utilisant une technologie graphique existante comme SceneKit ou SpriteKit (ou Unity ou Unreal, ou un moteur personnalisé construit avec Metal ou GL).


Puisque vous avez mentionné essayer de le faire avec SpriteKit ... méfiez-vous, cela devient compliqué. SpriteKit est un moteur 2D, et tandis que ARSKView fournit quelques façons de chausse-pied une troisième dimension là-dedans, ces façons ont leurs limites.

ARSKView met automatiquement à jour les xScale, yScale et zRotation de chaque Sprite associé à un ARAnchor, fournissant l'illusion d'une perspective 3D. Mais cela ne s'applique qu'aux nœuds attachés aux ancres, et comme indiqué ci-dessus, les ancres sont statiques.

Vous pouvez cependant ajouter d'autres nœuds à votre scène et utiliser ces mêmes propriétés pour que ces nœuds correspondent aux nœuds gérés par ARSKView. Voici du code que vous pouvez ajouter/remplacer dans le projet de modèle ARKit/SpriteKit Xcode pour ce faire. Nous allons commencer avec une logique de base pour exécuter une animation de rebond sur le troisième tap (après avoir utilisé les deux premiers taps pour placer des ancres).

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's 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)
    }
}

Ensuite, du plaisir avec SpriteKit pour réaliser cette animation:

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)
}

Voici une vidéo pour que vous puissiez voir ce code en action. Vous remarquerez que l'illusion ne fonctionne que tant que vous ne bougez pas la caméra - pas si génial pour la réalité augmentée. Lorsque vous utilisez SKAction, nous ne pouvons pas ajuster les états de début/fin de l'animation pendant l'animation, de sorte que la balle continue de rebondir d'avant en arrière entre ses positions/rotations/échelles d'origine (espace d'écran).

Vous pourriez faire mieux en animant directement le ballon, mais c'est beaucoup de travail. Vous devriez, sur chaque trame (ou chaque view(_:didUpdate:for:) rappel délégué):

  1. Enregistrez les valeurs de position, de rotation et d'échelle mises à jour pour les nœuds basés sur les ancres à chaque extrémité de l'animation. Vous devrez effectuer cette opération deux fois par didUpdate rappel, car vous obtiendrez un rappel pour chaque nœud.

  2. Calculez les valeurs de position, de rotation et d'échelle pour le nœud en cours d'animation, en interpolant entre les deux valeurs de point final en fonction de l'heure actuelle.

  3. Définissez les nouveaux attributs sur le nœud. (Ou peut-être l'animer à ces attributs sur une très courte durée, afin qu'il ne saute pas trop dans une image?)

C'est beaucoup de travail pour chausse-pied une fausse illusion 3D dans une boîte à outils graphique 2D - d'où mes commentaires sur SpriteKit qui n'est pas un excellent premier pas dans ARKit.


Si vous souhaitez un positionnement et une animation 3D pour vos superpositions AR, il est beaucoup plus facile d'utiliser une boîte à outils graphiques 3D. Voici une répétition de l'exemple précédent, mais en utilisant SceneKit à la place. Commencez avec le modèle Xcode ARKit/SceneKit, retirez le vaisseau spatial et collez la même fonction touchesBegan d'en haut dans le ViewController. (Modifiez également les conversions de as ARSKView En as ARSCNView.)

Ensuite, un code rapide pour placer des sprites en panneau d'affichage 2D, correspondant via SceneKit au comportement du modèle 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)
}

Enfin, en ajoutant l'animation pour la balle rebondissante:

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)
}

Cette fois, le code d'animation est beaucoup plus court que la version SpriteKit. Voici à quoi cela ressemble en action.

Parce que nous travaillons en 3D pour commencer, nous animons en fait entre deux positions 3D - contrairement à la version SpriteKit, l'animation reste là où elle est censée. (Et sans le travail supplémentaire pour interpoler et animer directement les attributs.)

36
rickster