How to Get the Yaw, Pitch, Roll of an Aranchor in Absolute Terms

How can I get the yaw, pitch, roll of an ARAnchor in absolute terms?

I've figured it out...

Once you have the face anchor, some calculations need to happen with its transform matrix, and the camera's transform.

Like this:

let arFrame = session.currentFrame!
guard let faceAnchor = arFrame.anchors[0] as? ARFaceAnchor else { return }

let projectionMatrix = arFrame.camera.projectionMatrix(for: .portrait, viewportSize: self.sceneView.bounds.size, zNear: 0.001, zFar: 1000)
let viewMatrix = arFrame.camera.viewMatrix(for: .portrait)

let projectionViewMatrix = simd_mul(projectionMatrix, viewMatrix)
let modelMatrix = faceAnchor.transform
let mvpMatrix = simd_mul(projectionViewMatrix, modelMatrix)

// This allows me to just get a .x .y .z rotation from the matrix, without having to do crazy calculations
let newFaceMatrix = SCNMatrix4.init(mvpMatrix)
let faceNode = SCNNode()
faceNode.transform = newFaceMatrix
let rotation = vector_float3(faceNode.worldOrientation.x, faceNode.worldOrientation.y, faceNode.worldOrientation.z)

rotation.x .y and .z will return the face's pitch, yaw, roll (respectively)

I'm adding a small multiplier and inverting 2 of the axis, so it ends up like this:

yaw = -rotation.y*3
pitch = -rotation.x*3
roll = rotation.z*1.5

Phew!

Extract yaw/pitch/roll from faceAnchor in ARKit

As there is apparently not (yet) a function provided by Apple for that purpose, it is a priori required to implement quaternion to euler angles computation. With these mathematical resources, and a radian-to-degrees conversion function, this can be implemented as an extension, as follows:

extension matrix_float4x4 {
// Function to convert rad to deg
func radiansToDegress(radians: Float32) -> Float32 {
return radians * 180 / (Float32.pi)
}
var translation: SCNVector3 {
get {
return SCNVector3Make(columns.3.x, columns.3.y, columns.3.z)
}
}
// Retrieve euler angles from a quaternion matrix
var eulerAngles: SCNVector3 {
get {
// Get quaternions
let qw = sqrt(1 + self.columns.0.x + self.columns.1.y + self.columns.2.z) / 2.0
let qx = (self.columns.2.y - self.columns.1.z) / (qw * 4.0)
let qy = (self.columns.0.z - self.columns.2.x) / (qw * 4.0)
let qz = (self.columns.1.x - self.columns.0.y) / (qw * 4.0)

// Deduce euler angles
/// yaw (z-axis rotation)
let siny = +2.0 * (qw * qz + qx * qy)
let cosy = +1.0 - 2.0 * (qy * qy + qz * qz)
let yaw = radiansToDegress(radians:atan2(siny, cosy))
// pitch (y-axis rotation)
let sinp = +2.0 * (qw * qy - qz * qx)
var pitch: Float
if abs(sinp) >= 1 {
pitch = radiansToDegress(radians:copysign(Float.pi / 2, sinp))
} else {
pitch = radiansToDegress(radians:asin(sinp))
}
/// roll (x-axis rotation)
let sinr = +2.0 * (qw * qx + qy * qz)
let cosr = +1.0 - 2.0 * (qx * qx + qy * qy)
let roll = radiansToDegress(radians:atan2(sinr, cosr))

/// return array containing ypr values
return SCNVector3(yaw, pitch, roll)
}
}
}

AREnvironmentProbeAnchor are not present in ARFrame anchors

You should use if-statement inside an instance method renderer(_:didAdd:for:), for example. Any ARAnchor must belong to SCNNode. Try this code to find out how it works (sorry, it's in Swift not in Objective-C):

import UIKit
import SceneKit
import ARKit

class ViewController: UIViewController, ARSCNViewDelegate {

@IBOutlet var sceneView: ARSCNView!

override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self
let scene = SCNScene(named: "art.scnassets/yourScene.scn")!

let sphereNode = SCNNode(geometry: SCNSphere(radius: 0.05))
sphereNode.position = SCNVector3(x: 0, y: -0.5, z: -1)
let reflectiveMaterial = SCNMaterial()
reflectiveMaterial.lightingModel = .physicallyBased
reflectiveMaterial.metalness.contents = 1.0
reflectiveMaterial.roughness.contents = 0
sphereNode.geometry?.firstMaterial = reflectiveMaterial

let moveLeft = SCNAction.moveBy(x: 0.25, y: 0, z: 0.25, duration: 2)
moveLeft.timingMode = .easeInEaseOut;
let moveRight = SCNAction.moveBy(x: -0.25, y: 0, z: -0.25, duration: 2)
moveRight.timingMode = .easeInEaseOut;
let moveSequence = SCNAction.sequence([moveLeft, moveRight])
let moveLoop = SCNAction.repeatForever(moveSequence)
sphereNode.runAction(moveLoop)

sceneView.scene = scene
sceneView.scene.rootNode.addChildNode(sphereNode)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let configuration = ARWorldTrackingConfiguration()
configuration.environmentTexturing = .automatic
sceneView.session.run(configuration)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
sceneView.session.pause()
}
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
guard anchor is AREnvironmentProbeAnchor else {
print("Environment anchor is NOT FOUND")
return
}
print("It's", anchor.isKind(of: AREnvironmentProbeAnchor.self))
}
}

Result:

// It's true

Hope this helps.

How do I programmatically move an ARAnchor?

You don't need to move anchors. (hand wave) That's not the API you're looking for...

Adding ARAnchor objects to a session is effectively about "labeling" a point in real-world space so that you can refer to it later. The point (1,1,1) (for example) is always the point (1,1,1) — you can't move it someplace else because then it's not the point (1,1,1) anymore.

To make a 2D analogy: anchors are reference points sort of like the bounds of a view. The system (or another piece of your code) tells the view where it's boundaries are, and the view draws its content relative to those boundaries. Anchors in AR give you reference points you can use for drawing content in 3D.

What you're asking is really about moving (and animating the movement of) virtual content between two points. And ARKit itself really isn't about displaying or animating virtual content — there are plenty of great graphics engines out there, so ARKit doesn't need to reinvent that wheel. What ARKit does is provide a real-world frame of reference for you to display or animate content using an existing graphics technology like SceneKit or SpriteKit (or Unity or Unreal, or a custom engine built with Metal or GL).


Since you mentioned trying to do this with SpriteKit... beware, it gets messy. SpriteKit is a 2D engine, and while ARSKView provides some ways to shoehorn a third dimension in there, those ways have their limits.

ARSKView automatically updates the xScale, yScale, and zRotation of each sprite associated with an ARAnchor, providing the illusion of 3D perspective. But that applies only to nodes attached to anchors, and as noted above, anchors are static.

You can, however, add other nodes to your scene, and use those same properties to make those nodes match the ARSKView-managed nodes. Here's some code you can add/replace in the ARKit/SpriteKit Xcode template project to do that. We'll start with some basic logic to run a bouncing animation on the third tap (after using the first two taps to place anchors).

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

Then, some SpriteKit fun for making that animation happen:

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

Here's a video so you can see this code in action. You'll notice that the illusion only works as long as you don't move the camera — not so great for AR. When using SKAction, we can't adjust the start/end states of the animation while animating, so the ball keeps bouncing back and forth between its original (screen-space) positions/rotations/scales.

You could do better by animating the ball directly, but it's a lot of work. You'd need to, on every frame (or every view(_:didUpdate:for:) delegate callback):

  1. Save off the updated position, rotation, and scale values for the anchor-based nodes at each end of the animation. You'll need to do this twice per didUpdate callback, because you'll get one callback for each node.

  2. Work out position, rotation, and scale values for the node being animated, by interpolating between the two endpoint values based on the current time.

  3. Set the new attributes on the node. (Or maybe animate it to those attributes over a very short duration, so it doesn't jump too much in one frame?)

That's kind of a lot of work to shoehorn a fake 3D illusion into a 2D graphics toolkit — hence my comments about SpriteKit not being a great first step into ARKit.


If you want 3D positioning and animation for your AR overlays, it's a lot easier to use a 3D graphics toolkit. Here's a repeat of the previous example, but using SceneKit instead. Start with the ARKit/SceneKit Xcode template, take the spaceship out, and paste the same touchesBegan function from above into the ViewController. (Change the as ARSKView casts to as ARSCNView, too.)

Then, some quick code for placing 2D billboarded sprites, matching via SceneKit the behavior of the ARKit/SpriteKit template:

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

Finally, adding the animation for the bouncing ball:

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

This time the animation code is a lot shorter than the SpriteKit version.
Here's how it looks in action.

Because we're working in 3D to start with, we're actually animating between two 3D positions — unlike in the SpriteKit version, the animation stays where it's supposed to. (And without the extra work for directly interpolating and animating attributes.)



Related Topics



Leave a reply



Submit