【问题标题】:Rotate SCNCamera node looking at an object around an imaginary sphere旋转 SCNCamera 节点,观察假想球体周围的对象
【发布时间】:2014-10-28 13:42:25
【问题描述】:

我在位置 (30,30,30) 有一个 SCNCamera,在位置 (0,0,0) 的对象上有一个 SCNLookAtConstraint。我正在尝试使用 A UIPanGestureRecognizer 让相机围绕假想球体上的对象旋转,同时保持相机和对象之间的半径。我假设我应该使用四元数投影,但我在这方面的数学知识很糟糕。我已知的变量是 x & y 平移 + 我要保持的半径。我已经用 Swift 编写了这个项目,但是同样可以接受 Objective-C 中的答案(希望使用标准的 Cocoa Touch 框架)。

地点:

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

谢谢!

【问题讨论】:

    标签: ios swift scenekit


    【解决方案1】:

    将您的问题分解为子问题可能会有所帮助。

    设置场景

    首先,考虑如何组织您的场景以实现您想要的那种运动。您谈到移动相机,就好像它连接到一个不可见的球体一样。使用这个想法!与其尝试计算将您的cameraNode.position 设置为假想球体上的某个点,不如考虑一下如果将相机连接到球体上您会做些什么来移动相机。也就是说,只需旋转球体即可。

    如果您想将球体与场景内容的其余部分分开旋转,请将其附加到单独的节点。当然,您实际上不需要在场景中插入sphere geometry。只需创建一个节点,其position 与您希望相机环绕的对象同心,然后将相机附加到该节点的子节点。然后您可以旋转该节点以移动相机。这是一个快速演示,没有滚动事件处理业务:

    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 轴放置在您想要的任何距离处。由于cameraNodecameraOrbit 的子节点,所以它自己的位置保持不变——相机由于cameraOrbit 的旋转而移动。
    • 只要您只希望相机指向这个假想球体的中心,就不需要注视约束。相机指向它所在空间的-Z方向——如果你在+Z方向移动它,然后旋转父节点,相机将始终指向父节点的中心(即旋转中心) .

    处理输入

    现在您已经为摄像机旋转构建了场景,将输入事件转换为旋转非常容易。简单程度取决于您所追求的控制类型:

    • 寻找轨迹球旋转? (这对于直接操作非常有用,因为您可以感觉到您在物理上推动 3D 对象上的一个点。)SO 上已经有一些questions and answers 与此相关——其中大多数使用GLKQuaternion。 (更新: GLK 类型在 Swift 1.2 / Xcode 6.3 中“分类”可用。在这些版本之前,您可以通过桥接头在 ObjC 中进行数学运算。)
    • 对于更简单的替代方案,您可以将手势的 x 和 y 轴映射到节点的偏航角和俯仰角。它不像轨迹球旋转那样漂亮,但它很容易实现 - 您需要做的就是计算出涵盖您所追求的旋转量的点到弧度的转换。

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

    在您的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
    }
    

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

    【讨论】:

    • 感谢您提供如此好的答案。我花了几个小时计算坐标,但由于某种原因,SceneKit 的定位存在错误,所以这节省了我的时间。
    • @rickster 很抱歉打扰您...如果您有时间和兴趣,请用您的精神力量考虑这个问题:stackoverflow.com/questions/36190789/…
    【解决方案2】:

    嘿,我前几天遇到了这个问题,我想出的解决方案相当简单,但效果很好。

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

        // 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 类变量。并创建了一个UIPanGestureRecognizer 和一个并在其回调中我输入了以下内容:

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

    然后我有这个方法,每帧都调用一次。请注意,我使用GLKit 进行四元数数学运算。

    -(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。事实上,这可能会搞砸,所以不要那样做。

    【讨论】:

    • 还在学习中……viewSlideDivisor的类型和值是什么?
    • 啊我忘了说! viewSlideDivisor 是我根据屏幕大小定义的常量浮点数。它的值会影响每次滑动的影响程度。
    • 在这个例子中你会如何防止滚动?我用它来控制宇宙飞船周围的相机,但会以我没想到的方式滚动(我相信 - 我还是 3d 的新手)。
    • roll 可能不是正确的术语,主要是希望船看起来总是与地平线对齐并且相机围绕船旋转。似乎有些倾斜,我不知道如何解决它。
    • 尝试修改并跟踪 EulerAngles。修改 X 和 Y 欧拉角,但不修改 Z 角。
    【解决方案3】:

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

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

    【讨论】:

    • 有时它会向与平移相反的方向移动
    【解决方案4】:

    也许这对读者有用。

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

    【讨论】:

      【解决方案5】:

      除了节点本身之外,不需要在任何地方保存状态。 当您反复来回滚动时,使用某种宽度比的代码表现得很奇怪,这里的其他代码看起来过于复杂。 基于@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
          }
        }
      

      我使用 velocity,与 translation 一样,当你放慢触摸速度时,它仍然是相同的事件,导致相机旋转得非常快,而不是你的'期待。

      panModifierpinchModifier 是简单的常数,可用于调整响应速度。我发现最佳值分别是 10015

      MaxZoomOutMaxZoomIn 也是常量,它们看起来就是这样。

      我还使用 Float 的扩展将度数转换为弧度,反之亦然。

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

      【讨论】:

        【解决方案6】:

        在尝试实现这些解决方案(在 Objective-C 中)之后,我意识到 Scene Kit 实际上使这比做所有这些都容易得多。 SCNView 有一个名为allowsCameraControl 的甜蜜属性,它可以放入适当的手势识别器并相应地移动相机。唯一的问题是它不是您要寻找的轨迹球旋转,尽管可以通过创建子节点、将其放置在您想要的任何位置并给它一个 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];
        

        【讨论】:

        • 我不清楚allowsCameraControl 在需要某些特定的相机行为(例如轨迹球旋转)时会提供什么帮助;据我了解,它不会影响场景中的任何摄像机。您能否详细说明一下您是如何使用它的?
        • 我的理解是它操纵了SCNView的当前观点。从文档中,“此操作不会修改场景图中已存在的相机对象或包含它们的节点。此属性的默认值为 NO。”当您像我在上面所做的那样将allowCameraControl 添加到SCNView 时,场景会根据包含您的相机的任何节点进行初始化和显示,否则会如此。 Scenekit 会自动(在幕后,这些函数不会在您的代码中弹出)添加平移/滑动手势识别器并根据这些手势移动相机。
        • allowsCameraControl 本质上是调试功能,排除了任何类型的自定义
        • 对于所请求的内容,这不是一个可行的解决方案。正如@AlfieHanssen 所说,它本质上是用于调试的,并且您绝对无法控制用户可以做什么或不可以做什么。
        猜你喜欢
        • 2016-03-02
        • 2014-12-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-03-18
        相关资源
        最近更新 更多