Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
921 views
in Technique[技术] by (71.8m points)

ios - SceneKit view is rendered backwards

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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...