I am trying to rotate SCNNode with UIRotationGestureRecognizer and changing of eulerAngles. I switch between rotation around y and around z axis. When I rotate only one of them everything is fine. But problem appears when I rotate both. It no longer rotate around axis itself but it rotates randomly.
I have read many answers especially from ARGeo like this one: SCNNode Z-rotation axis stays constant, while X and Y axes change when node is rotated , and I understand problem of eulerAngles and gimbal lock.
But I don't understand how to correctly use orientation ant that w component. Is here someone who successfully used UIRotationGestureRecognizer and orientation instead of eulerAngles?
Here's one of an examples how to implement UIRotationGestureRecognizer.
I copied it from How can I rotate an SCNNode.... SO post.
private var startingOrientation = GLKQuaternion.identity
private var rotationAxis = GLKVector3Make(0, 0, 0)
#objc private func handleRotation(_ rotation: UIRotationGestureRecognizer) {
guard let node = sceneView.hitTest(rotation.location(in: sceneView), options: nil).first?.node else {
return
}
if rotation.state == .began {
startingOrientation = GLKQuaternion(boxNode.orientation)
let cameraLookingDirection = sceneView.pointOfView!.parentFront
let cameraLookingDirectionInTargetNodesReference = boxNode.convertVector(cameraLookingDirection,
from: sceneView.pointOfView!.parent!)
rotationAxis = GLKVector3(cameraLookingDirectionInTargetNodesReference)
} else if rotation.state == .ended {
startingOrientation = GLKQuaternionIdentity
rotationAxis = GLKVector3Make(0, 0, 0)
} else if rotation.state == .changed {
let quaternion = GLKQuaternion(angle: Float(rotation.rotation), axis: rotationAxis)
node.orientation = SCNQuaternion((startingOrientation * quaternion).normalized())
}
}
Hope this helps.
Related
I am trying to make an app where the user enters some text in a textfield and then the app displays this text in front of the ar camera to the user. I positioned the text correctly in front of the camera and I have changed the anchor of the text to be in the center of the text. but when I add the text into the scene the text is rotated 90 degrees around the z-axis. And I know why but I don't know how to solve it. The reason is that the camera of the arscene.session has a rotation of 0 for all x, y, z when the device is in landscape but since I want my app to be in portrait I rotate the device 90 degrees which rotates the camera as well and since the text has the same camera rotation, it's rotated as well. I tried correcting the rotation of the text by rotating it again around the z-axis but that doesn't solve the entire issue because when I change the direction of my phone, that affects the camera axis which will affect different axis of the text(not the same axis because I rotated the axis in the correction step). so I think the only way to solve the issue is to rotate the camera to be in consistent with the portrait mode from the beginning but I haven't found any way to set the rotation of the camera
here is the code of adding the text:
private func createTextNode(text:String?)
{
guard let text = text else {return}
let arText = SCNText(string: text, extrusionDepth: 1)
arText.font = UIFont(name: arText.font.fontName, size: 2)
arText.firstMaterial?.diffuse.contents = selectedColor
//making the node
let node = SCNNode()
node.geometry = arText
center(node: node)
guard let currentFrame = sceneView.session.currentFrame else {return}
let camera = currentFrame.camera
let cameraTransform = camera.transform
var newTransform = matrix_identity_float4x4
newTransform.columns.3.z = -0.2
let modifiedTransform = matrix_multiply(cameraTransform, newTransform)
node.transform = SCNMatrix4(modifiedTransform)
node.scale = SCNVector3(0.02, 0.02, 0.02)
self.sceneView.scene.rootNode.addChildNode(node)
node.eulerAngles.x = 90.degrees
}
and that's how the output looks like..
output
any help will be appreciated
You cannot use the matrix identity for any orientation, it has to be rotated depending on device orientation. I have a function in my apps that I call to update that before I perform the matrix multiplication :
var translation = matrix_identity_float4x4
func updateTranslationMatrix() {
switch UIDevice.current.orientation{
case .portrait, .portraitUpsideDown, .unknown, .faceDown, .faceUp:
print("portrait ")
translation.columns.0.x = -cos(.pi/2)
translation.columns.0.y = sin(.pi/2)
translation.columns.1.x = -sin(.pi/2)
translation.columns.1.y = -cos(.pi/2)
case .landscapeLeft :
print("landscape left")
translation.columns.0.x = 1
translation.columns.0.y = 0
translation.columns.1.x = 0
translation.columns.1.y = 1
case .landscapeRight :
print("landscape right")
translation.columns.0.x = cos(.pi)
translation.columns.0.y = -sin(.pi)
translation.columns.1.x = sin(.pi)
translation.columns.1.y = cos(.pi)
}
translation.columns.3.z = -0.6 //60cm in front of the camera
}
If you mean that you don't want the orientation of the device to change along with the rotation, then:
Go to Project > General > Deployment Info
Under device orientation, uncheck all boxes except 'Portrait'.
If this does not solve your problem and what you really want is to fix the euler angles of your text node, let me know, I'll be happy to help.
I'm using ARKit to display 3D objects. I managed to place the nodes in the real world in front of the user (aka the camera). But I don't manage to make them to face the camera when I drop them.
let tap_point=CGPoint(x: x, y: y)
let results=arscn_view.hitTest(tap_point, types: .estimatedHorizontalPlane)
guard results.count>0 else{
return
}
guard let r=results.first else{
return
}
let hit_tf=SCNMatrix4(r.worldTransform)
let new_pos=SCNVector3Make(hit_tf.m41, hit_tf.m42+Float(0.2), hit_tf.m43)
guard let scene=SCNScene(named: file_name) else{
return
}
guard let node=scene.rootNode.childNode(withName: "Mesh", recursively: true) else{
return
}
node.position=new_pos
arscn_view.scene.rootNode.addChildNode(node)
The nodes are well positioned on the plane, in front of the camera. But they are all looking in the same direction. I guess I should rotate the SCNNode but I didn't manage to do this.
First, get the rotation matrix of the camera:
let rotate = simd_float4x4(SCNMatrix4MakeRotation(sceneView.session.currentFrame!.camera.eulerAngles.y, 0, 1, 0))
Then, combine the matrices:
let rotateTransform = simd_mul(r.worldTransform, rotate)
Lastly, apply a transform to your node, casting as SCNMatrix4:
node.transform = SCNMatrix4(rotateTransform)
Hope that helps
EDIT
here how you can create SCNMatrix4 from simd_float4x4
let rotateTransform = simd_mul(r.worldTransform, rotate)
node.transform = SCNMatrix4(m11: rotateTransform.columns.0.x, m12: rotateTransform.columns.0.y, m13: rotateTransform.columns.0.z, m14: rotateTransform.columns.0.w, m21: rotateTransform.columns.1.x, m22: rotateTransform.columns.1.y, m23: rotateTransform.columns.1.z, m24: rotateTransform.columns.1.w, m31: rotateTransform.columns.2.x, m32: rotateTransform.columns.2.y, m33: rotateTransform.columns.2.z, m34: rotateTransform.columns.2.w, m41: rotateTransform.columns.3.x, m42: rotateTransform.columns.3.y, m43: rotateTransform.columns.3.z, m44: rotateTransform.columns.3.w)
guard let frame = self.sceneView.session.currentFrame else {
return
}
node.eulerAngles.y = frame.camera.eulerAngles.y
here's my code for the SCNNode facing the camera..hope help for someone
let location = touches.first!.location(in: sceneView)
var hitTestOptions = [SCNHitTestOption: Any]()
hitTestOptions[SCNHitTestOption.boundingBoxOnly] = true
let hitResultsFeaturePoints: [ARHitTestResult] = sceneView.hitTest(location, types: .featurePoint)
let hitTestResults = sceneView.hitTest(location)
guard let node = hitTestResults.first?.node else {
if let hit = hitResultsFeaturePoints.first {
let rotate = simd_float4x4(SCNMatrix4MakeRotation(sceneView.session.currentFrame!.camera.eulerAngles.y, 0, 1, 0))
let finalTransform = simd_mul(hit.worldTransform, rotate)
sceneView.session.add(anchor: ARAnchor(transform: finalTransform))
}
return
}
Do you want the nodes to always face the camera, even as the camera moves? That's what SceneKit constraints are for. Either SCNLookAtConstraint or SCNBillboardConstraint can keep a node always pointing at the camera.
Do you want the node to face the camera when placed, but then hold still (so you can move the camera around and see the back of it)? There are a few ways to do that. Some involve fun math, but a simpler way to handle it might just be to design your 3D assets so that "front" is always in the positive Z-axis direction. Set a placed object's transform based on the camera transform, and its initial orientation will match the camera's.
Here's how I did it:
func faceCamera() {
guard constraints?.isEmpty ?? true else {
return
}
SCNTransaction.begin()
SCNTransaction.animationDuration = 5
SCNTransaction.completionBlock = { [weak self] in
self?.constraints = []
}
constraints = [billboardConstraint]
SCNTransaction.commit()
}
private lazy var billboardConstraint: SCNBillboardConstraint = {
let constraint = SCNBillboardConstraint()
constraint.freeAxes = [.Y]
return constraint
}()
As stated earlier a SCNBillboardConstraint will make the node always look at the camera. I am animating it so the node doesn't just immediately snap into place, this is optional. In the SCNTransaction.completionBlock I remove the constraint, also optional.
Also I set the SCNBillboardConstraint's freeAxes, which customizes on what axis the node follows the camera, again optional.
I want the node to face the camera when I place it then keep it here (and be able to move around). – Marie Dm
Blockquote
You can put object facing to camera, using this:
if let rotate = sceneView.session.currentFrame?.camera.transform {
node.simdTransform = rotate
}
This code will save you from gimbal lock and other troubles.
The four-component rotation vector specifies the direction of the rotation axis in the first three components and the angle of rotation (in radians) in the fourth. The default rotation is the zero vector, specifying no rotation. Rotation is applied relative to the node’s simdPivot property.
The simdRotation, simdEulerAngles, and simdOrientation properties all affect the rotational aspect of the node’s simdTransform property. Any change to one of these properties is reflected in the others.
https://developer.apple.com/documentation/scenekit/scnnode/2881845-simdrotation
https://developer.apple.com/documentation/scenekit/scnnode/2881843-simdtransform
Currently I am confused in terms of making a functionality of 3D compass. With SceneKit, I loaded an arrow object and then fetched sensor data like the code below (The effect is this video)
let motionManager = CMMotionManager()
motionManager.deviceMotionUpdateInterval = 1.0/60.0
if motionManager.isDeviceMotionAvailable {
motionManager.startDeviceMotionUpdates(to: OperationQueue.main, withHandler: { (devMotion, error) -> Void in
arrow.orientation = SCNQuaternion(-CGFloat((motionManager.deviceMotion?.attitude.quaternion.x)!), -CGFloat((motionManager.deviceMotion?.attitude.quaternion.y)!), -CGFloat((motionManager.deviceMotion?.attitude.quaternion.z)!), CGFloat((motionManager.deviceMotion?.attitude.quaternion.w)!))
})}
After done this, if the user rotate the device in any axis, the arrow can be rotated as well. Since this is a 3D compass, I need the arrow to point the direction. So I fetched the heading data via CLLocationManager, and then the problem is coming.
let gq1: GLKQuaternion = GLKQuaternionMakeWithAngleAndAxis(GLKMathDegreesToRadians(heading), 0, 0, 1)
arrow.orientation = SCNVector4(gq1.x,gq1.y,gq1.z,gq1.w)
The above code is doing perfectly in 2D like the pic below, it points the east direction correctly. But I want it in 3D environment, so I did like below
motionManager.deviceMotionUpdateInterval = 1.0/60.0
if motionManager.isDeviceMotionAvailable {
motionManager.startDeviceMotionUpdates(to: OperationQueue.main, withHandler: { (devMotion, error) -> Void in
arrow.orientation = orient(q: (motionManager.deviceMotion?.attitude.quaternion)!, angle: heading)
}
func orient(q:CMQuaternion,angle:Float) -> SCNQuaternion{
GLKQuaternionMakeWithAngleAndAxis(GLKMathDegreesToRadians(-angle), 0, 0, 1)
let gq1: GLKQuaternion = GLKQuaternionMakeWithAngleAndAxis(GLKMathDegreesToRadians(-angle), 0, 0, 1)
// add a rotation in z axis
let gq2: GLKQuaternion = GLKQuaternionMake(Float(q.x), Float(q.y), Float(q.z), Float(q.w))
// the current orientation
let qp: GLKQuaternion = GLKQuaternionMultiply(gq1, gq2)
// get the "new" orientation
var rq = CMQuaternion()
rq.x = Double(qp.x)
rq.y = Double(qp.y)
rq.z = Double(qp.z)
rq.w = Double(qp.w)
return SCNVector4(-Float(rq.x), -Float(rq.y), -Float(rq.z), Float(rq.w))
}
There is a bug(this video shows), if I rotate the device around z axis, the arrow still rotates around y axis. Did I do anything wrong?
If I rotate the device around z axis by this method, the direction data comes out continuously. As a result, I can't use CLLocationManager to achieve my goal. This thread is what I want.
I'm trying to rotate an SCNBox I created using swipe gestures. For example, when I swipe right the box should rotate 90degs in the Y-axis and -90degs when I swipe left. To achieve this I have been using the node's SCNAction.rotateByX method to perform the rotation animation. Now the problem I'm having is when rotating along either the X-axis or Z-axis after a rotation in the Y-axis and vice-versa is that the positions of the axes change.
What I have notice is that any rotation perform on either of the X,Y,Z axes changes the direction in which the other axes point.
Example: Default position
Then after a rotation in the Z-axis:
Of course this pose a problem because now when I swipe left or right I no longer get the desire effect because the X-axis and Y-axis have now swapped positions. What I would like to know is why does this happen? and is there anyway to perform the rotation animation without it affecting the other axes?
I apologize for my lack of understanding on this subject as this is my first go at 3d graphics.
Solution:
func swipeRight(recognizer: UITapGestureRecognizer) {
// rotation animation
let action = SCNAction.rotateByX(0, y: CGFloat(GLKMathDegreesToRadians(90)), z: 0, duration: 0.5)
boxNode.runAction(action)
//repositoning of the x,y,z axes after the rotation has been applied
let currentPivot = boxNode.pivot
let changePivot = SCNMatrix4Invert(boxNode.transform)
boxNode.pivot = SCNMatrix4Mult(changePivot, currentPivot)
boxNode.transform = SCNMatrix4Identity
}
I haven't ran into any problems yet but it may be safer to use a completion handler to ensure any changes to X,Y,Z axes are done before repositioning them.
I had the same issue, here's what I use to give the desired behavior:
func panGesture(sender: UIPanGestureRecognizer) {
let translation = sender.translationInView(sender.view!)
let pan_x = Float(translation.x)
let pan_y = Float(-translation.y)
let anglePan = sqrt(pow(pan_x,2)+pow(pan_y,2))*(Float)(M_PI)/180.0
var rotVector = SCNVector4()
rotVector.x = -pan_y
rotVector.y = pan_x
rotVector.z = 0
rotVector.w = anglePan
// apply to your model container node
boxNode.rotation = rotVector
if(sender.state == UIGestureRecognizerState.Ended) {
let currentPivot = boxNode.pivot
let changePivot = SCNMatrix4Invert(boxNode.transform)
boxNode.pivot = SCNMatrix4Mult(changePivot, currentPivot)
boxNode.transform = SCNMatrix4Identity
}
}
Hello everyone,
I come back to you about my current problem. I already asked a question about that but no one had success to help me. Then I will explain my complete problem and how I tried to fix it. (I tried several things)
So, I need to code a lib that adds many functions in order to manage cameras and objects in a 3D world. For that we have chosen SceneKit Framework to use Metal.
I will post a very simplified code but all necessary things are here.
To illustrate my thought here is a GIF which explains how I want my camera acts like:
http://i.stack.imgur.com/5tHUl.gif
This comes from Stack question (thanks rickster) : Rotate SCNCamera node looking at an object around an imaginary sphere
The goal is to load a scene and handle User Pan Action to move camera around the gravity center point of a 3D object in my 3D world. Here is my basic simplified code:
import SceneKit
import UIKit
class SceneManager
{
private let scene: SCNScene
private let view: SCNView
private let camera: SCNNode
private let cameraOrbit: SCNNode
init(view: SCNView, assetFolder: String, sceneName: String, cameraName: String, backgroundColor: UIColor) {
self.view = view
self.scene = SCNScene(named: (assetFolder + "/" + sceneName))!
if (self.scene.rootNode.childNodeWithName(cameraName, recursively: true) == nil) {
print("Fatal error: Cannot find camera in scene with name :\"", cameraName, "\"")
exit(1)
}
self.camera = self.scene.rootNode.childNodeWithName(cameraName, recursively: true)! // Retrieve cameraNode created in scene file
self.camera.removeFromParentNode()
self.cameraOrbit = SCNNode()
self.cameraOrbit.addChildNode(self.camera)
self.scene.rootNode.addChildNode(self.cameraOrbit)
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panHandler(_:)))
panGesture.maximumNumberOfTouches = 1
self.view.addGestureRecognizer(panGesture)
self.view.backgroundColor = backgroundColor
self.view.pointOfView = cameraNode
self.view.scene = self.scene
}
#objc private func panHandler(sender: UIPanGestureRecognizer) {
// some code here
}
}
Then I have explore many possible solutions I had found on Internet. I will present the principal solution I had explore.
1) EulerAngle
Src: Rotate SCNCamera node looking at an object around an imaginary sphere
I wanted to applied the rickster's method in this Stack. Here is my trying code:
#objc private func panHandler(sender: UIPanGestureRecognizer) {
if (sender.state == UIGestureRecognizerState.Changed) {
let scrollWidthRatio = Float(sender.velocityInView(sender.view!).x) / 10000 * -1
let scrollHeightRatio = Float(sender.velocityInView(sender.view!).y) / 10000
cameraOrbit.eulerAngles.y += Float(-2 * M_PI) * scrollWidthRatio
cameraOrbit.eulerAngles.x += Float(-M_PI) * scrollHeightRatio
}
}
That works well for Y axis but the camera spin around itself on X axis. I do not understand very well what refers to the eulerAngle but I notice the Z axis never changes. Is this why the rotation is not spherical?
2) Homemade solution
src: SceneKit Child node position not changed during Parent node rotation
That is a question I have posted about the worldPosition and localPosition. But the solution proposed did not work... (Or I did not understand)
This is the solution I will use principally. But if you have another solution, I am ready to explore and try it!
Theoretically the var alpha is the angle between abscissa axis and position of the camera (2D (x, z)) in trigonometry circle. I use that to calculate the ratio to apply at X and Z rotation's axes.
The goal is to have a rotation that follows a segment on 2D plan (x, z).
But that did not work cause of self.camera.position is coordinated relative to cameraOrbit (parent node) and it needs the worldPosition property.
The parameters of camera worldTransform is a matrix I did not understand so I can not use it. Even if I can use the worldPosition property, I am not sure that it will work very well cause of my angle and ratio applied to X and Z axes.
What do you think about this, maybe I need to change my method?
#objc private func panHandler(sender: UIPanGestureRecognizer) {
let cameraROrbitRadius = sqrt(pow(self.camera.position.x, 2) + pow(self.camera.position.y, 2))
let alpha = cos(self.camera.position.z / self.cameraOrbitRadius) // Get angle of camera
var ratioX = 1 - ((CGFloat)(alpha) / (CGFloat)(M_PI)) // Get the ratio with angle for apply to Z and X axes rotation
var ratioZ = ((CGFloat)(alpha) / (CGFloat)(M_PI))
// Change direction of rotation depending camera's position in trigonometric circle
if (self.camera.position.x > 0 && self.camera.position.z < 0) {
ratioX *= -1
} else if (self.camera.position.z < 0 && self.camera.position.x < 0) {
ratioX *= -1
ratioZ *= -1
} else if (self.camera.position.z > 0 && self.camera.position.x > 0) {
ratioZ *= -1
}
// Set the angle rotation to add at imaginary sphere (cameraOrbit)
let xAngleToAdd = (sender.velocityInView(sender.view!).y / 10000) * ratioX
let yAngleToAdd = (sender.velocityInView(sender.view!).x / 10000) * (-1)
let zAngleToAdd = (sender.velocityInView(sender.view!).y / 10000) * ratioZ
let rotation = SCNAction.rotateByX(xAngleToAdd, y: yAngleToAdd, z: zAngleToAdd, duration: 0.5)
self.cameraOrbit.runAction(rotation)
}
This method works for Y axis too. But the rotation above the object works bad. I can not explain it simply but the rotation of camera is offbeat of this theoretically movement.
I think I have explain every important things. If you have any ideas or tips?
Regards,