【问题标题】:How can I rotate an SCNNode on the axis the camera is looking down?如何在相机向下看的轴上旋转 SCNNode?
【发布时间】:2018-07-09 20:30:19
【问题描述】:

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

如果在放置节点后相机没有移动,这将正常工作。

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

我怎样才能始终围绕相机的轴旋转它?

【问题讨论】:

  • 哇,这很棘手。您是否尝试过相对于相机的方向旋转枢轴并反旋转盒子(.began
  • 我的问题是如何做到这一点。

标签: ios swift scenekit arkit


【解决方案1】:

我想我理解你的问题,但你对 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!)

为了实现加法旋转,我不清楚你是否需要这部分,我使用四元数而不是矢量旋转。基本上,当手势开始时,我采用框的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)
    }
}

【讨论】:

    【解决方案2】:

    简而言之,在旋转对象之前应用相机旋转的倒数,然后在旋转后移除相机旋转的倒数。

    我设置了一个小的 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 下的节点都是如此。

    【讨论】:

    • 感谢您的建议!不幸的是,演示项目并没有完全满足需要。我只希望节点在垂直于相机的轴上旋转。演示项目在多个轴上旋转,这绝对有用,但不是我需要的。
    • 不错的简单解决方案,也适用于 z 方向(显然只是在 z 轴上进行旋转),如果人们不想沿着命中测试和四元数路线走……谢谢!跨度>
    猜你喜欢
    • 2020-04-07
    • 2018-03-22
    • 2013-03-24
    • 2018-07-06
    • 1970-01-01
    • 2013-09-15
    • 1970-01-01
    • 1970-01-01
    • 2017-07-23
    相关资源
    最近更新 更多