Cómo usar iOS (Swift) SceneKit SCNSceneRenderer unprojectPoint correctamente

Estoy desarrollando algún código con SceneKit en iOS y en mi código. Quiero determinar las coordenadas xey en el plano global z donde z es 0.0 yx e y se determinan a partir de un gesto de toque. MI configuración es la siguiente:

override func viewDidLoad() { super.viewDidLoad() // create a new scene let scene = SCNScene() // create and add a camera to the scene let cameraNode = SCNNode() let camera = SCNCamera() cameraNode.camera = camera scene.rootNode.addChildNode(cameraNode) // place the camera cameraNode.position = SCNVector3(x: 0, y: 0, z: 15) // create and add an ambient light to the scene let ambientLightNode = SCNNode() ambientLightNode.light = SCNLight() ambientLightNode.light.type = SCNLightTypeAmbient ambientLightNode.light.color = UIColor.darkGrayColor() scene.rootNode.addChildNode(ambientLightNode) let triangleNode = SCNNode() triangleNode.geometry = defineTriangle(); scene.rootNode.addChildNode(triangleNode) // retrieve the SCNView let scnView = self.view as SCNView // set the scene to the view scnView.scene = scene // configure the view scnView.backgroundColor = UIColor.blackColor() // add a tap gesture recognizer let tapGesture = UITapGestureRecognizer(target: self, action: "handleTap:") let gestureRecognizers = NSMutableArray() gestureRecognizers.addObject(tapGesture) scnView.gestureRecognizers = gestureRecognizers } func handleTap(gestureRecognize: UIGestureRecognizer) { // retrieve the SCNView let scnView = self.view as SCNView // check what nodes are tapped let p = gestureRecognize.locationInView(scnView) // get the camera var camera = scnView.pointOfView.camera // screenZ is percentage between z near and far var screenZ = Float((15.0 - camera.zNear) / (camera.zFar - camera.zNear)) var scenePoint = scnView.unprojectPoint(SCNVector3Make(Float(px), Float(py), screenZ)) println("tapPoint: (\(px), \(py)) scenePoint: (\(scenePoint.x), \(scenePoint.y), \(scenePoint.z))") } func defineTriangle() -> SCNGeometry { // Vertices var vertices:[SCNVector3] = [ SCNVector3Make(-2.0, -2.0, 0.0), SCNVector3Make(2.0, -2.0, 0.0), SCNVector3Make(0.0, 2.0, 0.0) ] let vertexData = NSData(bytes: vertices, length: vertices.count * sizeof(SCNVector3)) var vertexSource = SCNGeometrySource(data: vertexData, semantic: SCNGeometrySourceSemanticVertex, vectorCount: vertices.count, floatComponents: true, componentsPerVector: 3, bytesPerComponent: sizeof(Float), dataOffset: 0, dataStride: sizeof(SCNVector3)) // Normals var normals:[SCNVector3] = [ SCNVector3Make(0.0, 0.0, 1.0), SCNVector3Make(0.0, 0.0, 1.0), SCNVector3Make(0.0, 0.0, 1.0) ] let normalData = NSData(bytes: normals, length: normals.count * sizeof(SCNVector3)) var normalSource = SCNGeometrySource(data: normalData, semantic: SCNGeometrySourceSemanticNormal, vectorCount: normals.count, floatComponents: true, componentsPerVector: 3, bytesPerComponent: sizeof(Float), dataOffset: 0, dataStride: sizeof(SCNVector3)) // Indexes var indices:[CInt] = [0, 1, 2] var indexData = NSData(bytes: indices, length: sizeof(CInt) * indices.count) var indexElement = SCNGeometryElement( data: indexData, primitiveType: .Triangles, primitiveCount: 1, bytesPerIndex: sizeof(CInt) ) var geo = SCNGeometry(sources: [vertexSource, normalSource], elements: [indexElement]) // material var material = SCNMaterial() material.diffuse.contents = UIColor.redColor() material.doubleSided = true material.shininess = 1.0; geo.materials = [material]; return geo } 

Como puedes ver. Tengo un triángulo de 4 unidades de alto por 4 unidades de ancho y ubicado en el plano z (z = 0) centrado en x, y (0.0, 0.0). La cámara es la SCNCamera predeterminada que se ve en la dirección z negativa y la he colocado en (0, 0, 15). El valor predeterminado para zNear y zFar es 1.0 y 100.0 respectivamente. En mi método handleTap, tomo las coordenadas xey de la pantalla del bash e bash encontrar las coordenadas de escena globales xey, donde z = 0.0. Estoy usando una llamada para desproveerPoint.

Los documentos para unprojectPoint indican

Al no proyectar un punto cuya coordenada z es 0.0 devuelve un punto en el plano de recorte cercano; al no proyectar un punto cuya coordenada z es 1.0, se devuelve un punto en el plano de recorte lejano.

Si bien no dice específicamente que para los puntos intermedios hay una relación de línea entre el plano cercano y el plano lejano, he hecho esa suposición y he calculado que el valor de pantallaZ es la distancia porcentual entre el plano cercano y el plano lejano que z = 0 avión está ubicado. Para verificar mi respuesta, puedo hacer clic cerca de las esquinas del triángulo porque sé dónde están en coordenadas globales.

Mi problema es que no obtengo los valores correctos y no obtengo valores constantes cuando comienzo a cambiar los planos de recorte zNear y zFar en la cámara. Entonces mi pregunta es, ¿cómo puedo hacer esto? Al final, voy a crear una nueva pieza de geometría y colocarla en el plano z correspondiente al lugar donde hizo clic el usuario.

Gracias de antemano por tu ayuda.

Los búferes de profundidad típicos en una tubería de gráficos 3D no son lineales . La división en perspectiva hace que las profundidades en las coordenadas del dispositivo normalizado estén en una escala diferente . ( Ver también aquí )

Por lo tanto, la coordenada z que está alimentando en unprojectPoint no es realmente la que desea.

¿Cómo, entonces, encontrar la coordenada de profundidad normalizada que coincide con un plano en el espacio mundial? Bueno, ayuda si ese avión es ortogonal a la cámara, que es la tuya. Entonces todo lo que necesitas hacer es proyectar un punto en ese plano:

 let projectedOrigin = gameView.projectPoint(SCNVector3Zero) 

Ahora tiene la ubicación del origen mundial en la vista 3D + espacio de profundidad normalizada. Para asignar otros puntos en el espacio de vista 2D a este plano, use la coordenada z de este vector:

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

Esto le proporciona un punto en el espacio mundial que mapea la ubicación de clic / toque en el plano z = 0, adecuado para usar como la position de un nodo si desea mostrar esa ubicación al usuario.

(Tenga en cuenta que este enfoque funciona solo mientras esté mapeando en un plano perpendicular a la dirección de visualización de la cámara. Si desea asignar coordenadas de vista a una superficie orientada de forma diferente, el valor de profundidad normalizada en vpWithZ no será constante.)

Después de un poco de experimentación, esto es lo que desarrollamos para proyectar un punto de contacto en un punto determinado de la escena para una profundidad arbitraria.

La modificación que necesita es calcular la intersección del plano Z = 0 con esta línea, y ese será su punto.

 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  return scenePoint } 

Esta es mi solución para obtener el punto exacto en 3D Space.

 // If the object is in 0,0,0 point you can use float zDepth = [self projectPoint:SCNVector3Zero].z; // or myNode.position. //zDepth = [self projectPoint:myNode.position].z; NSLog(@"2D point: X %f, Y: %f, zDepth: %f", click.x, click.y, zDepth); SCNVector3 worldPoint = [self unprojectPoint:SCNVector3Make(click.x, click.y, zDepth)]; SCNVector3 nearVec = SCNVector3Make(click.x, click.y, 0.0); SCNVector3 nearPoint = [self unprojectPoint:nearVec]; SCNVector3 farVec = SCNVector3Make(click.x, click.y, 1.0); SCNVector3 farPoint = [self unprojectPoint:farVec]; float z_magnitude = fabs(farPoint.z - nearPoint.z); float near_pt_factor = fabs(nearPoint.z) / z_magnitude; float far_pt_factor = fabs(farPoint.z) / z_magnitude; GLKVector3 nearP = GLKVector3Make(nearPoint.x, nearPoint.y, nearPoint.z); GLKVector3 farP = GLKVector3Make(farPoint.x, farPoint.y, farPoint.z); GLKVector3 final_pt = GLKVector3Add(GLKVector3MultiplyScalar(nearP, far_pt_factor), GLKVector3MultiplyScalar(farP, near_pt_factor)); NSLog(@"3D world point = %f, %f, %f", final_pt.x, final_pt.y, worldPoint.z);