旋转SCNCamera节点,查看假想球体周围的对象

Dan*_*avo 36 ios scenekit swift

我在位置(30,30,30)有一个SCNCamera,在位于(0,0,0)位置的对象上有一个SCNLookAtConstraint.我正在尝试使用A UIPanGestureRecognizer让相机围绕虚拟球体上的物体旋转,同时保持相机和物体之间的半径.我假设我应该使用四元数预测,但我在这方面的数学知识很糟糕.我知道的变量是x&y平移+我试图保留的半径.我在Swift中编写了这个项目,但Objective-C中的答案也同样被接受(希望使用标准的Cocoa Touch Framework).

哪里:

private var cubeView : SCNView!;
private var cubeScene : SCNScene!;
private var cameraNode : SCNNode!;
Run Code Online (Sandbox Code Playgroud)

这是我设置场景的代码:

// 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);
Run Code Online (Sandbox Code Playgroud)

以下是手势识别器处理的代码:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

谢谢!

ric*_*ter 100

将问题分解为子问题可能会有所帮助.

设置场景

首先,考虑如何组织场景以实现所需的运动.你谈到移动相机就像它附着在一个看不见的球体上一样.用这个主意!而不是试图计算出你cameraNode.position想要在假想的球体上设置某个点的数学,而只考虑如果将相机连接到球体上,你将如何移动相机.也就是说,只需旋转球体即可.

如果要将球体与场景内容的其余部分分开旋转,则需要将其附加到单独的节点.当然,您实际上不需要在场景中插入球体几何体.只需创建一个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)
Run Code Online (Sandbox Code Playgroud)

这是您在左侧看到的内容,以及它在右侧如何工作的可视化.格子球是cameraOrbit,绿色的锥体是cameraNode.

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

这种方法有几个奖励:

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

处理输入

现在您已经为摄像机旋转设计了场景,将输入事件转换为旋转非常简单.究竟有多简单取决于您所追求的控制程度:

  • 寻找弧球旋转?(这对于直接操作非常有用,因为你可以感觉到你在物理上推动3D对象上的一点.)在SO上已经有一些问题和答案 - 大多数都使用了GLKQuaternion.(更新: GLK类型在Swift 1.2/Xcode 6.3中是"sorta".在这些版本之前,您可以通过桥接头在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
}
Run Code Online (Sandbox Code Playgroud)

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


Wol*_*ink 10

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

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

    // 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
Run Code Online (Sandbox Code Playgroud)

然后我做了一个CGPoint slideVelocity类变量.并创建了一个UIPanGestureRecognizer和一个并在其回调中我提出以下内容:

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

然后我有这个每帧调用的方法.请注意,我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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

  • 啊,我忘记提到了!`viewSlideDivisor`是我根据屏幕大小定义的常量浮点数.它的值会影响每次滑动的效果有多大. (2认同)

JuJ*_*oDi 7

如果您想使用手势识别器实现rickster的答案,则必须保存状态信息,因为您只会获得相对于手势开头的翻译.我在班上加了两个vars

var lastWidthRatio: Float = 0
var lastHeightRatio: Float = 0
Run Code Online (Sandbox Code Playgroud)

并实现了他的旋转代码如下:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 有时它会向与平移相反的方向移动 (2认同)

Lor*_*tti 5

也许这对读者可能有用。

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.
}
}
Run Code Online (Sandbox Code Playgroud)