如何在相机向下看的轴上旋转 SCNNode?
How can I rotate an SCNNode on the axis the camera is looking down?
我添加了一个 UIRotationGestureRecognizer
并想用它来旋转用户选择的节点。
目前绕z轴旋转,像这样:
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))
}
如果自从放置节点后相机没有移动,这将正常工作。
但是,如果用户移动到节点的一侧,则它不再围绕相机所面对的轴旋转。
如何始终围绕相机的轴旋转它?
简而言之,在旋转对象之前应用相机旋转的逆旋转,然后在旋转之后移除相机旋转的逆旋转。
我设置了一个小型 SceneKit 示例项目来获得您想要的行为。它在 Objective C 中,但主要部分 (handlePan) 应该很容易转换为 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);
}
}
它包括在屏幕空间中的平移和旋转,无论节点的方向和位置如何,无论相机的旋转和位置如何,对于子节点和直接在根节点下的节点。
我想我理解你的问题,但你对 Xartec 回答的评论让我有点困惑,我是否真的理解。
重述:
目标是围绕从相机原点 "straight through" 对象画一条线形成的矢量旋转对象。这是垂直于相机平面的矢量,在本例中为 phone 屏幕。这个向量就是相机的-Z轴。
解决方案
根据我对你的目标的理解,这就是你所需要的
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())
}
}
说明
真正关键的部分是确定要围绕哪个矢量旋转,幸运的是 SceneKit 提供了非常方便的方法。不幸的是,他们没有提供您需要的所有方法。
首先,您需要表示相机前部的矢量(相机始终朝向其前轴)。 SCNNode.localFront
是-Z轴(0, 0, -1),这只是SceneKit中的一种约定。但是您想要在相机的父坐标系中表示 Z 轴的轴。我发现我经常需要这个,所以我创建了一个扩展来从 SCNNode
.
中获取 parentFront
现在我们有了相机的前轴
let cameraLookingDirection = sceneView.pointOfView!.parentFront
为了将其转换为目标的参考系,我们使用 convertVector(_,from:)
来获取可以应用旋转的矢量。当场景首次启动时,此方法的结果将是盒子的 -Z 轴(就像在您的静态代码中一样,但您使用了 Z 轴并取反了角度)。
let cameraLookingDirectionInTargetNodesReference = boxNode.convertVector(cameraLookingDirection, from: sceneView.pointOfView!.parent!)
为了实现加法旋转,这是我不清楚您是否需要的部分,我使用了四元数而不是矢量旋转。基本上,我在手势开始时获取框的 orientation
并通过四元数乘法应用旋转。这两行:
let quaternion = GLKQuaternion(angle: Float(rotation.rotation), axis: rotationAxis)
node.orientation = SCNQuaternion((startingOrientation * quaternion).normalized())
这个数学也可以用旋转向量或变换矩阵来完成,但这是我熟悉的方法。
结果
扩展程序
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)
}
}
我添加了一个 UIRotationGestureRecognizer
并想用它来旋转用户选择的节点。
目前绕z轴旋转,像这样:
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))
}
如果自从放置节点后相机没有移动,这将正常工作。
但是,如果用户移动到节点的一侧,则它不再围绕相机所面对的轴旋转。
如何始终围绕相机的轴旋转它?
简而言之,在旋转对象之前应用相机旋转的逆旋转,然后在旋转之后移除相机旋转的逆旋转。
我设置了一个小型 SceneKit 示例项目来获得您想要的行为。它在 Objective C 中,但主要部分 (handlePan) 应该很容易转换为 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);
}
}
它包括在屏幕空间中的平移和旋转,无论节点的方向和位置如何,无论相机的旋转和位置如何,对于子节点和直接在根节点下的节点。
我想我理解你的问题,但你对 Xartec 回答的评论让我有点困惑,我是否真的理解。
重述:
目标是围绕从相机原点 "straight through" 对象画一条线形成的矢量旋转对象。这是垂直于相机平面的矢量,在本例中为 phone 屏幕。这个向量就是相机的-Z轴。
解决方案
根据我对你的目标的理解,这就是你所需要的
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())
}
}
说明
真正关键的部分是确定要围绕哪个矢量旋转,幸运的是 SceneKit 提供了非常方便的方法。不幸的是,他们没有提供您需要的所有方法。
首先,您需要表示相机前部的矢量(相机始终朝向其前轴)。 SCNNode.localFront
是-Z轴(0, 0, -1),这只是SceneKit中的一种约定。但是您想要在相机的父坐标系中表示 Z 轴的轴。我发现我经常需要这个,所以我创建了一个扩展来从 SCNNode
.
parentFront
现在我们有了相机的前轴
let cameraLookingDirection = sceneView.pointOfView!.parentFront
为了将其转换为目标的参考系,我们使用 convertVector(_,from:)
来获取可以应用旋转的矢量。当场景首次启动时,此方法的结果将是盒子的 -Z 轴(就像在您的静态代码中一样,但您使用了 Z 轴并取反了角度)。
let cameraLookingDirectionInTargetNodesReference = boxNode.convertVector(cameraLookingDirection, from: sceneView.pointOfView!.parent!)
为了实现加法旋转,这是我不清楚您是否需要的部分,我使用了四元数而不是矢量旋转。基本上,我在手势开始时获取框的 orientation
并通过四元数乘法应用旋转。这两行:
let quaternion = GLKQuaternion(angle: Float(rotation.rotation), axis: rotationAxis)
node.orientation = SCNQuaternion((startingOrientation * quaternion).normalized())
这个数学也可以用旋转向量或变换矩阵来完成,但这是我熟悉的方法。
结果
扩展程序
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)
}
}