如何让一个SCNNode绕着摄像机所看的轴旋转?

13

我已添加了一个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))
}

如果相机自放置节点以来没有移动,则此方法可以正常工作。

enter image description here

然而,如果用户移动到节点的侧面,它就不再绕相机所面对的轴旋转。

enter image description here

我该如何始终围绕相机轴旋转?


哇,这很棘手。你尝试过根据相机的方向旋转枢轴并在.began时反向旋转盒子吗? - Alok Subedi
我的问题是询问如何做到这一点。 - Hunter Monk
2个回答

24

我认为我理解了你的问题,但是你对Xartec的答案的评论让我有点困惑,不确定我是否真正理解了你的问题。

重新陈述:

目标是围绕从摄像机原点“直接通过”对象形成的矢量旋转对象。该矢量垂直于相机所在平面(在本例中为手机屏幕),是相机的 -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!)
为了实现加性旋转,我使用四元数而不是向量旋转。基本上,我通过四元数乘法取手势开始时箱子的朝向并施加一个旋转。这两行代码:
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)
    }
}

7
简而言之,在旋转物体之前应用相机的反向旋转,然后在旋转后移除该相机反向旋转。
我设置了一个小的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);

    }
}

它包括在屏幕空间中平移和旋转,无论节点的方向和位置,无论相机的旋转和位置,并且对于childNodes和直接在rootNode下的节点都适用。

谢谢您的建议!不幸的是,演示项目并不能完全满足需求。我只想让节点绕相机垂直轴旋转。演示项目可以在多个轴上旋转,这确实很有用,但不是我需要的。 - Hunter Monk
很好的简单解决方案,z轴方向也可以工作(显然只需在z轴上旋转),如果人们不想走下命中测试和四元数路线..谢谢! - Gary

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接