web-dev-qa-db-ja.com

カメラが見下ろしている軸上でSCNNodeを回転させるにはどうすればよいですか?

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

カメラの軸を中心に常に回転させるにはどうすればよいですか?

11
Hunter Monk

私はあなたの質問を理解していると思いますが、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軸を表す軸が必要です。これが頻繁に必要になるため、parentFrontからSCNNodeを取得するための拡張機能を作成しました。

これで、カメラの前軸ができました

_let cameraLookingDirection = sceneView.pointOfView!.parentFront
_

ターゲットの参照フレームに変換するには、 convertVector(_,from:) を使用して、回転を適用できるベクトルを取得します。このメソッドの結果は、シーンが最初に起動されたときのボックスの-Z軸になります(静的コードのように、Z軸を使用し、角度を無効にしました)。

_let cameraLookingDirectionInTargetNodesReference = boxNode.convertVector(cameraLookingDirection, from: sceneView.pointOfView!.parent!)
_

必要かどうかわからない部分である加法回転を実現するために、ベクトル回転の代わりにクォータニオンを使用しました。基本的に、ジェスチャの開始時にボックスのorientationを取得し、クォータニオン乗算を介して回転を適用します。これらの2行:

_let quaternion = GLKQuaternion(angle: Float(rotation.rotation), axis: rotationAxis)
node.orientation = SCNQuaternion((startingOrientation * quaternion).normalized())
_

この計算は、回転ベクトルまたは変換行列を使用して実行することもできますが、これは私がよく知っている方法です。

結果

enter image description here

拡張機能

_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)
    }
}
_
12
allenh

つまり、オブジェクトを回転させる前にカメラの回転の逆数を適用し、回転後にカメラの回転の逆数を削除します。

必要な動作を実現するために、小さなSceneKitサンプルプロジェクトを設定しました。これはObjectiveCにありますが、主要部分(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);

    }
}

これには、ノードの方向と位置に関係なく、カメラの回転と位置に関係なく、また、childNodeとrootNodeの直下のノードの両方について、画面空間でのパンと回転が含まれます。

5
Xartec