web-dev-qa-db-fra.com

Faire pivoter le noeud SCNCamera en regardant un objet autour d'une sphère imaginaire

J'ai un SCNCamera à la position (30,30,30) avec un SCNLookAtConstraint sur un objet situé à la position (0,0,0). J'essaie de faire tourner la caméra autour de l'objet sur une sphère imaginaire en utilisant A UIPanGestureRecognizer, tout en maintenant le rayon entre la caméra et l'objet. Je suppose que je devrais utiliser des projections de Quaternion, mais mes connaissances en mathématiques dans ce domaine sont abominables. Mes variables connues sont la traduction x & y + le rayon que je cherche à conserver. J'ai écrit le projet dans Swift mais une réponse en Objective-C serait également acceptée (en utilisant un framework standard Cocoa Touch). 

Où:

private var cubeView : SCNView!;
private var cubeScene : SCNScene!;
private var cameraNode : SCNNode!;

Voici mon code pour mettre en scène:

// setup the SCNView
cubeView = SCNView(frame: CGRectMake(0, 0, self.width(), 175));
cubeView.autoenablesDefaultLighting = YES;
self.addSubview(cubeView);

// setup the scene
cubeScene = SCNScene();
cubeView.scene = cubeScene;

// setup the camera
let camera = SCNCamera();
camera.usesOrthographicProjection = YES;
camera.orthographicScale = 9;
camera.zNear = 0;
camera.zFar = 100;

cameraNode = SCNNode();
cameraNode.camera = camera;
cameraNode.position = SCNVector3Make(30, 30, 30)  
cubeScene.rootNode.addChildNode(cameraNode)

// setup a target object
let box = SCNBox(width: 10, height: 10, length: 10, chamferRadius: 0);
let boxNode = SCNNode(geometry: box)
cubeScene.rootNode.addChildNode(boxNode)

// put a constraint on the camera
let targetNode = SCNLookAtConstraint(target: boxNode);
targetNode.gimbalLockEnabled = YES;
cameraNode.constraints = [targetNode];

// add a gesture recogniser
let gesture = UIPanGestureRecognizer(target: self, action: "panDetected:");
cubeView.addGestureRecognizer(gesture);

Et voici le code pour la gestion de reconnaissance de geste:

private var position: CGPoint!;

internal func panDetected(gesture:UIPanGestureRecognizer) {

    switch(gesture.state) {
    case UIGestureRecognizerState.Began:
        position = CGPointZero;
    case UIGestureRecognizerState.Changed:
        let aPosition = gesture.translationInView(cubeView);
        let delta = CGPointMake(aPosition.x-position.x, aPosition.y-position.y);

        // ??? no idea...

        position = aPosition;
    default:
        break
    }
}

Merci!

33
Danny Bravo

Il pourrait être utile de décomposer votre problème en sous-problèmes. 

Mise en scène

Premièrement, réfléchissez à la façon d’organiser votre scène pour activer le type de mouvement souhaité. Vous parlez de déplacer la caméra comme si elle était attachée à une sphère invisible. Utilisez cette idée! Au lieu d'essayer de calculer le calcul pour régler votre cameraNode.position sur une sphère imaginaire, pensez à ce que vous feriez pour déplacer la caméra si elle était attachée à une sphère. C'est-à-dire qu'il suffit de faire pivoter la sphère.

Si vous souhaitez faire pivoter une sphère séparément du reste du contenu de votre scène, vous devez l'attacher à un nœud distinct. Bien sûr, vous n'avez pas besoin d'insérer une géométrie sphère dans votre scène. Créez simplement un nœud dont la variable position est concentrique avec l’objet sur lequel vous voulez que votre caméra tourne, puis attachez la caméra à un nœud enfant de ce nœud. Ensuite, vous pouvez faire pivoter ce nœud pour déplacer la caméra. Voici une démonstration rapide de cela, en l'absence de l'entreprise de traitement d'événement de défilement: 

let camera = SCNCamera()
camera.usesOrthographicProjection = true
camera.orthographicScale = 9
camera.zNear = 0
camera.zFar = 100
let cameraNode = SCNNode()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 50)
cameraNode.camera = camera
let cameraOrbit = SCNNode()
cameraOrbit.addChildNode(cameraNode)
cubeScene.rootNode.addChildNode(cameraOrbit)

// rotate it (I've left out some animation code here to show just the rotation)
cameraOrbit.eulerAngles.x -= CGFloat(M_PI_4)
cameraOrbit.eulerAngles.y -= CGFloat(M_PI_4*3)

Voici ce que vous voyez à gauche et une visualisation de son fonctionnement à droite. La sphère à damiers est cameraOrbit et le cône vert est cameraNode.

camera rotate around cubecamera rotate visualization

Il y a quelques bonus à cette approche:

  • Il n'est pas nécessaire de définir la position initiale de la caméra en coordonnées cartésiennes. Placez-le simplement à la distance que vous souhaitez le long de l'axe z. Puisque cameraNode est un noeud enfant de cameraOrbit, sa propre position reste constante - la caméra se déplace en raison de la rotation de cameraOrbit
  • Tant que vous voulez que la caméra soit dirigée vers le centre de cette sphère imaginaire, vous n'avez pas besoin d'une contrainte de regard. La caméra pointe dans la direction -Z de l'espace dans lequel elle se trouve - si vous la déplacez dans la direction + Z, puis faites pivoter le nœud parent, la caméra pointera toujours vers le centre du nœud parent (le centre de rotation). .

Traitement des entrées

Maintenant que votre scène est conçue pour la rotation de la caméra, il est assez facile de convertir les événements d'entrée en rotation. La facilité dépend du type de contrôle que vous recherchez:

  • Vous cherchez une rotation d'arcball? (C'est bien pour la manipulation directe, car on sent que l'on pousse physiquement un point sur l'objet 3D.) Il y a déjà quelques questions et réponses à ce sujet sur SO - les utilisent GLKQuaternion. (UPDATE: Les types GLK sont "en quelque sorte" disponibles dans Swift 1.2/Xcode 6.3. Avant ces versions, vous pouvez effectuer vos calculs dans ObjC via un en-tête de pontage.)
  • Pour une alternative plus simple, vous pouvez simplement mapper les axes x et y de votre geste sur les angles de lacet et de tangage de votre nœud. Ce n’est pas aussi astucieux que la rotation d’arcball, mais c’est assez facile à mettre en œuvre - il vous suffit de mettre au point une conversion de points en radians qui couvre la quantité de rotation que vous recherchez.

Dans les deux cas, vous pouvez ignorer certaines des règles du jeu et obtenir des comportements interactifs pratiques en utilisant UIScrollView à la place. (Ce n’est pas qu’il ne soit pas utile de s’en tenir aux identifiants de gestes - c’est une alternative facile à mettre en œuvre.)

Déposez-en un sur votre SCNView (sans y placer un autre point de vue pour le faire défiler) et définissez sa contentSize sur un multiple de la taille de son cadre ... vous pourrez alors mapper la contentOffset sur votre eulerAngles:

func scrollViewDidScroll(scrollView: UIScrollView) {
    let scrollWidthRatio = Float(scrollView.contentOffset.x / scrollView.frame.size.width)
    let scrollHeightRatio = Float(scrollView.contentOffset.y / scrollView.frame.size.height)
    cameraOrbit.eulerAngles.y = Float(-2 * M_PI) * scrollWidthRatio
    cameraOrbit.eulerAngles.x = Float(-M_PI) * scrollHeightRatio
}

D'une part, vous devez faire un peu plus de travail pour défilement infini si vous souhaitez tourner sans fin dans un sens ou dans les deux. De l’autre, vous obtenez des comportements d’inertie et de rebond sympa de type parchemin.

91
rickster

Hé, j’ai rencontré le problème l’autre jour et la solution que j’ai proposée est assez simple mais fonctionne bien.

J'ai d'abord créé mon appareil photo et l'a ajouté à ma scène comme suit:

    // create and add a camera to the scene
    cameraNode = [SCNNode node];
    cameraNode.camera = [SCNCamera camera];
    cameraNode.camera.automaticallyAdjustsZRange = YES;
    [scene.rootNode addChildNode:cameraNode];

    // place the camera
    cameraNode.position = SCNVector3Make(0, 0, 0);
    cameraNode.pivot = SCNMatrix4MakeTranslation(0, 0, -15); //the -15 here will become the rotation radius

Ensuite, j'ai créé une variable de classe CGPoint slideVelocity. Et créé un UIPanGestureRecognizer et un et dans son rappel, je mets le texte suivant:

-(void)handlePan:(UIPanGestureRecognizer *)gestureRecognize{
    slideVelocity = [gestureRecognize velocityInView:self.view];
}

Ensuite, j'ai cette méthode qui s'appelle chaque image. Notez que j'utilise GLKit pour les mathématiques quaternion.

-(void)renderer:(id<SCNSceneRenderer>)aRenderer didRenderScene:(SCNScene *)scenie atTime:(NSTimeInterval)time {        
    //spin the camera according the the user's swipes
    SCNQuaternion oldRot = cameraNode.rotation;  //get the current rotation of the camera as a quaternion
    GLKQuaternion rot = GLKQuaternionMakeWithAngleAndAxis(oldRot.w, oldRot.x, oldRot.y, oldRot.z);  //make a GLKQuaternion from the SCNQuaternion


    //The next function calls take these parameters: rotationAngle, xVector, yVector, zVector
    //The angle is the size of the rotation (radians) and the vectors define the axis of rotation
    GLKQuaternion rotX = GLKQuaternionMakeWithAngleAndAxis(-slideVelocity.x/viewSlideDivisor, 0, 1, 0); //For rotation when swiping with X we want to rotate *around* y axis, so if our vector is 0,1,0 that will be the y axis
    GLKQuaternion rotY = GLKQuaternionMakeWithAngleAndAxis(-slideVelocity.y/viewSlideDivisor, 1, 0, 0); //For rotation by swiping with Y we want to rotate *around* the x axis.  By the same logic, we use 1,0,0
    GLKQuaternion netRot = GLKQuaternionMultiply(rotX, rotY); //To combine rotations, you multiply the quaternions.  Here we are combining the x and y rotations
    rot = GLKQuaternionMultiply(rot, netRot); //finally, we take the current rotation of the camera and rotate it by the new modified rotation.

    //Then we have to separate the GLKQuaternion into components we can feed back into SceneKit
    GLKVector3 axis = GLKQuaternionAxis(rot);
    float angle = GLKQuaternionAngle(rot);

    //finally we replace the current rotation of the camera with the updated rotation
    cameraNode.rotation = SCNVector4Make(axis.x, axis.y, axis.z, angle);

    //This specific implementation uses velocity.  If you don't want that, use the rotation method above just replace slideVelocity.
    //decrease the slider velocity
    if (slideVelocity.x > -0.1 && slideVelocity.x < 0.1) {
        slideVelocity.x = 0;
    }
    else {
        slideVelocity.x += (slideVelocity.x > 0) ? -1 : 1;
    }

    if (slideVelocity.y > -0.1 && slideVelocity.y < 0.1) {
        slideVelocity.y = 0;
    }
    else {
        slideVelocity.y += (slideVelocity.y > 0) ? -1 : 1;
    }
}

Ce code donne une rotation infinie d’Arcball avec vélocité, ce qui, je crois, est ce que vous recherchez. De plus, vous n'avez pas besoin de la variable SCNLookAtConstraint avec cette méthode. En fait, cela va probablement gâcher, alors ne faites pas ça.

9
WolfLink

Si vous souhaitez implémenter la réponse de rickster à l'aide d'un identificateur de geste, vous devez enregistrer les informations d'état car vous ne recevrez qu'une traduction relative au début du geste. J'ai ajouté deux vars à ma classe

var lastWidthRatio: Float = 0
var lastHeightRatio: Float = 0

Et implémenté son code de rotation comme suit:

func handlePanGesture(sender: UIPanGestureRecognizer) {
    let translation = sender.translationInView(sender.view!)
    let widthRatio = Float(translation.x) / Float(sender.view!.frame.size.width) + lastWidthRatio
    let heightRatio = Float(translation.y) / Float(sender.view!.frame.size.height) + lastHeightRatio
    self.cameraOrbit.eulerAngles.y = Float(-2 * M_PI) * widthRatio
    self.cameraOrbit.eulerAngles.x = Float(-M_PI) * heightRatio
    if (sender.state == .Ended) {
        lastWidthRatio = widthRatio % 1
        lastHeightRatio = heightRatio % 1
    }
}
6
JuJoDi

Peut-être que cela pourrait être utile pour les lecteurs.

class GameViewController: UIViewController {

var cameraOrbit = SCNNode()
let cameraNode = SCNNode()
let camera = SCNCamera()


//HANDLE PAN CAMERA
var lastWidthRatio: Float = 0
var lastHeightRatio: Float = 0.2
var fingersNeededToPan = 1
var maxWidthRatioRight: Float = 0.2
var maxWidthRatioLeft: Float = -0.2
var maxHeightRatioXDown: Float = 0.02
var maxHeightRatioXUp: Float = 0.4

//HANDLE PINCH CAMERA
var pinchAttenuation = 20.0  //1.0: very fast ---- 100.0 very slow
var lastFingersNumber = 0

override func viewDidLoad() {
    super.viewDidLoad()

    // create a new scene
    let scene = SCNScene(named: "art.scnassets/ship.scn")!

    // create and add a light to the scene
    let lightNode = SCNNode()
    lightNode.light = SCNLight()
    lightNode.light!.type = SCNLightTypeOmni
    lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
    scene.rootNode.addChildNode(lightNode)

    // create and add an ambient light to the scene
    let ambientLightNode = SCNNode()
    ambientLightNode.light = SCNLight()
    ambientLightNode.light!.type = SCNLightTypeAmbient
    ambientLightNode.light!.color = UIColor.darkGrayColor()
    scene.rootNode.addChildNode(ambientLightNode)

//Create a camera like Rickster said
    camera.usesOrthographicProjection = true
    camera.orthographicScale = 9
    camera.zNear = 1
    camera.zFar = 100

    cameraNode.position = SCNVector3(x: 0, y: 0, z: 50)
    cameraNode.camera = camera
    cameraOrbit = SCNNode()
    cameraOrbit.addChildNode(cameraNode)
    scene.rootNode.addChildNode(cameraOrbit)

    //initial camera setup
    self.cameraOrbit.eulerAngles.y = Float(-2 * M_PI) * lastWidthRatio
    self.cameraOrbit.eulerAngles.x = Float(-M_PI) * lastHeightRatio

    // retrieve the SCNView
    let scnView = self.view as! SCNView

    // set the scene to the view
    scnView.scene = scene

    //allows the user to manipulate the camera
    scnView.allowsCameraControl = false  //not needed

    // add a tap gesture recognizer
    let panGesture = UIPanGestureRecognizer(target: self, action: "handlePan:")
    scnView.addGestureRecognizer(panGesture)

    // add a pinch gesture recognizer
    let pinchGesture = UIPinchGestureRecognizer(target: self, action: "handlePinch:")
    scnView.addGestureRecognizer(pinchGesture)
}

func handlePan(gestureRecognize: UIPanGestureRecognizer) {

    let numberOfTouches = gestureRecognize.numberOfTouches()

    let translation = gestureRecognize.translationInView(gestureRecognize.view!)
    var widthRatio = Float(translation.x) / Float(gestureRecognize.view!.frame.size.width) + lastWidthRatio
    var heightRatio = Float(translation.y) / Float(gestureRecognize.view!.frame.size.height) + lastHeightRatio

    if (numberOfTouches==fingersNeededToPan) {

        //  HEIGHT constraints
        if (heightRatio >= maxHeightRatioXUp ) {
            heightRatio = maxHeightRatioXUp
        }
        if (heightRatio <= maxHeightRatioXDown ) {
            heightRatio = maxHeightRatioXDown
        }


        //  WIDTH constraints
        if(widthRatio >= maxWidthRatioRight) {
            widthRatio = maxWidthRatioRight
        }
        if(widthRatio <= maxWidthRatioLeft) {
            widthRatio = maxWidthRatioLeft
        }

        self.cameraOrbit.eulerAngles.y = Float(-2 * M_PI) * widthRatio
        self.cameraOrbit.eulerAngles.x = Float(-M_PI) * heightRatio

        print("Height: \(round(heightRatio*100))")
        print("Width: \(round(widthRatio*100))")


        //for final check on fingers number
        lastFingersNumber = fingersNeededToPan
    }

    lastFingersNumber = (numberOfTouches>0 ? numberOfTouches : lastFingersNumber)

    if (gestureRecognize.state == .Ended && lastFingersNumber==fingersNeededToPan) {
        lastWidthRatio = widthRatio
        lastHeightRatio = heightRatio
        print("Pan with \(lastFingersNumber) finger\(lastFingersNumber>1 ? "s" : "")")
    }
}

func handlePinch(gestureRecognize: UIPinchGestureRecognizer) {
    let pinchVelocity = Double.init(gestureRecognize.velocity)
    //print("PinchVelocity \(pinchVelocity)")

    camera.orthographicScale -= (pinchVelocity/pinchAttenuation)

    if camera.orthographicScale <= 0.5 {
        camera.orthographicScale = 0.5
    }

    if camera.orthographicScale >= 10.0 {
        camera.orthographicScale = 10.0
    }

}

override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
    return .Landscape
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Release any cached data, images, etc that aren't in use.
}
}
4

Après avoir essayé d'implémenter ces solutions (en Objective-C), j'ai réalisé que Scene Kit rend cela beaucoup plus facile que de faire tout cela. SCNView a une propriété douce appelée allowCameraControl qui insère les dispositifs de reconnaissance de mouvements appropriés et déplace la caméra en conséquence. Le seul problème est que ce n'est pas la rotation d'arcball que vous recherchez, bien que cela puisse être facilement ajouté en créant un nœud enfant, en le positionnant où vous le souhaitez et en lui donnant une SCNCamera. Par exemple:

    _sceneKitView.allowsCameraControl = YES; //_sceneKitView is a SCNView

    //Setup Camera
    SCNNode *cameraNode = [[SCNNode alloc]init];
    cameraNode.position = SCNVector3Make(0, 0, 1);

    SCNCamera *camera = [SCNCamera camera];
    //setup your camera to fit your specific scene
    camera.zNear = .1;
    camera.zFar = 3;

    cameraNode.camera = camera;
    [_sceneKitView.scene.rootNode addChildNode:cameraNode];
0
sts54

Il n'est pas nécessaire de sauvegarder l'état ailleurs que sur le nœud lui-même. Le code qui utilise une sorte de rapport de largeur se comporte bizarrement lorsque vous faites défiler de manière répétée, et un autre code semble trop compliqué . Je suis venu avec une solution différente (et je crois une meilleure) pour les reconnaisseurs de mouvements , basé sur l'approche de @ rickster.

UIPanGestureRecognizer:

@objc func handlePan(recognizer: UIPanGestureRecognizer) {
    let translation = recognizer.velocity(in: recognizer.view)
    cameraOrbit.eulerAngles.y -= Float(translation.x/CGFloat(panModifier)).radians
    cameraOrbit.eulerAngles.x -= Float(translation.y/CGFloat(panModifier)).radians
}

UIPinchGestureRecognizer:

@objc func handlePinch(recognizer: UIPinchGestureRecognizer) {
    guard let camera = cameraOrbit.childNodes.first else {
      return
    }
    let scale = recognizer.velocity
    let z = camera.position.z - Float(scale)/Float(pinchModifier)
    if z < MaxZoomOut, z > MaxZoomIn {
      camera.position.z = z
    }
  }

J'ai utilisé velocity, comme avec translation lorsque vous ralentissez le toucher, ce serait toujours le même événement, ce qui ferait tourner la caméra très rapidement, pas ce que vous attendez.

panModifier et pinchModifier sont de simples nombres constants que vous pouvez utiliser pour ajuster la réactivité. J'ai trouvé que les valeurs optimales étaient 100 et 15 respectivement.

MaxZoomOut et MaxZoomIn sont également des constantes et sont exactement ce qu'elles semblent être.

J'utilise également une extension sur Float pour convertir les degrés en radians et vice-versa.

extension Float {
  var radians: Float {
    return self * .pi / 180
  }

  var degrees: Float {
    return self  * 180 / .pi
  }
}
0
ethamine