ARKit cannot rotate a SCNNode correctly on a vertical plane - swift

I want to rotate a SCNNode, which is a painting image.
I can rotate it on the floor, but I cannot rotate it correctly on wall.
(I am using UIRotationGestureRecognizer)
...
func addPainting(_ hitResult: ARHitTestResult, _ grid: Grid) {
...
// Add the painting
let newPaintingNode = SCNNode(geometry: planeGeometry)
newPaintingNode.transform = SCNMatrix4(hitResult.anchor!.transform)
newPaintingNode.eulerAngles = SCNVector3(newPaintingNode.eulerAngles.x + (-Float.pi / 2), newPaintingNode.eulerAngles.y, newPaintingNode.eulerAngles.z)
newPaintingNode.position = SCNVector3(hitResult.worldTransform.columns.3.x, hitResult.worldTransform.columns.3.y, hitResult.worldTransform.columns.3.z)
self.paintingNode = newPaintingNode
self.currentAngleY = newPaintingNode.eulerAngles.y
self.currentAngleZ = newPaintingNode.eulerAngles.z
augmentedRealityView.scene.rootNode.addChildNode(self.paintingNode!)
grid.removeFromParentNode()
}
...
#objc func rotateNode(_ gesture: UIRotationGestureRecognizer){
if let currentNode = self.paintingNode {
//1. Get The Current Rotation From The Gesture
let rotation = Float(gesture.rotation)
if self.paintingAlignment == "horizontal" {
log.verbose("rotate horizontal!")
//2. If The Gesture State Has Changed Set The Nodes EulerAngles.y
if gesture.state == .changed {
currentNode.eulerAngles.y = currentAngleY + rotation
}
//3. If The Gesture Has Ended Store The Last Angle Of The Cube
if(gesture.state == .ended) {
currentAngleY = currentNode.eulerAngles.y
}
} else if self.paintingAlignment == "vertical" {
log.verbose("rotate vertical!")
//2. If The Gesture State Has Changed Set The Nodes EulerAngles.z
if gesture.state == .changed {
currentNode.eulerAngles.z = currentAngleZ + rotation
}
//3. If The Gesture Has Ended Store The Last Angle Of The Cube
if(gesture.state == .ended) {
currentAngleZ = currentNode.eulerAngles.z
}
}
}
}
Does anyone know how can I rotate it correctly on wall? Thank you!

You're using eulerAngles instance property in your code:
var eulerAngles: SCNVector3 { get set }
According to Apple documentation:
SceneKit applies eulerAngles rotations relative to the node’s pivot property in the reverse order of the components: first roll (Z), then yaw (Y), then pitch (X).
...but three-component rotation can lead to Gimbal Lock.
Gimbal lock is the loss of one degree of freedom in a three-dimensional, three-gimbal mechanism that occurs when the axes of two of the three gimbals are driven into a parallel configuration, "locking" the system into rotation in a degenerate two-dimensional space.
So you need to use a four-component rotation property:
var rotation: SCNVector4 { get set }
The four-component rotation vector specifies the direction of the rotation axis in the first three components (XYZ) and the angle of rotation, expressed in radians, in the fourth (W).
currentNode.rotation = SCNVector4(x: 1,
y: 0,
z: 0,
w: -Float.pi / 2)
If you want to know more about SCNVector4 structure and four-component rotation (and its W component expressed in radians) look at THIS POST and THIS POST.
P.S.
In RealityKit framework instead of SCNVector4 structure you need to use SIMD4<Float> generic structure or simd_float4 type alias.
var rotation: simd_float4 { get set }

I tried SCNNode eulerAngles and SCNNode rotation and I have no luck...
I guess that is because I invoked SCNNode.transform() when I add the painting. When later I modify SCNNode.eulerAngles and invoke SCNNode.rotation(), they may have influence on the first transform.
I finally get it working using SCNMatrix4Rotate, not using eulerAngles and rotation.
func addPainting(_ hitResult: ARHitTestResult, _ grid: Grid) {
...
let newPaintingNode = SCNNode(geometry: planeGeometry)
newPaintingNode.transform = SCNMatrix4(hitResult.anchor!.transform)
newPaintingNode.transform = SCNMatrix4Rotate(newPaintingNode.transform, -Float.pi / 2.0, 1.0, 0.0, 0.0)
newPaintingNode.position = SCNVector3(hitResult.worldTransform.columns.3.x, hitResult.worldTransform.columns.3.y, hitResult.worldTransform.columns.3.z)
self.paintingNode = newPaintingNode
...
}
...
#objc func rotateNode(_ gesture: UIRotationGestureRecognizer){
if let _ = self.paintingNode {
// Get The Current Rotation From The Gesture
let rotation = Float(gesture.rotation)
if gesture.state == .began {
self.rotationZ = rotation
}
if gesture.state == .changed {
let diff = rotation - self.rotationZ
self.paintingNode?.transform = SCNMatrix4Rotate(self.paintingNode!.transform, diff, 0.0, 0.0, 1.0)
self.rotationZ = rotation
}
}
}
And the above code works on both vertical and horizontal plane.

Related

How to rotate and lock the rotation of a SCNNode by degrees?

I need to rotate a model that can be freely rotated, to exact degrees, regardless of how many times it's been rotated.
I have a UIPanGestureRecognizer that is rotating freely a 3D model around the Y axis. However I'm struggling to get it to lock to a integer degree when panning is stopped, and I'm struggling with being able to know it's rotation in degrees from 0-359.
let translation = recognizer.translation(in: self.view)
var newAngleY = Double(translation.x) * (Double.pi) / 180.0
newAngleY += self.currentAngle
self.shipNode?.eulerAngles.y = Float(newAngleY)
if (recognizer.state == .ended)
{
self.currentAngle = newAngleY
}
It rotates freely, but all attempts for locking to the closest exact degree, and being able to 'know' it's rotational degree in a value from 0-359.
I know that:
let degrees = newAngleY * ( 180 / Double.pi)
And I know that if degrees > 360 then -= 360 (pseudo code)
However, whilst the UIPanGestureRecognizer is doing it's thing, these checks seem to fail and I don't know why. Is it because when it's still being panned, you can't edit the private properties of the ViewController?
You can edit the value while the gesture is occurring.
Quite a few options, so this seems the simplest to start with:
You could try only applying euler when the state changes AND only when .x > .x * (some value, such as 1.1). This would provide a more "snap to" kind of approach, something like:
var currentLocation = CGPoint.zero
var beginLocation = CGPoint.zero
#objc func handlePan(recognizer: UIPanGestureRecognizer) {
currentLocation = recognizer.location(in: gameScene)
var newAngleY = Double(translation.x) * (Double.pi) / 180.0
newAngleY += self.currentAngle
switch recognizer.state
{
case UIGestureRecognizer.State.began: break
case UIGestureRecognizer.State.changed:
if(currentLocation.x > beginLocation.x * 1.1)
{
gNodes.bnode.eulerAngles.y = Float(newAngleY)
beginLocation.x = currentLocation.x
}
if(currentLocation.x < beginLocation.x * 0.9) { .etc. }
break
case UIGestureRecognizer.State.ended:
gNodes.bnode.eulerAngles.y = Float(newAngleY)
break
}
}
Then you could switch to an SCNAction (changing your math) to give more control, such as
let vAction = SCNAction.rotateTo(x: 0, y: vAmount, z: 0, duration: 0)
bnode.runAction(vAction)

Apply multiple rotations to ARKit SCNNode

I'm working with an AR app that uses Apple's Focus Square to guide user interaction on both vertical and horizontal surfaces. However, it's only designed to work on horizontal surfaces. I have modified the method that updates the square's rotation to account for vertical surfaces as well. However, using the method that they used only one rotation can be performed at a time, whereas in order to have the square positioned properly on a horizontal surface I need to have two rotations performed (one to apply the original yaw correction, and one to pitch the square vertically).
The relevant code is below, but the specific changes I made are only at the beginning and end.
// MARK: - Appearence
func update(for position: float3, planeAnchor: ARPlaneAnchor?, camera: ARCamera?) {
lastPosition = position
let thisAlignment = planeAnchor?.alignment ?? .horizontal
if thisAlignment != lastAlignment {
lastAlignment = thisAlignment
print("alignmentChanged")
recentFocusSquarePositions = []
}
if let anchor = planeAnchor {
close(flash: !anchorsOfVisitedPlanes.contains(anchor))
lastPositionOnPlane = position
anchorsOfVisitedPlanes.insert(anchor)
} else {
open()
}
updateTransform(for: position, alignment: thisAlignment, camera: camera)
}
private func updateTransform(for position: float3, alignment: ARPlaneAnchor.Alignment, camera: ARCamera?) {
// add to list of recent positions
recentFocusSquarePositions.append(position)
// remove anything older than the last 8
recentFocusSquarePositions.keepLast(8)
// move to average of recent positions to avoid jitter
if let average = recentFocusSquarePositions.average {
self.simdPosition = average
self.setUniformScale(scaleBasedOnDistance(camera: camera))
}
// Correct y rotation of camera square
if let camera = camera {
let tilt = abs(camera.eulerAngles.x)
let threshold1: Float = .pi / 2 * 0.65
let threshold2: Float = .pi / 2 * 0.75
let yaw = atan2f(camera.transform.columns.0.x, camera.transform.columns.1.x)
var angle: Float = 0
switch tilt {
case 0..<threshold1:
angle = camera.eulerAngles.y
case threshold1..<threshold2:
let relativeInRange = abs((tilt - threshold1) / (threshold2 - threshold1))
let normalizedY = normalize(camera.eulerAngles.y, forMinimalRotationTo: yaw)
angle = normalizedY * (1 - relativeInRange) + yaw * relativeInRange
default:
angle = yaw
}
if alignment == .vertical{
self.rotation = SCNVector4(0, 1, 1, Float.pi)
} else {
self.rotation = SCNVector4(0, 1, 0, angle)
}
}
}
My goal is to have both the rotations in the control statement at the end performed on the object. If the else statement is removed, only that rotation is applied and the square remains horizontal. I have tried making both modifications to the transform directly using SCNMatrix4MakeRotation(_:_:_:_:) but this caused the square to spin endlessly and position itself incorrectly. I also tried adjusting the node's Pivot but this resulted in no visible changes to the object.

Placing an object in front of camera at Touch Location

The following code places the node in front of the camera but always at the center 10cm away from the camera position. I want to place the node 10cm away in z-direction but at the x and y co-ordinates of where I touch the screen. So touching on different parts of the screen should result in a node being placed 10cm away in front of the camera but at the x and y location of the touch and not always at the center.
var cameraRelativePosition = SCNVector3(0,0,-0.1)
let sphere = SCNNode()
sphere.geometry = SCNSphere(radius: 0.0025)
sphere.geometry?.firstMaterial?.diffuse.contents = UIColor.white
Service.addChildNode(sphere, toNode: self.sceneView.scene.rootNode,
inView: self.sceneView, cameraRelativePosition:
cameraRelativePosition)
Service.swift
class Service: NSObject {
static func addChildNode(_ node: SCNNode, toNode: SCNNode, inView:
ARSCNView, cameraRelativePosition: SCNVector3) {
guard let currentFrame = inView.session.currentFrame else { return }
let camera = currentFrame.camera
let transform = camera.transform
var translationMatrix = matrix_identity_float4x4
translationMatrix.columns.3.x = cameraRelativePosition.x
translationMatrix.columns.3.y = cameraRelativePosition.y
translationMatrix.columns.3.z = cameraRelativePosition.z
let modifiedMatrix = simd_mul(transform, translationMatrix)
node.simdTransform = modifiedMatrix
toNode.addChildNode(node)
}
}
The result should look exactly like this : https://justaline.withgoogle.com
We can use the unprojectPoint(_:) method of SCNSceneRenderer (SCNView and ARSCNView both conform to this protocol) to convert a point on the screen to a 3D point.
When tapping the screen we can calculate a ray this way:
func getRay(for point: CGPoint, in view: SCNSceneRenderer) -> SCNVector3 {
let farPoint = view.unprojectPoint(SCNVector3(Float(point.x), Float(point.y), 1))
let nearPoint = view.unprojectPoint(SCNVector3(Float(point.x), Float(point.y), 0))
let ray = SCNVector3Make(farPoint.x - nearPoint.x, farPoint.y - nearPoint.y, farPoint.z - nearPoint.z)
// Normalize the ray
let length = sqrt(ray.x*ray.x + ray.y*ray.y + ray.z*ray.z)
return SCNVector3Make(ray.x/length, ray.y/length, ray.z/length)
}
The ray has a length of 1, so by multiplying it by 0.1 and adding the camera location we get the point you were searching for.

SCNNode facing towards the camera

I am trying to put SCNCylinder node in the scene on the touch point. I always want to show the cylinder shape diameter facing towards camera. Its working fine for horizontal scene but it have a problem in vertical scene. In vertical scene I can see the cylinder sides but I want to show the full diameter facing towards the camera no matter whats the camera orientation is. I know there is some transformation needs to be applied depending on the camera transform but don't know how. I am not using plane detection its the simple node which is directly added to the scene.
Vertical Image:
Horizontal Image:
The code to insert the node is as follows,
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {
return
}
let result = sceneView.hitTest(touch.location(in: sceneView), types: [ARHitTestResult.ResultType.featurePoint])
guard let hitResult = result.last else {
print("returning because couldn't find the touch point")
return
}
let hitTransform = SCNMatrix4(hitResult.worldTransform)
let position = SCNVector3Make(hitTransform.m41, hitTransform.m42, hitTransform.m43)
let ballShape = SCNCylinder(radius: 0.02, height: 0.01)
let ballNode = SCNNode(geometry: ballShape)
ballNode.position = position
sceneView.scene.rootNode.addChildNode(ballNode)
}
Any help would be appreciated.
I'm not certain this is the right way to handle what you need but here is something which may help you.
I think CoreMotion could be useful to help you determine if the device is at a horizontal or vertical angle.
This class has a property called attitude, which describes the rotation of our device in terms of roll, pitch, and yaw. If we are holding our phone in portrait orientation, the roll describes the angle of rotation about the axis that runs through the top and bottom of the phone. The pitch describes the angle of rotation about the axis that runs through the sides of your phone (where the volume buttons are). And finally, the yaw describes the angle of rotation about the axis that runs through the front and back of your phone. With these three values, we can determine how the user is holding their phone in reference to what would be level ground (Stephan Baker).
Begin by importing CoreMotion:
import CoreMotion
Then create the following variables:
let deviceMotionDetector = CMMotionManager()
var currentAngle: Double!
We will then create a function which will check the angle of our device like so:
/// Detects The Angle Of The Device
func detectDeviceAngle(){
if deviceMotionDetector.isDeviceMotionAvailable == true {
deviceMotionDetector.deviceMotionUpdateInterval = 0.1;
let queue = OperationQueue()
deviceMotionDetector.startDeviceMotionUpdates(to: queue, withHandler: { (motion, error) -> Void in
if let attitude = motion?.attitude {
DispatchQueue.main.async {
let pitch = attitude.pitch * 180.0/Double.pi
self.currentAngle = pitch
print(pitch)
}
}
})
}
else {
print("Device Motion Unavailable");
}
}
This only needs to be called once for example in viewDidLoad:
detectDeviceAngle()
In your touchesBegan method you can add this to the end:
//1. If We Are Holding The Device Above 60 Degress Change The Node
if currentAngle > 60 {
//2a. Get The X, Y, Z Values Of The Desired Rotation
let rotation = SCNVector3(1, 0, 0)
let vector3x = rotation.x
let vector3y = rotation.y
let vector3z = rotation.z
let degreesToRotate:Float = 90
//2b. Set The Position & Rotation Of The Object
sphereNode.rotation = SCNVector4Make(vector3x, vector3y, vector3z, degreesToRotate * 180 / .pi)
}else{
}
I am sure there are better ways to achieve what you need (and I would be very interested in hearing them too), but I hope it will get you started.
Here is the result:

How to flip a SKPriteNode upside down while being generated automatically

I have a SKSpriteNode generated at every 1 second on a floor. Sometime it's over and sometime under it. The SpriteNode is an image and I want it to be flipped upside down when its under the floor. I tried
SKAction.scaleYto(scale, duration: 0.1
But it doesn't work!
Here is the code
func startGeneratingWallsEvery(seconds: NSTimeInterval) {
generationTimer = NSTimer.scheduledTimerWithTimeInterval(seconds, target: self, selector: "generateWall", userInfo: nil, repeats: true)
}
func stopGenerating() {
generationTimer?.invalidate()
}
func generateWall(){
var scale: CGFloat
let wall = ADWall()
let rand = arc4random_uniform(2)
if rand == 0 {
scale = -1.0
} else {
scale = 1.0
}
//let translate1 = SKAction.moveByX(0, y: scale*(wall.size.height), duration: 0.1)
let flip = SKAction.scaleYTo(scale, duration: 0.1)
wall.position.x = size.width/2 + wall.size.width/2
wall.position.y = scale * (kADGroundHeight/2 + wall.size.height/2)
walls.append(wall)
addChild(wall)
runAction(flip)
}
Help me please!!
I can think of a couple of different strategies for causing the sprit to be generated upside down automatically when they are created:
The first strategy is to have two different images at hand, one which is vertically flipped, and one which is not. When the sprite is created, if it spawns below the floor, simply retexture the SKSpriteNode with:
wall.texture = SKTexture(imageNamed:"flipped_image")
You can use that same method to retexture the sprite again should it ever appear above the floor.
Rotate the image. If you want to keep the texturing simple and but a single image for the wall object, another method you could try is to rotate the image by 180 degress, like so:
if belowFloor == true {
zRotation = CGFloat(M_PI)
}
HTH!