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.)

Leave a Comment