摄像机节点如何随着iOS设备移动而旋转?

13
这是一个360度视频播放器项目。
我向根节点添加了一个摄像机节点(SCNNode),将该节点置于SCNSphere的中心(0,0,0),现在它可以播放视频。
现在我需要使用设备运动,当设备移动时,我需要旋转摄像机。不仅仅是旋转某个角度。(不只是设备移动,当我握着设备移动时,也被视为设备移动。因为我发现如果我使用deviceMotion.attitude.roll,摄像机节点只会在设备自行移动时移动,而不是在我围绕设备转动时移动)
当设备位于(x1,y1,z1)位置时,摄像机节点会随着设备的移动而旋转,当设备再次位于(x1,y1,z1)位置时,视图应与我们上次离开时相同。
这是我所做的:
if (self.motionManager.gyroAvailable) {
        self.motionManager.gyroUpdateInterval = 1.0/60.0;
        [self.motionManager startGyroUpdatesToQueue:self.queue withHandler:^(CMGyroData *gyroData, NSError *error){
            if (error) {
                [self.motionManager stopGyroUpdates];
                NSLog(@"Gyroscope encountered error:%@",error);
            }else {
                CGFloat tempX = 0;
                CGFloat tempY = 0;
                CGFloat tempZ = 0;
                if ((fabs(gyroData.rotationRate.y)/60) > 0.002) {
                    tempY = gyroData.rotationRate.y/60;
                }
                tempX = gyroData.rotationRate.x/60;
                tempZ = gyroData.rotationRate.z/60;
                [self.cameraNode runAction:[SCNAction rotateByX:-tempY y:tempX z:tempZ duration:0]];
            }
        }];
    }else {
        NSLog(@"This device has no gyroscope");
    }

这个数学有可能是错的,我不知道为什么要除以60,但在使用 [self.cameraNode runAction:[SCNAction rotateByX:-tempY y:tempX z:tempZ duration:0]]; 时似乎是我需要的最接近的值。

问题是什么?

  1. 我应该使用哪个数据?

CMGyroData 还是 CMDeviceMotion?如果使用 CMDeviceMotion,应该使用哪个具体的值呢?deviceMotion.attitudedeviceMotion.attitude.quaternion 还是 deviceMotion.gravity

  1. 旋转一个相机节点的正确方法是什么?有很多方法,我不确定。

+ (SCNAction *)rotateByX:(CGFloat)xAngle y:(CGFloat)yAngle z:(CGFloat)zAngle duration:(NSTimeInterval)duration;

+ (SCNAction *)rotateToX:(CGFloat)xAngle y:(CGFloat)yAngle z:(CGFloat)zAngle duration:(NSTimeInterval)duration;

3个回答

29

设备运动是最好的选择,因为它将多个传感器结合在一起,并以有意义的方式融合数据。最好避免使用欧拉角,因为它们容易受到万向节锁定的影响。相反,使用四元数并用它们设置相机方向。

我创建了一个Swift类扩展,用于CMDeviceMotion,可以让您确定设备正在查看什么(适用于屏幕的任何方向)。

从那里开始,旋转相机就很简单了。首先,在视图出现时或其他适当的位置设置设备运动:

if motionManager.deviceMotionAvailable {

    motionManager.deviceMotionUpdateInterval = 0.017
    motionManager.startDeviceMotionUpdatesToQueue(NSOperationQueue(), withHandler: deviceDidMove)
}

然后在你的deviceDidMove实现中处理这些更新,使用之前提到的CMDeviceMotion扩展:

func deviceDidMove(motion: CMDeviceMotion?, error: NSError?) {

    if let motion = motion {

        yourCameraNode.orientation = motion.gaze(atOrientation: UIApplication.sharedApplication().statusBarOrientation)
    }
}

现在您的相机应该能够跟随您设备在空间中的方向。


这应该被标记为正确答案。为了完整起见,可能很好地(1)向您的类扩展添加注释以解释它具体执行的操作,并且(2)将框架导入语句添加到文件顶部。 - Alfie Hanssen
在您的解决方案中,当您构建最终的SCNQuaternion时,您能否解释为什么有时需要交换x和y的符号和值?我理解这是必要的,以便在所有情况下都能正常工作。我们是否可以使用另一个乘法向量来代替这个呢? - Alfie Hanssen
@AlfieHanssen 这些符号和交换是为了校正设备的姿态。如果有更聪明的计算方法,我不知道。但我同意,这会很好。 - Travis
@Travis 感谢您非常有用的扩展。您有没有想法如何阻止我的相机中的滚动组件?http://stackoverflow.com/q/39026329/235297 - Ortwin Gentz

3

Swift 5.0 / iOS 13 更新

使用最初答案中提供的扩展功能,您可以轻松地执行以下操作:

override func viewDidAppear(_ animated: Bool) {
    // let motionManager = CMMotionManager() // definition made globally
    
    // CoreMotion
    motionManager.startDeviceMotionUpdates()
    motionManager.deviceMotionUpdateInterval = 1.0 / 60.0
    
    let interfaceOrientation = UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.windowScene?.interfaceOrientation // for iOS13 and later
    
    // .xTrueNorthZVertical // always aligns to the real north
    // .xArbitraryZVertical // will use this one for initial direction
    motionManager.startDeviceMotionUpdates(using: .xArbitraryZVertical, to: OperationQueue.main, withHandler: { (motion: CMDeviceMotion?, err: Error?) in
        guard let m = motion else { return }
        // self.cameraNode.orientation = m.gaze(atOrientation: UIApplication.shared.statusBarOrientation) // before iOS 13
        self.cameraNode.orientation = m.gaze(atOrientation: interfaceOrientation!) // for iOS13 and later
    })
}

0

对于使用初始答案的扩展程序的iOS 15

override func viewDidAppear(_ animated: Bool) {
    // let motionManager = CMMotionManager() // definition made globally
// CoreMotion
motionManager.startDeviceMotionUpdates()
motionManager.deviceMotionUpdateInterval = 1.0 / 60.0

//this following line is changed to avoid deprecated term 
let interfaceOrientation = keyWindow!.windowScene?.interfaceOrientation

// .xTrueNorthZVertical // always aligns to the real north
// .xArbitraryZVertical // will use this one for initial direction
motionManager.startDeviceMotionUpdates(using: .xArbitraryZVertical, to: OperationQueue.main, withHandler: { (motion: CMDeviceMotion?, err: Error?) in
    guard let m = motion else { return }
    // self.cameraNode.orientation = m.gaze(atOrientation: UIApplication.shared.statusBarOrientation) // before iOS 13
    self.cameraNode.orientation = m.gaze(atOrientation: interfaceOrientation!) // for iOS13 and later
})
}

extension (change name UIApplication) {

var keyWindow: UIWindow? {
    // Get connected scenes
    return UIApplication.shared.connectedScenes
        // Keep only active scenes, onscreen and visible to the user
        .filter { $0.activationState == .foregroundActive }
        // Keep only the first `UIWindowScene`
        .first(where: { $0 is UIWindowScene })
        // Get its associated windows
        .flatMap({ $0 as? UIWindowScene })?.windows
        // Finally, keep only the key window
        .first(where: \.isKeyWindow)
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接