Rotation nœud SCNCamera regardant un objet autour d'une sphère imaginaire
j'ai un SCNCamera en position(30,30,30) avec un SCNLookAtConstraint sur un objet situé en position(0,0). J'essaie de faire tourner la caméra autour de l'objet sur une sphère imaginaire en utilisant un 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 abyssales. Mes variables connues sont X & Y translation + le rayon que j'essaie de garder. J'ai écrit le projet dans Swift, mais une réponse dans L'Objectif-C serait également acceptée (en utilisant, espérons-le, un cadre standard de Cocoa Touch).
où:
private var cubeView : SCNView!;
private var cubeScene : SCNScene!;
private var cameraNode : SCNNode!;
Voici mon code pour la mise 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 manipulation 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!
5 réponses
il pourrait aider à décomposer votre problème en sous-problèmes.
mise en Scène
tout d'Abord, pensez à la façon d'organiser votre scène pour activer le genre de mouvement que vous voulez. 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 mettre votre cameraNode.position
à un certain point sur une sphère imaginaire, pensez juste à ce que vous feriez pour déplacer la caméra si elle était attaché à une sphère. C'est, il suffit de tourner la sphère.
si vous vouliez tourner une sphère séparément du reste du contenu de votre scène, vous l'attacheriez à un noeud séparé. Bien sûr, vous n'avez pas besoin d'insérer un géométrie sphère dans votre scène. Il suffit de faire un noeud dont position
est concentrique avec l'objet que vous voulez que votre caméra tourne autour, puis attachez la caméra à un noeud enfant de ce noeud. Alors vous pouvez tourner ce noeud pour déplacer la caméra. Voici une démonstration rapide de cela, en l'absence de l'activité de gestion d'événements par 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 comment cela fonctionne à droite. La sphère à damier est cameraOrbit
, et le cône vert est cameraNode
.
il y a quelques bonus à cette approche:
- vous n'avez pas à définir la position initiale de la caméra en coordonnées cartésiennes. Il suffit de le placer à n'importe quelle distance que vous voulez le long de l'axe Z. Depuis
cameraNode
est un nœud enfant decameraOrbit
, sa propre position reste constante-la caméra se déplace en raison de la rotation decameraOrbit
. - tant que vous voulez simplement que la caméra soit pointée au 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 c'est -- si vous le déplacez dans la direction +Z, puis tournez le noeud parent, la caméra pointera toujours au centre du noeud parent (c.-à-d. le centre de rotation).
Traitement Des Données
maintenant que vous avez votre scène architecturée pour la rotation de la caméra, transformer les événements d'entrée en rotation est assez facile. La facilité dépend du type de contrôle que vous recherchez:
- cherche arcball la rotation? (C'est parfait pour la manipulation directe, puisque vous pouvez avoir l'impression de pousser physiquement un point sur l'objet 3D.) Il y a quelques questions et réponses à ce sujet déjà sur SO -- la plupart d'entre eux utilisent
GLKQuaternion
. ( mise à jour: les types GLK sont" sorta " disponibles en Swift 1.2 / Xcode 6.3. Avant ces versions, vous pouvez faire vos maths en ObjC via un en-tête de pont.) - pour une alternative plus simple, vous pouvez simplement cartographier le x et axes de y de votre geste aux angles de lacet et de tangage de votre noeud. Ce n'est pas aussi chic que la rotation arcball, mais c'est assez facile à mettre en œuvre -- tout ce que vous devez faire est de travailler sur une conversion points-à-radians qui couvre la quantité de rotation que vous êtes après.
dans tous les cas, vous pouvez sauter une partie du boilerplate de reconnaissance de geste et d'obtenir quelques comportements interactifs pratiques en utilisant UIScrollView
à la place. (Non pas qu'il ne soit pas utile de rester reconnaissance gestuelle -- c'est juste une alternative facile à mettre en œuvre.)
déposez - en un sur votre SCNView
(sans mettre une autre vue à l'intérieur) et réglez son contentSize
à un multiple de sa taille de cadre... ensuite, pendant le défilement, vous pouvez faire la carte du contentOffset
à 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 avez à faire un peu plus de travail pour infini défilement si vous voulez spin sans fin dans une ou les deux directions. De l'autre, vous obtenez de l'inertie de style scroll et des comportements de rebond.
Hey j'ai rencontré le problème l'autre jour et la solution que j'ai trouvée est assez simple mais fonctionne bien.
J'ai d'abord créé mon appareil photo et je l'ai ajouté à ma scène comme ceci:
// 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
puis j'ai fait une variable de classe CGPoint slideVelocity
. Et a créé un UIPanGestureRecognizer
et un et dans son rappel j'ai mis ce qui suit:
-(void)handlePan:(UIPanGestureRecognizer *)gestureRecognize{
slideVelocity = [gestureRecognize velocityInView:self.view];
}
alors j'ai cette méthode qui s'appelle chaque cadre. Notez que j'utilise GLKit
pour quaternion mathématiques.
-(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 la rotation infinie Arcball avec la vitesse, qui je crois est ce que vous cherchez. De plus, vous n'avez pas besoin du SCNLookAtConstraint
avec cette méthode. En fait, ça va probablement tout gâcher, alors ne fais pas ça.
si vous voulez mettre en œuvre la réponse de rickster en utilisant un reconnaisseur de geste, vous devez sauvegarder les informations d'état car vous ne recevrez une traduction relative au début du geste. J'ai ajouté deux Var à ma classe
var lastWidthRatio: Float = 0
var lastHeightRatio: Float = 0
et a mis en œuvre 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
}
}
peut-être 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.
}
}
après avoir essayé de mettre en œuvre ces solutions (dans Objective-C) j'ai réalisé que Scene Kit rend en fait beaucoup plus facile que de faire tout cela. SCNView a une propriété douce appelée allowsCameraControl qui met dans le geste approprié des reconnaisseurs 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 noeud enfant, en le positionnant où vous voulez, et je lui donne un 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];