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!
Il pourrait être utile de décomposer votre problème en sous-problèmes.
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
.
Il y a quelques bonus à cette approche:
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
. 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:
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.)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.
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.
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
}
}
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.
}
}
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];
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
}
}