旋转SCNCamera节点,查看围绕虚拟球体的对象

我在位置(30,30,30)有一个SCNCamera,SCNLookAtConstraint位于位置(0,0,0)的对象。 我试图让摄像头在使用UIPanGestureRecognizer的假想球体上围绕对象旋转,同时保持摄像头和对象之间的半径。 我假设我应该使用四元数预测,但是我在这个领域的math知识是糟糕的。 我已知的variables是x&y翻译+我试图保留的半径。 我已经在Swift中编写了这个项目,但Objective-C中的答案会被同样接受(希望使用标准的Cocoa Touch Framework)。

哪里:

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

这是我设置场景的代码:

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

这里是手势识别器处理的代码:

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

谢谢!

这可能有助于将你的问题分解成几个子问题。

设置场景

首先,考虑如何组织你的场景来启用你想要的运动。 你谈论移动相机,就好像它连接到一个看不见的球体。 使用这个想法! 而不是试图算出math设置你的cameraNode.position点到一个假想的球体上的某个点,只要想想你会做什么来移动相机,如果它连接到一个球体。 那就是旋转球体。

如果你想旋转一个独立的场景内容的其余部分,你可以把它附加到一个单独的节点。 当然,你并不需要在你的场景中插入一个球体几何graphics 。 只需制作一个节点,其position与您希望摄像机绕过的对象同心,然后将摄像机连接到该节点的子节点。 然后,您可以旋转该节点来移动相机。 下面是一个快速演示,缺less滚动事件处理业务:

 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) 

以下是您在左侧看到的内容,以及如何在右侧显示它的可视化。 方格的球体是cameraOrbit ,绿色的锥体是cameraNode

相机围绕立方体旋转相机旋转可视化

这种方法有几个奖金:

  • 您不必在笛卡尔坐标中设置初始相机位置。 只要把它放在你想要的任何距离沿z轴。 由于cameraNodecameraNode的子节点, cameraOrbit它自己的位置保持不变 – 由于cameraOrbit的旋转,相机移动。
  • 只要你想让摄像机指向这个想象的球体的中心,你就不需要看看约束。 摄像机指向所在空间的-Z方向 – 如果沿+ Z方向移动摄像机,然后旋转父节点,摄像机将始终指向父节点的中心(即旋转中心) 。

处理input

现在,您已经掌握了用于相机旋转的场景,将input事件转换为旋转非常容易。 究竟是多么容易取决于你在什么样的控制之后:

  • 寻找弧球旋转? (对于直接操作来说很好,因为你可以感觉到你正在物理上推动3D对象上的一个点)。关于这个问题已经有了一些问题和答案 – 其中大多数使用GLKQuaternion 。 ( 更新: GLKtypes是Swift 1.2 / Xcode 6.3中的“sorta”。在这些版本之前,你可以通过一个桥接头在ObjC中进行math计算。
  • 对于更简单的替代方法,您可以将手势的x轴和y轴映射到节点的偏航angular和俯仰angular。 它不像arcball旋转那样漂亮,但是实现起来非常简单 – 你只需要做一个点到弧度的转换就可以了,这个转换可以覆盖你之后的旋转量。

无论哪种方式,您可以跳过一些手势识别器样板,并通过使用UIScrollView来获得一些方便的交互式行为。 (并不是说用手势识别器没有用处 – 这只是一个容易实现的select。)

将一个放在SCNView (不要把另一个视图放在它里面滚动),并将其contentSize设置为它的帧大小的倍数…然后在滚动期间,可以将contentOffset映射到你的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 } 

一方面,如果你想在一个或两个方向上无休止地旋转,你必须为无限滚动做更多的工作。 另一方面,你会得到很好的滚动式惯性和反弹行为。

嘿,我有一天遇到了问题,我提出的解决scheme是相当简单,但效果很好。

首先,我创build了我的相机并将其添加到我的场景中,如下所示:

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

然后我做了一个CGPoint slideVelocity类的variables。 并创build了一个UIPanGestureRecognizer和一个和它的callback我把以下内容:

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

然后我有这个方法被称为每一帧。 请注意,我使用GLKit四元math。

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

这段代码给出无限的Arcball旋转速度,我相信你正在寻找。 此外,您不需要使用此方法的SCNLookAtConstraint 。 事实上,这可能会搞砸了,所以不要这样做。

如果您想使用手势识别器来实现rickerster的回答,则必须保存状态信息,因为您只会获得相对于手势开始的翻译。 我在课堂上增加了两个变数

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

并执行他的旋转代码如下:

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

也许这可能对读者有用。

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

在尝试实现这些解决scheme(在Objective-C中)后,我意识到Scene Kit实际上比这么做要容易得多。 SCNView有一个甜蜜的属性叫allowedCameraControl放入适当的手势识别器,并相应地移动相机。 唯一的问题是,它不是你正在寻找的弧形旋转,但是可以通过创build一个子节点,将它放在任何你想要的地方,并给它一个SCNCamera来轻松添加。 例如:

  _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];