ios 旋转 SCNCamera 节点,观察虚拟球体周围的物体

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/25654772/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-31 02:11:37  来源:igfitidea点击:

Rotate SCNCamera node looking at an object around an imaginary sphere

iosswiftscenekit

提问by Danny Bravo

I've got an SCNCamera at position(30,30,30) with a SCNLookAtConstraint on an object located at position(0,0,0). I'm trying to get the camera to rotate around the object on an imaginary sphere using A UIPanGestureRecognizer, while maintaining the radius between the camera and the object. I'm assuming I should use Quaternion projections but my math knowledge in this area is abysmal. My known variables are x & y translation + the radius I am trying to keep. I've written the project in Swift but an answer in Objective-C would be equally accepted (Hopefully using a standard Cocoa Touch Framework).

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

Where:

在哪里:

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

Here's my code for setting the scene:

这是我设置场景的代码:

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

And here is the code for the gesture recogniser handling:

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

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

Thanks!

谢谢!

回答by rickster

It might help to break down your issue into subproblems.

它可能有助于将您的问题分解为子问题。

Setting the Scene

设置场景

First, think about how to organize your scene to enable the kind of motion you want. You talk about moving the camera as if it's attached to an invisible sphere. Use that idea! Instead of trying to work out the math to set your cameraNode.positionto some point on an imaginary sphere, just think about what you would do to move the camera if it were attached to a sphere. That is, just rotate the sphere.

首先,考虑如何组织场景以启用所需的运动类型。你谈到移动相机,就好像它附着在一个不可见的球体上一样。用这个主意!与其尝试计算将您设置cameraNode.position到假想球体上的某个点的数学方法,不如想想如果将相机连接到球体上您会如何移动它。也就是说,只需旋转球体。

If you wanted to rotate a sphere separately from the rest of your scene contents, you'd attach it to a separate node. Of course, you don't actually need to insert a sphere geometryinto your scene. Just make a node whose positionis concentric with the object you want your camera to orbit around, then attach the camera to a child node of that node. Then you can rotate that node to move the camera. Here's a quick demo of that, absent the scroll-event handling business:

如果您想将球体与场景内容的其余部分分开旋转,您可以将它附加到一个单独的节点。当然,您实际上并不需要在场景中插入球体几何体。只需制作一个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)

Here's what you see on the left, and a visualization of how it works on the right. The checkered sphere is cameraOrbit, and the green cone is cameraNode.

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

camera rotate around cubecamera rotate visualization

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

There's a couple of bonuses to this approach:

这种方法有几个好处:

  • You don't have to set the initial camera position in Cartesian coordinates. Just place it at whatever distance you want along the z-axis. Since cameraNodeis a child node of cameraOrbit, its own position stays constant -- the camera moves due to the rotation of cameraOrbit.
  • As long as you just want the camera pointed at the center of this imaginary sphere, you don't need a look-at constraint. The camera points in the -Z direction of the space it's in -- if you move it in the +Z direction, then rotate the parent node, the camera will always point at the center of the parent node (i.e. the center of rotation).
  • 您不必在笛卡尔坐标中设置初始相机位置。只需将其沿 z 轴放置在您想要的任何距离即可。由于cameraNode是 的子节点cameraOrbit,它自己的位置保持不变——相机由于 的旋转而移动cameraOrbit
  • 只要您只希望相机指向这个假想球体的中心,您就不需要注视约束。相机指向它所在空间的-Z方向——如果你在+Z方向移动它,然后旋转父节点,相机将始终指向父节点的中心(即旋转中心) .

Handling Input

处理输入

Now that you've got your scene architected for camera rotation, turning input events into rotation is pretty easy. Just how easy depends on what kind of control you're after:

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

  • Looking for arcball rotation? (It's great for direct manipulation, since you can feel like you're physically pushing a point on the 3D object.) There are some questions and answersabout that already on SO -- most of them use GLKQuaternion. (UPDATE:GLK types are "sorta" available in Swift 1.2 / Xcode 6.3. Prior to those versions you can do your math in ObjC via a bridging header.)
  • For a simpler alternative, you can just map the x and y axes of your gesture to the yaw and pitch angles of your node. It's not as spiffy as arcball rotation, but it's pretty easy to implement -- all you need to do is work out a points-to-radians conversion that covers the amount of rotation you're after.
  • 寻找弧球旋转?(这对于直接操作非常有用,因为您会感觉自己在物理上推动 3D 对象上的一个点。)SO 上已经有一些关于此的问题和答案- 其中大多数使用GLKQuaternion. (更新:GLK 类型在 Swift 1.2 / Xcode 6.3 中“有点”可用。在这些版本之前,您可以通过桥接头在 ObjC 中进行数学计算。)
  • 对于更简单的替代方案,您可以将手势的 x 轴和 y 轴映射到节点的偏航角和俯仰角。它不像弧球旋转那么漂亮,但它很容易实现——你需要做的就是计算出一个点到弧度的转换,它涵盖了你所追求的旋转量。

Either way, you can skip some of the gesture recognizer boilerplate and gain some handy interactive behaviors by using UIScrollViewinstead. (Not that there isn't usefulness to sticking with gesture recognizers -- this is just an easily implemented alternative.)

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

Drop one on top of your SCNView(without putting another view inside it to be scrolled) and set its contentSizeto a multiple of its frame size... then during scrolling you can map the contentOffsetto your eulerAngles:

将一个放在您的顶部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
}

On the one hand, you have to do a bit more work for infinite scrollingif you want to spin endlessly in one or both directions. On the other, you get nice scroll-style inertia and bounce behaviors.

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

回答by WolfLink

Hey I ran into the problem the other day and the solution I came up with is fairly simple but works well.

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

First I created my camera and added it to my scene like so:

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

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

Then I made a CGPoint slideVelocityclass variable. And created a UIPanGestureRecognizerand a and in its callback I put the following:

然后我做了一个CGPoint slideVelocity类变量。并创建了 aUIPanGestureRecognizer和 a 并在其回调中添加了以下内容:

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

Then I have this method that is called every frame. Note that I use GLKitfor quaternion math.

然后我有这个方法,每帧都被调用。请注意,我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;
    }
}

This code gives infinite Arcball rotation with velocity, which I believe is what you are looking for. Also, you don't need the SCNLookAtConstraintwith this method. In fact, that will probably mess it up, so don't do that.

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

回答by JuJoDi

If you want to implement rickster's answer using a gesture recognizer, you have to save state information as you'll only be given a translation relative to the beginning of the gesture. I added two vars to my class

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

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

And implemented his rotate code as follows:

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

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

回答by Lorenzo Andraghetti

Maybe this could be useful for readers.

也许这对读者有用。

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

回答by ethamine

There's no need to save the state anywhere but the node itself. The code which uses some sort of width ratio behaves weirdly when you scroll back and forth repeatedly, and other code here looks overcomplicated. I came up with a different (and I believe a better one) solution for gesture recognizers, based on @rickster's approach.

除了节点本身,没有必要在任何地方保存状态。当您反复来回滚动时,使用某种宽度比率的代码表现得很奇怪,而这里的其他代码看起来过于复杂。基于@rickster 的方法,我为手势识别器提出了一种不同的(我相信是更好的)解决方案。

UIPanGestureRecognizer:

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:

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

I used velocity, as with translationwhen you slow down the touch it would still be the same event, causing the camera to whirl very fast, not what you'd expect.

我使用了velocity,与平移一样,当您减慢触摸速度时,它仍然是同一事件,导致相机旋转得非常快,而不是您所期望的。

panModifierand pinchModifierare simple constant numbers which you can use to adjust responsiveness. I found the optimal values to be 100and 15respectively.

panModifier并且pinchModifier是简单的常数,您可以使用它们来调整响应能力。我发现最佳值分别为10015

MaxZoomOutand MaxZoomInare constants as well and are exactly what they appear to be.

MaxZoomOutMaxZoomIn也是常数,并且正是它们看起来的样子。

I also use an extension on Float to convert degrees to radians and vice-versa.

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

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

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

回答by sts54

After trying to implement these solutions (in Objective-C) I realized that Scene Kit actually makes this a lot easier than doing all of this. SCNView has a sweet property called allowsCameraControlthat puts in the appropriate gesture recognizers and moves the camera accordingly. The only problem is that it's not the arcball rotation that you're looking for, although that can be easily added by creating a child node, positioning it wherever you want, and giving it a SCNCamera. For example:

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