SceneKit view is rendered backwards

2019-09-21 23:16发布

I'm attempting my first SceneKit app. My goal is to simulate a view from the surface of the Earth and being able to point the device's camera in any direction and overlay information over the camera view.

To start, I'm simply trying to get the SceneKit camera view to match the device's orientation. To verify that it is working as desired, I am adding a bunch of spheres at specific latitude and longitude coordinates.

Everything is working except for one important issue. The view is mirrored left/right (east/west) from what it should be showing. I've spent hours trying different permutations of adjusting the camera.

Below is my complete test app view controller. I can't figure out the right combination of changes to get the scene to render properly. The sphere at the North Pole is correct. The line of spheres stretching from the North Pole to the equator at my own current longitude appears overhead as expected. It's the other lines of spheres that are incorrect. They are mirrored east/west from what they should be as if the mirror is at my own longitude.

If you want to test this code, create a new Game project with SceneKit. Replace the template GameViewController.swift file with the one below. You also need to add the "Privacy - Location When In Use Description" key to the Info.plist. I also suggest adjusting the for lon in ... line so the numbers either start or end with your own longitude. Then you can see whether the spheres are being drawn on the correct portion of the display. This might also require a slight adjustment to the UIColor(hue: CGFloat(lon + 104) * 2 / 255.0 color.

import UIKit
import QuartzCore
import SceneKit
import CoreLocation
import CoreMotion

let EARTH_RADIUS = 6378137.0

class GameViewController: UIViewController, CLLocationManagerDelegate {
    var motionManager: CMMotionManager!
    var scnCameraArm: SCNNode!
    var scnCamera: SCNNode!
    var locationManager: CLLocationManager!
    var pitchAdjust = 1.0
    var rollAdjust = -1.0
    var yawAdjust = 0.0

    func radians(_ degrees: Double) -> Double {
        return degrees * Double.pi / 180
    }

    func degrees(_ radians: Double) -> Double {
        return radians * 180 / Double.pi
    }

    func setCameraPosition(lat: Double, lon: Double, alt: Double) {
        let yaw = lon
        let pitch = lat

        scnCameraArm.eulerAngles.y = Float(radians(yaw))
        scnCameraArm.eulerAngles.x = Float(radians(pitch))
        scnCameraArm.eulerAngles.z = 0
        scnCamera.position = SCNVector3(x: 0.0, y: 0.0, z: Float(alt + EARTH_RADIUS))
    }

    func setCameraPosition(loc: CLLocation) {
        setCameraPosition(lat: loc.coordinate.latitude, lon: loc.coordinate.longitude, alt: loc.altitude)
    }

    // MARK: - UIViewController methods

    override func viewDidLoad() {
        super.viewDidLoad()

        // create a new scene
        let scene = SCNScene()

        let scnCamera = SCNNode()
        let camera = SCNCamera()
        camera.zFar = 2.5 * EARTH_RADIUS
        scnCamera.camera = camera
        scnCamera.position = SCNVector3(x: 0.0, y: 0.0, z: Float(EARTH_RADIUS))
        self.scnCamera = scnCamera

        let scnCameraArm = SCNNode()
        scnCameraArm.position = SCNVector3(x: 0, y: 0, z: 0)
        scnCameraArm.addChildNode(scnCamera)
        self.scnCameraArm = scnCameraArm

        scene.rootNode.addChildNode(scnCameraArm)

        // create and add an ambient light to the scene
        let ambientLightNode = SCNNode()
        ambientLightNode.light = SCNLight()
        ambientLightNode.light!.type = .ambient
        ambientLightNode.light!.color = UIColor.darkGray
        scene.rootNode.addChildNode(ambientLightNode)

        // retrieve the SCNView
        let scnView = self.view as! SCNView

        // set the scene to the view
        scnView.scene = scene
        //scnView.pointOfView = scnCamera

        // Draw spheres over part of the western hemisphere
        for lon in stride(from: 0, through: -105, by: -15) {
            for lat in stride(from: 0, through: 90, by: 15) {
                let mat4 = SCNMaterial()
                if lat == 90 {
                    mat4.diffuse.contents = UIColor.yellow
                } else if lat == -90 {
                    mat4.diffuse.contents = UIColor.orange
                } else {
                    //mat4.diffuse.contents = UIColor(red: CGFloat(lat + 90) / 255.0, green: CGFloat(lon + 104) * 4 / 255.0, blue: 1, alpha: 1)
                    mat4.diffuse.contents = UIColor(hue: CGFloat(lon + 104) * 2 / 255.0, saturation: 1, brightness: CGFloat(255 - lat * 2) / 255.0, alpha: 1)
                }

                let ball = SCNSphere(radius: 100000)
                ball.firstMaterial = mat4
                let ballNode = SCNNode(geometry: ball)
                ballNode.position = SCNVector3(x: 0.0, y: 0.0, z: Float(100000 + EARTH_RADIUS))

                let ballArm = SCNNode()
                ballArm.position = SCNVector3(x: 0, y: 0, z: 0)
                ballArm.addChildNode(ballNode)
                scene.rootNode.addChildNode(ballArm)

                ballArm.eulerAngles.y = Float(radians(Double(lon)))
                ballArm.eulerAngles.x = Float(radians(Double(lat)))
            }
        }

        // configure the view
        scnView.backgroundColor = UIColor(red: 0, green: 191/255, blue: 255/255, alpha: 1) // sky blue

        locationManager = CLLocationManager()
        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
        let auth = CLLocationManager.authorizationStatus()
        switch auth {
        case .authorizedWhenInUse:
            locationManager.startUpdatingLocation()
        case .notDetermined:
            locationManager.requestWhenInUseAuthorization()
        default:
            break
        }

        motionManager = CMMotionManager()
        motionManager.deviceMotionUpdateInterval = 1 / 30
        motionManager.startDeviceMotionUpdates(using: .xTrueNorthZVertical, to: OperationQueue.main) { (motion, error) in
            if error == nil {
                if let motion = motion {
                    //print("pitch: \(self.degrees(motion.attitude.roll * self.pitchAdjust)), roll: \(self.degrees(motion.attitude.pitch * self.rollAdjust)), yaw: \(self.degrees(-motion.attitude.yaw))")
                    self.scnCamera.eulerAngles.z = Float(motion.attitude.yaw + self.yawAdjust)
                    self.scnCamera.eulerAngles.x = Float(motion.attitude.roll * self.pitchAdjust)
                    self.scnCamera.eulerAngles.y = Float(motion.attitude.pitch * self.rollAdjust)
                }
            }
        }
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        if UIApplication.shared.statusBarOrientation == .landscapeRight {
            pitchAdjust = -1.0
            rollAdjust = 1.0
            yawAdjust = Double.pi
        } else {
            pitchAdjust = 1.0
            rollAdjust = -1.0
            yawAdjust = 0.0
        }
    }

    override var shouldAutorotate: Bool {
        return false
    }

    override var prefersStatusBarHidden: Bool {
        return true
    }

    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        return .landscape
    }

    // MARK: - CLLocationManagerDelegate methods

    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        if status == .authorizedWhenInUse {
            manager.startUpdatingLocation()
        }
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        for loc in locations {
            print(loc)
            if loc.horizontalAccuracy > 0 && loc.horizontalAccuracy <= 100 {
                setCameraPosition(loc: loc)
            }
        }
    }

    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {

    }
}

The changes probably need to be made in some combination of the setCameraPosition method and/or the motionManager.startDeviceMotionUpdates closure at the end of viewDidLoad.

2条回答
Fickle 薄情
2楼-- · 2019-09-21 23:56

Thanks to Hal for his very helpful suggestion for creating an "scn" file from my scene. After viewing that scene in the scene editor, I realized that everything was reversed left/right because the yaw Euler angle (Y-axis) rotation was reverse of what I thought. So when I set the node's eulerAngles.y to the desired longitude, I was rotating in the opposite direction of what I wanted. But since the camera had the same error, the spheres at my location were in the correct location but everything else was reversed left-to-right.

The solution was to negate the longitude value for both the spheres and the camera location.

scnCameraArm.eulerAngles.y = Float(radians(yaw))

needs to be:

scnCameraArm.eulerAngles.y = -Float(radians(yaw))

And:

ballArm.eulerAngles.y = Float(radians(Double(lon)))

needs to be:

ballArm.eulerAngles.y = -Float(radians(Double(lon)))
查看更多
Viruses.
3楼-- · 2019-09-22 00:04

I don't understand precisely what's wrong, but I believe that the way SceneKit uses pitch and yaw in eulerAngles doesn't match the way you're picturing it. I was unable to find chapter/verse that specifies which direction positive pitch, roll, and yaw are.

I was able to get it working (at least I think I've done what you were trying for) by changing the pitch/yaw setup to

    ballArm.eulerAngles.x = -1 * Float(radians(Double(lat)))

I also added some debugging colors and labeling, and archived the scene to .SCN format to load it into the Xcode Scene Editor (I think Xcode 9 will do this for you in the debugger). Putting a name on each node lets you see what's what in the editor. Revised code:

    // Draw spheres over part of the western hemisphere
    for lon in stride(from: 0, through: -105, by: -15) {
        for lat in stride(from: 0, through: 90, by: 15) {
            let mat4 = SCNMaterial()
            if lon == 0 {
                mat4.diffuse.contents = UIColor.black
            } else if lon == -105 {
                mat4.diffuse.contents = UIColor.green
            } else if lat == 90 {
                mat4.diffuse.contents = UIColor.yellow
            } else if lat == 0 {
                mat4.diffuse.contents = UIColor.orange
            } else  {
                mat4.diffuse.contents = UIColor(red: CGFloat(lat + 90) / 255.0, green: CGFloat(lon + 104) * 4 / 255.0, blue: 1, alpha: 1)
                //mat4.diffuse.contents = UIColor(hue: CGFloat(lon + 104) * 2 / 255.0, saturation: 1, brightness: CGFloat(255 - lat * 2) / 255.0, alpha: 1)
                //mat4.diffuse.contents = UIColor.green
            }

            let ball = SCNSphere(radius: 100000)
            ball.firstMaterial = mat4
            let ballNode = SCNNode(geometry: ball)
            ballNode.position = SCNVector3(x: 0.0, y: 0.0, z: Float(100000 + EARTH_RADIUS))

            let ballArm = SCNNode()
            ballArm.position = SCNVector3(x: 0, y: 0, z: 0)
// debugging label
            ballArm.name = "\(lat) \(lon)"
            ballArm.addChildNode(ballNode)
            scene.rootNode.addChildNode(ballArm)

            ballArm.eulerAngles.y = Float(radians(Double(lon)))
            ballArm.eulerAngles.x = -1 * Float(radians(Double(lat)))
        }
    }
    // configure the view
    scnView.backgroundColor = UIColor.cyan

    let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
    let docsFolderURL = urls[urls.count - 1]
    let archiveURL = docsFolderURL.appendingPathComponent("rmaddy.scn")
    let archivePath = archiveURL.path
// for  copy/paste from Simulator's file system
    print (archivePath)  
    let archiveResult = NSKeyedArchiver.archiveRootObject(scene, toFile: archivePath)
    print(archiveResult)

And here's a screenshot of the scene in the Scene Editor. Note the faint orange circle around the sphere for the lat 45/lon -90 node, selected in the nodes listing.

enter image description here

查看更多
登录 后发表回答