Gire el nodo SCNCamera mirando un objeto alrededor de una esfera imaginaria


Tengo una SCNCamera en la posición(30,30,30) con una SCNLookAtConstraint en un objeto ubicado en la posición(0,0,0). Estoy tratando de hacer que la cámara gire alrededor del objeto en una esfera imaginaria usando un UIPanGestureRecognizer, manteniendo el radio entre la cámara y el objeto. Asumo que debería usar proyecciones de Cuaterniones pero mi conocimiento matemático en esta área es abismal. Mis variables conocidas son la traducción x e y + el radio que estoy tratando de mantener. He escrito el proyecto en Swift, pero una respuesta en Objective-C sería igualmente aceptada(con suerte utilizando un marco estándar Cocoa Touch).

Donde:

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

Aquí está mi código para establecer la escena:

// 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);

Y aquí está el código para el manejo del reconocedor de gestos:

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
    }
}

Gracias!

Author: Gigantic, 2014-09-04

5 answers

Podría ayudar a descomponer su problema en subproblemas.

Estableciendo la escena

Primero, piense en cómo organizar su escena para habilitar el tipo de movimiento que desea. Hablas de mover la cámara como si estuviera unida a una esfera invisible. ¡Usa esa idea! En lugar de tratar de resolver las matemáticas para establecer su cameraNode.position en algún punto en una esfera imaginaria, solo piense en lo que haría para mover la cámara si estuviera unida a una esfera. Es decir, simplemente rotar el esfera.

Si desea rotar una esfera por separado del resto del contenido de la escena, la adjuntaría a un nodo separado. Por supuesto, no es necesario insertar una geometría de esfera en la escena. Simplemente haga un nodo cuyo position sea concéntrico con el objeto que desea que su cámara orbite alrededor, luego conecte la cámara a un nodo hijo de ese nodo. Luego puede rotar ese nodo para mover la cámara. Aquí hay una demostración rápida de eso, sin el manejo de eventos de desplazamiento negocios:

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)

Esto es lo que ves a la izquierda, y una visualización de cómo funciona a la derecha. La esfera a cuadros es cameraOrbit, y el cono verde es cameraNode.

la cámara gira alrededor del cubovisualización de rotación de la cámara

Hay un par de bonificaciones para este enfoque:

  • No es necesario establecer la posición inicial de la cámara en coordenadas cartesianas. Simplemente colóquelo a la distancia que desee a lo largo del eje z. Dado que cameraNode es un nodo hijo de cameraOrbit, su propia posición permanece constante the la cámara se mueve debido a la rotación de cameraOrbit.
  • Mientras solo quieras que la cámara apunte al centro de esta esfera imaginaria, no necesitas una restricción de mirada. La cámara apunta en la dirección-Z del espacio en el que está move si la mueve en la dirección +Z, luego gira el nodo padre, la cámara siempre apuntará al centro del nodo padre (es decir, el centro de rotación).

Manejo de entrada

Ahora que tienes tu escena diseñada para cámara rotación, convertir eventos de entrada en rotación es bastante fácil. Qué tan fácil depende del tipo de control que buscas:

  • Buscando rotación arcball? (Es ideal para la manipulación directa, ya que puede sentir que está empujando físicamente un punto en el objeto 3D.) Hay algunas preguntas y respuestas sobre eso ya en SO most la mayoría de ellos usan GLKQuaternion. (ACTUALIZACIÓN: Los tipos GLK son "sorta" disponibles en Swift 1.2 / Xcode 6.3. Antes de esas versiones se puede haga sus cálculos en ObjC a través de un encabezado puente.)
  • Para una alternativa más simple, puede asignar los ejes x e y de su gesto a los ángulos de guiñada y tono de su nodo. No es tan elegante como la rotación de arcball, pero es bastante fácil de implementar all todo lo que necesita hacer es elaborar una conversión de puntos a radianes que cubra la cantidad de rotación que busca.

De cualquier manera, puede omitir parte del boilerplate de gesture recognizer y obtener algunos comportamientos interactivos útiles al usando UIScrollView en su lugar. (No es que no sea útil quedarse con los reconocedores de gestos this esto es solo una alternativa fácil de implementar.)

Coloque una encima de su SCNView (sin poner otra vista dentro de ella para ser desplazada) y establezca su contentSize a un múltiplo de su tamaño de fotograma... luego, durante el desplazamiento puede asignar el contentOffset su 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
}

Por un lado, tienes que hacer un poco más de trabajo para desplazamiento infinito si quieres girar sin fin en uno o en ambas direcciones. Por otro lado, obtienes una buena inercia estilo desplazamiento y comportamientos de rebote.

 83
Author: rickster,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:10:30

Hey me encontré con el problema el otro día y la solución que se me ocurrió es bastante simple, pero funciona bien.

Primero creé mi cámara y la agregué a mi escena de la siguiente manera:

    // 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

Entonces hice una variable de clase CGPoint slideVelocity. Y creó un UIPanGestureRecognizer y un y en su devolución de llamada puse lo siguiente:

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

Entonces tengo este método que se llama cada fotograma. Tenga en cuenta que uso GLKit para las matemáticas del cuaternión.

-(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;
    }
}

Este código da rotación infinita de Arcball con velocidad, que creo que es lo que estás buscando. Además, no necesita el SCNLookAtConstraint con este método. De hecho, eso probablemente lo estropeará, así que no hagas eso.

 9
Author: WolfLink,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-09-18 05:06:29

Si desea implementar la respuesta de rickster utilizando un reconocedor de gestos, debe guardar la información de estado, ya que solo se le dará una traducción relativa al comienzo del gesto. He añadido dos vars a mi clase

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

E implementó su código de rotación de la siguiente manera:

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
Author: JuJoDi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-03-08 21:40:06

Tal vez esto podría ser útil para los lectores.

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
Author: Lorenzo Andraghetti,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-09 09:06:38

Después de intentar implementar estas soluciones (en Objective-C) me di cuenta de que Scene Kit en realidad hace esto mucho más fácil que hacer todo esto. SCNView tiene una propiedad sweet llamada allowsCameraControl que coloca los reconocedores de gestos apropiados y mueve la cámara en consecuencia. El único problema es que no es la rotación de arcball lo que estás buscando, aunque eso se puede agregar fácilmente creando un nodo hijo, colocándolo donde quieras y dándole una SCNCamera. Por ejemplo:

    _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
Author: sts54,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-01-29 18:11:02