Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I rotate an SCNNode on the axis the camera is looking down?

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.

enter image description here

However, if the user moves to the side of the node, it no longer rotates on the axis the camera is facing.

enter image description here

How can I always rotate it around the camera's axis?

like image 910
Hunter Monk Avatar asked Jan 30 '18 20:01

Hunter Monk


Video Answer


2 Answers

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.

like image 79
Xartec Avatar answered Nov 15 '22 21:11

Xartec


I think I understand your question but your comment on Xartec's answer has me a little confused as to whether I really do.

To restate:

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.

Solution

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

Explanation

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.

Result

enter image description here

Extensions

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)
    }
}
like image 36
allenh Avatar answered Nov 15 '22 22:11

allenh