I've added a UIRotationGestureRecognizer
and want to use it rotate a node that the user has selected.
Currently, it rotates around z-axis, like so:
private var startingRotation: CGFloat = 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 {
startingRotation = CGFloat(node.rotation.w)
}
node.rotation = SCNVector4(0, 0, 1, -Float(startingRotation + rotation.rotation))
}
This works correctly if the camera hasn't moved since placing the node.
However, if the user moves to the side of the node, it no longer rotates on the axis the camera is facing.
How can I always rotate it around the camera's axis?
In short, apply the inverse of the rotation of the camera before rotating the object, and then remove that inverse of the rotation of the camera after rotating.
I set up a small SceneKit sample project to get the behavior you want. It's in Objective C but the main part (handlePan) should be easy enough to translate to Swift: https://github.com/Xartec/ScreenSpaceRotationAndPan
- (void) handlePan:(UIPanGestureRecognizer*)gestureRecognize {
SCNView *scnView = (SCNView *)self.view;
CGPoint delta = [gestureRecognize translationInView:self.view];
CGPoint loc = [gestureRecognize locationInView:self.view];
if (gestureRecognize.state == UIGestureRecognizerStateBegan) {
prevLoc = loc;
touchCount = (int)gestureRecognize.numberOfTouches;
} else if (gestureRecognize.state == UIGestureRecognizerStateChanged) {
delta = CGPointMake(loc.x -prevLoc.x, loc.y -prevLoc.y);
prevLoc = loc;
if (touchCount != (int)gestureRecognize.numberOfTouches) {
return;
}
SCNMatrix4 rotMat;
if (touchCount == 2) { //create move/translate matrix
rotMat = SCNMatrix4MakeTranslation(delta.x*0.025, delta.y*-0.025, 0);
} else { //create rotate matrix
SCNMatrix4 rotMatX = SCNMatrix4Rotate(SCNMatrix4Identity, (1.0f/100)*delta.y , 1, 0, 0);
SCNMatrix4 rotMatY = SCNMatrix4Rotate(SCNMatrix4Identity, (1.0f/100)*delta.x , 0, 1, 0);
rotMat = SCNMatrix4Mult(rotMatX, rotMatY);
}
//get the translation matrix of the child node
SCNMatrix4 transMat = SCNMatrix4MakeTranslation(selectedNode.position.x, selectedNode.position.y, selectedNode.position.z);
//move the child node to the origin of its parent (but keep its local rotation)
selectedNode.transform = SCNMatrix4Mult(selectedNode.transform, SCNMatrix4Invert(transMat));
//apply the "rotation" of the parent node extra
SCNMatrix4 parentNodeTransMat = SCNMatrix4MakeTranslation(selectedNode.parentNode.worldPosition.x, selectedNode.parentNode.worldPosition.y, selectedNode.parentNode.worldPosition.z);
SCNMatrix4 parentNodeMatWOTrans = SCNMatrix4Mult(selectedNode.parentNode.worldTransform, SCNMatrix4Invert(parentNodeTransMat));
selectedNode.transform = SCNMatrix4Mult(selectedNode.transform, parentNodeMatWOTrans);
//apply the inverse "rotation" of the current camera extra
SCNMatrix4 camorbitNodeTransMat = SCNMatrix4MakeTranslation(scnView.pointOfView.worldPosition.x, scnView.pointOfView.worldPosition.y, scnView.pointOfView.worldPosition.z);
SCNMatrix4 camorbitNodeMatWOTrans = SCNMatrix4Mult(scnView.pointOfView.worldTransform, SCNMatrix4Invert(camorbitNodeTransMat));
selectedNode.transform = SCNMatrix4Mult(selectedNode.transform,SCNMatrix4Invert(camorbitNodeMatWOTrans));
//perform the rotation based on the pan gesture
selectedNode.transform = SCNMatrix4Mult(selectedNode.transform, rotMat);
//remove the extra "rotation" of the current camera
selectedNode.transform = SCNMatrix4Mult(selectedNode.transform, camorbitNodeMatWOTrans);
//remove the extra "rotation" of the parent node (we can use the transform because parent node is at world origin)
selectedNode.transform = SCNMatrix4Mult(selectedNode.transform,SCNMatrix4Invert(parentNodeMatWOTrans));
//add back the local translation mat
selectedNode.transform = SCNMatrix4Mult(selectedNode.transform, transMat);
}
}
It includes panning as well as rotating in screenspace, regardless of the node's orientation and position, regardless of the camera's rotation and position, and both for childNodes and nodes directly under rootNode.
I think I understand your question but your comment on Xartec's answer has me a little confused as to whether I really do.
The goal is to rotate an object around the vector formed by drawing a line from the camera's origin "straight through" the object. Which is a vector perpendicular to the plane of the camera, in this case the phone screen. This vector is the camera's -Z axis.
Based on my understanding of your goal here is what you need
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 {
// This will be the total rotation to apply to the starting orientation
let quaternion = GLKQuaternion(angle: Float(rotation.rotation), axis: rotationAxis)
// Apply the rotation
node.orientation = SCNQuaternion((startingOrientation * quaternion).normalized())
}
}
The really crucial part is figuring out which vector you want to rotate about, fortunately SceneKit provides methods that are very handy for doing that. Unfortunately, they do not provide all the methods you need.
First, you need the vector that represents the camera's front (camera's are always looking toward their front axis). SCNNode.localFront
is the -Z axis (0, 0, -1), this is simply a convention in SceneKit. But you want the axis that represents the Z axis in the camera's parent's coordinate system. I find that I need this so often, that I created an extension to get parentFront
from an SCNNode
.
Now we have the camera's front axis
let cameraLookingDirection = sceneView.pointOfView!.parentFront
to convert it to the target's reference frame, we use convertVector(_,from:)
to get a vector that we can apply a rotation with. The result of this method will be the -Z axis of the box when the scene is first launched (like in your static code, but you used the Z axis and negated the angle).
let cameraLookingDirectionInTargetNodesReference = boxNode.convertVector(cameraLookingDirection, from: sceneView.pointOfView!.parent!)
To achieve an additive rotation, which is the part I'm not clear whether you need, I used quaternions instead of vector rotations. Basically, I take the orientation
of the box when the gesture starts and apply a rotation via quaternion multiplication. These 2 lines:
let quaternion = GLKQuaternion(angle: Float(rotation.rotation), axis: rotationAxis)
node.orientation = SCNQuaternion((startingOrientation * quaternion).normalized())
This math can also be done with rotation vectors or transformation matrices, but this is the method that I'm familiar with.
extension SCNNode {
/// The local unit Y axis (0, 1, 0) in parent space.
var parentUp: SCNVector3 {
let transform = self.transform
return SCNVector3(transform.m21, transform.m22, transform.m23)
}
/// The local unit X axis (1, 0, 0) in parent space.
var parentRight: SCNVector3 {
let transform = self.transform
return SCNVector3(transform.m11, transform.m12, transform.m13)
}
/// The local unit -Z axis (0, 0, -1) in parent space.
var parentFront: SCNVector3 {
let transform = self.transform
return SCNVector3(-transform.m31, -transform.m32, -transform.m33)
}
}
extension GLKQuaternion {
init(vector: GLKVector3, scalar: Float) {
let glkVector = GLKVector3Make(vector.x, vector.y, vector.z)
self = GLKQuaternionMakeWithVector3(glkVector, scalar)
}
init(angle: Float, axis: GLKVector3) {
self = GLKQuaternionMakeWithAngleAndAxis(angle, axis.x, axis.y, axis.z)
}
func normalized() -> GLKQuaternion {
return GLKQuaternionNormalize(self)
}
static var identity: GLKQuaternion {
return GLKQuaternionIdentity
}
}
func * (left: GLKQuaternion, right: GLKQuaternion) -> GLKQuaternion {
return GLKQuaternionMultiply(left, right)
}
extension SCNQuaternion {
init(_ quaternion: GLKQuaternion) {
self = SCNVector4(quaternion.x, quaternion.y, quaternion.z, quaternion.w)
}
}
extension GLKQuaternion {
init(_ quaternion: SCNQuaternion) {
self = GLKQuaternionMake(quaternion.x, quaternion.y, quaternion.z, quaternion.w)
}
}
extension GLKVector3 {
init(_ vector: SCNVector3) {
self = SCNVector3ToGLKVector3(vector)
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With