Search code examples
ios3dscenekitscnnodesceneview

SceneKit: unprojectPoint returns same/similar point no matter where you touch screen


The code below should translate touch coordinates into world coordinates for a SceneKit scene.

However, as illustrated by the output below, the point returned by unprojectPoint returns effectively the same point no matter where you touch on the screen (iPhone 5s).

The class docs for unprojectPoint suggest using Z values between 0 and 1, but using different values like 0.5 did not change the output for unprojectPoint.

This SO post discusses how to set the depth value for unprojectPoint, but setting the Z value to values greater than 1 (e.g., 15, 20) also did not change the output.

In both cases, the X and Y values return from unprojectPoint effectively remained the same as well.

1) What is the right way to use unprojectPoint?

2) How does unprojectPoint account for camera rotations? For instance, if you moved the camera to (0, 20, 0) and rotated the camera down 90 degrees so it's facing the ground, how do you ensure the rotation is accounted for? If you set a depth of 20 and tapped on the origin, the desired return value from unprojectPoint should be (0, 0, 0).

3) How do you get unprojectPoint to return values in front of the camera (e.g., Z values are lower than the camera's Z value)

Code:

cameraNode.position = SCNVector3(x: 0, y: Float(0), z: Float(8))

func sceneViewTapped(recognizer: UITapGestureRecognizer) {
    let point = recognizer.locationInView(sceneView)
    let unprojectPoint = SCNVector3(x: Float(point.x), y: Float(point.y), z: 0.0)
    let scenePos = sceneView.unprojectPoint(unprojectPoint)
    print("2D point: \(point). 3D point: \(scenePos)")
}

Output:

2D point: (154.5, 169.5). 3D point: SCNVector3(x: -0.00111810782, y: 0.0232769605, z: 7.9000001)

2D point: (280.5, 252.0). 3D point: SCNVector3(x: 0.0244967155, y: 0.00650534919, z: 7.9000001)

2D point: (32.0, 181.0). 3D point: SCNVector3(x: -0.0260214079, y: 0.0209390987, z: 7.9000001)

2D point: (12.0, 505.0). 3D point: SCNVector3(x: -0.0300872531, y: -0.0449275821, z: 7.9000001)

2D point: (311.5, 12.5). 3D point: SCNVector3(x: 0.0307987742, y: 0.0551938377, z: 7.9000001)

2D point: (22.5, 88.0). 3D point: SCNVector3(x: -0.0279526841, y: 0.0398452766, z: 7.9000001)

2D point: (313.5, 358.0). 3D point: SCNVector3(x: 0.0312053617, y: -0.0150436237, z: 7.9000001)

2D point: (314.0, 507.0). 3D point: SCNVector3(x: 0.0313070044, y: -0.0453341678, z: 7.9000001)

2D point: (155.0, 360.5). 3D point: SCNVector3(x: -0.00101646129, y: -0.0155518558, z: 7.9000001)


Solution

  • As long as you use 0 and 1 the values do change for unprojectPoint. Using 0 for the Z value represents the point on the near plane while using 1 yields the point on the far plane.

    So to return a scene point that is an arbitrary distance from the camera, we developed the following function. We are new to SceneKit so please offer any edits or corrections!

    Effectively, you define the ray/line between the near and far points then pick some point along the line.

    private func touchPointToScenePoint(recognizer: UIGestureRecognizer) -> SCNVector3 {
        // Get touch point
        let touchPoint = recognizer.locationInView(sceneView)
    
        // Compute near & far points
        let nearVector = SCNVector3(x: Float(touchPoint.x), y: Float(touchPoint.y), z: 0)
        let nearScenePoint = sceneView.unprojectPoint(nearVector)
        let farVector = SCNVector3(x: Float(touchPoint.x), y: Float(touchPoint.y), z: 1)
        let farScenePoint = sceneView.unprojectPoint(farVector)
    
        // Compute view vector
        let viewVector = SCNVector3(x: Float(farScenePoint.x - nearScenePoint.x), y: Float(farScenePoint.y - nearScenePoint.y), z: Float(farScenePoint.z - nearScenePoint.z))
    
        // Normalize view vector
        let vectorLength = sqrt(viewVector.x*viewVector.x + viewVector.y*viewVector.y + viewVector.z*viewVector.z)
        let normalizedViewVector = SCNVector3(x: viewVector.x/vectorLength, y: viewVector.y/vectorLength, z: viewVector.z/vectorLength)
    
        // Scale normalized vector to find scene point
        let scale = Float(15)
        let scenePoint = SCNVector3(x: normalizedViewVector.x*scale, y: normalizedViewVector.y*scale, z: normalizedViewVector.z*scale)
    
        print("2D point: \(touchPoint). 3D point: \(nearScenePoint). Far point: \(farScenePoint). scene point: \(scenePoint)")
    
        // Return <scenePoint>
        return scenePoint
    }