Weston
Weston

Reputation: 1491

Rotate an object so that a tapped point faces camera

I am really bad at explaining these situations so bear with me.

image of a globe

What I want is when the user taps the white annotation, that point will scroll to the center (along with the globe)

I would also like to be able to do this programmatically, scrolling to a point when i provide x/y coords for the globe

I am using the following function to calculate the SCNVector3 based on x/y coordinates

func positionForCoordinates(coordinates: CGPoint, radius: CGFloat) -> SCNVector3  {
    let s = coordinates.x
    let t = coordinates.y
    let r = radius

    let x = r * cos(s) * sin(t)
    let y = r * sin(s) * sin(t)
    let z = r * cos(t)

    return SCNVector3(x: Float(x), y: Float(y), z: Float(z))
}

its the math that really is eluding me.

Upvotes: 2

Views: 715

Answers (1)

haxpor
haxpor

Reputation: 2601

Let's assume according to your problem, we knew the following

  • coordinate (latitude/longitude) of annotation that you placed it on the globe, and you want to simulate rotating a globe model to center on it
  • a camera is facing at the center of the screen
  • we will use a camera to rotate around globe model instead of rotating the globe itself

We can take advantage of animatable properties of SCNNode and SCNCamera so this will make it easier for us to do animation and not to manually lerp values in render loop.

First - Set up camera

func setupCamera(scene: SCNScene) {
  cameraOrbit = SCNNode()
  cameraNode = SCNNode()
  camera = SCNCamera()

  // camera stuff
  camera.usesOrthographicProjection = true
  camera.orthographicScale = 10
  camera.zNear = 1
  camera.zFar = 100

  // initially position is far away as we will animate moving into the globe
  cameraNode.position = SCNVector3(x: 0, y: 0, z: 70)
  cameraNode.camera = camera
  cameraOrbit = SCNNode()
  cameraOrbit.addChildNode(cameraNode)
  scene.rootNode.addChildNode(cameraOrbit)
}

Camera node is set with SCNCamera instance via its camera property, and is wrapped inside another node as we will use to manipulate its rotation.

I left code for defining those variables in the class for brevity.

Second - Implement conversion method

We need a method that convert map coordinate (latitude/longitude) to rotation angles for us to plug it into SCNNode.eulerAngles in order to rotate our camera around the globe model.

/**
  Get rotation angle of sphere along x and y direction from input map coordinate to show such location at the center of view.

  - Parameter from: Map coordinate to get rotation angle for sphere

  - Returns: Tuple of rotation angle in form (x:, y:)
*/
func rotationXY(from coordinate: CLLocationCoordinate2D) -> (x: Double, y: Double) {
    // convert map coordiante to texture coordinate
    let v = 0.5 - (coordinate.latitude / 180.0)
    let u = (coordinate.longitude / 360.0) + 0.5

    // convert texture coordinate to rotation angles
    let angleX = (u-0.5) * 2 * Double.pi
    let angleY = (0.5-v) * -Double.pi

    return (x: angleX, y: angleY)
}

From the code, we need to convert from map coordinate to texture coordinate in which when you place a texture ie. diffuse texture onto a sphere to render normally without any modification of rotation angle of globe node, and camera's position is placed along z-axis (ie. x=0, y=0, z=N), the center of such texture will be shown at the camera. So in the equation, we take into account 0.5 to accommodate on this.

After that we convert results into angles (radians). Along x direction, we could rotate sphere for 360 degrees to wrap around it fully. For y direction, it takes 180 degrees to wrap around it.

Please note I didn't get rid of 0.5 for both case as to make it as-is in each conversion, and for clearer to see along with explanation. You can simplify it by removing from the code.

Third - Animate

As a plus, I will include zoom level as well.

Let's assume that you allow zooming in level of 0.0 to 10.0. We use orthographicScale property of SCNNode to simulate zooming in for orthographic type of camera we've set up above. In contrast, orthographicScale is inverse of zoom level in normal understanding. So at zoom level of 10.0, orthographicScale will be 0.0 in order to achieve zoom-in effect.

/**
  Rotate camera around globe to specified coordinate.

  - Parameter to: Location in coordinate (latitude/longitude)
  - Parameter zoomLevel: Zoom level in range 0.0 - 10.0.
  - Parameter duration: Duration for rotation
  - Parameter completion: Delegate when rotation completes to notify back to user
*/
func flyGlobeTo(to location: CLLocationCoordinate2D, zoomLevel: Double, duration: Double, completion: (()->Void)?=nil) {
    // make a call to our conversion method
    let rotation = self.rotationXY(from: location)

    SCNTransaction.begin()
    SCNTransaction.animationDuration = duration
    SCNTransaction.completionBlock = {
        completion?()
    }
    self.cameraOrbit.eulerAngles.x = Float(rotation.y)
    self.cameraOrbit.eulerAngles.y = Float(rotation.x)
    self.camera.orthographicScale = 10.0 - zoomLevel    // calculate value from inverse from orthographicScale
    SCNTransaction.commit()
}

And that's it. Whenever you need to fly to a target coordinate on the map, just use flyGlobeTo() method. Make sure you call it in main thread. Animation is done via SCNTransaction and not through UIView.animate, obviously it's different technology but I note it here too as first time I did use the latter myself.

Upvotes: 2

Related Questions