ios swift scenekit

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



swift scenekit (6)

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 dulce propiedad llamada allowsCameraControl que coloca los reconocedores de gestos apropiados y mueve la cámara en consecuencia. El único problema es que no está buscando la rotación de la bola de arco, aunque eso se puede agregar fácilmente creando un nodo secundario, posicionándolo donde desee 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];

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 A UIPanGestureRecognizer, mientras mantengo el radio entre la cámara y el objeto. Supongo que debería usar las proyecciones de Quaternion, pero mi conocimiento matemático en esta área es abismal. Mis variables conocidas son la traducción x & y + el radio que intento mantener. He escrito el proyecto en Swift, pero una respuesta en Objective-C sería igualmente aceptada (ojalá que use un Cocoa Touch Framework estándar).

Dónde:

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

Aquí está mi código para configurar 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!


Hola, 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 así:

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

Luego hice una variable de clase CGPoint slideVelocity . Y creé un UIPanGestureRecognizer y a 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 cuadro. Tenga en cuenta que uso GLKit para las matemáticas de 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; } }

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


No es necesario guardar el estado en otro lugar que no sea el nodo en sí. El código que usa algún tipo de relación de ancho se comporta de manera extraña cuando se desplaza hacia adelante y hacia atrás repetidamente, y otro código aquí parece demasiado complicado. Se me ocurrió una solución diferente (y creo que es mejor) para los reconocedores de gestos, basada en el enfoque 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 } }

Utilicé la velocidad , ya que con la traducción cuando desaceleras el toque, seguiría siendo el mismo evento, haciendo que la cámara gire muy rápido, no es lo que esperas.

panModifier y pinchModifier son números constantes simples que puede usar para ajustar la capacidad de respuesta. Encontré que los valores óptimos son 100 y 15 respectivamente.

MaxZoomOut y MaxZoomIn son constantes y son exactamente lo que parecen ser.

También uso una extensión en Float para convertir grados a radianes y viceversa.

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


Podría ayudar dividir su problema en subproblemas.

Preparando la escena

Primero, piense en cómo organizar su escena para permitir 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 posición cameraNode.position 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, solo gira la esfera.

Si quisiera rotar una esfera por separado del resto de los contenidos de su escena, la adjuntaría a un nodo separado. Por supuesto, en realidad no necesita insertar una geometría de esfera en su escena. Simplemente haga un nodo cuya position sea ​​concéntrica con el objeto sobre el que desea que su cámara orbite, luego conecte la cámara a un nodo hijo de ese nodo. Luego puede girar ese nodo para mover la cámara. Aquí hay una demostración rápida de eso, en ausencia del negocio de manejo de eventos de desplazamiento:

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 .

Hay un par de bonificaciones en este enfoque:

  • No tiene que 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. Como cameraNode es un nodo hijo de cameraOrbit , su propia posición se mantiene constante: la cámara se mueve debido a la rotación de cameraOrbit .
  • Mientras solo desee que la cámara apunte al centro de esta esfera imaginaria, no necesita una restricción de observación. La cámara apunta en la dirección -Z del espacio en el que se encuentra: si la mueve en la dirección + Z, luego gira el nodo principal, la cámara siempre apuntará al centro del nodo principal (es decir, el centro de rotación) .

Manejo de entrada

Ahora que tiene su escena diseñada para la rotación de la cámara, convertir eventos de entrada en rotación es bastante fácil. Cuán fácil depende de qué tipo de control buscas:

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

De cualquier manera, puede omitir algunas de las repeticiones del reconocedor de gestos y obtener algunos comportamientos interactivos útiles utilizando UIScrollView lugar. (No es que no sea útil quedarse con los reconocedores de gestos; esta es solo una alternativa fácil de implementar).

SCNView uno en la parte superior de su SCNView (sin poner otra vista dentro de él para desplazarse) y configure su contentSize en un múltiplo de su tamaño de marco ... luego, durante el desplazamiento, puede asignar el contentOffset a 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, debe hacer un poco más de trabajo para el desplazamiento infinito si desea girar sin parar en una o ambas direcciones. Por otro lado, obtienes una agradable inercia de estilo de desplazamiento y comportamientos de rebote.


Si desea implementar la respuesta de rickster usando 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. Agregué 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 } }


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