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

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