How to use iOS (Swift) SceneKit SCNSceneRenderer unprojectPoint properly

Typical depth buffers in a 3D graphics pipeline are not linear. Perspective division causes depths in normalized device coordinates to be on a different scale. (See also here.)

So the z-coordinate you're feeding into unprojectPoint isn't actually the one you want.

How, then, to find the normalized-depth coordinate matching a plane in world space? Well, it helps if that plane is orthogonal to the camera -- which yours is. Then all you need to do is project a point on that plane:

let projectedOrigin = gameView.projectPoint(SCNVector3Zero)

Now you have the location of the world origin in 3D view + normalized-depth space. To map other points in 2D view space onto this plane, use the z-coordinate from this vector:

let vp = gestureRecognizer.locationInView(scnView)
let vpWithZ = SCNVector3(x: vp.x, y: vp.y, z: projectedOrigin.z)
let worldPoint = gameView.unprojectPoint(vpWithZ)

This gets you a point in world space that maps the click/tap location to the z = 0 plane, suitable for use as the position of a node if you want to show that location to the user.

(Note that this approach works only as long as you're mapping onto a plane that's perpendicular to the camera's view direction. If you want to map view coordinates onto a differently-oriented surface, the normalized-depth value in vpWithZ won't be constant.)


After some experimentation, here's what we developed to project a touch point to a given point in the scene for some arbitrary depth.

The modification you need is to compute the intersection of the Z=0 plane with this line, and that will be your point.

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
}