Как использовать iOS (Swift) SceneKit SCNSceneRenderer unprojectPoint правильно
Я разрабатываю некоторый код, используя SceneKit для iOS, и в моем коде я хочу определить координаты x и y на глобальной плоскости z, где z равно 0.0, а x и y определяются по жестом касания. Моя настройка такова:
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(p.x), Float(p.y), screenZ))
println("tapPoint: (\(p.x), \(p.y)) 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
}
Как вы можете видеть. У меня есть треугольник, который составляет 4 единицы на 4 единицы по ширине и установлен на плоскости z (z = 0) с центром в x, y (0.0, 0.0). Камера является SCNCamera по умолчанию, которая смотрит в направлении отрицательного z, и я разместил ее на (0, 0, 15). Значение по умолчанию для zNear и zFar равно 1.0 и 100.0 соответственно. В моем методе handleTap я беру координаты экрана x и y крана и пытаюсь найти координаты глобальной сцены x и y, где z = 0.0. Я использую вызов unprojectPoint.
Документы для unprojectPoint указывают
Непроектируя точку, z-координата которой равна 0.0, возвращает точку на вблизи плоскости отсечения; непроектируя точку, z-координата которой равна 1,0 возвращает точку на далекой плоскости отсечения.
Хотя конкретно не говорится, что для точек между ними существует связь между ближайшей и дальней плоскостью, я сделал это предположение и вычислил значение screenZ как процентное расстояние между ближней и дальней плоскостью, плоскость z = 0. Чтобы проверить мой ответ, я могу щелкнуть по углам треугольника, потому что знаю, где они находятся в глобальных координатах.
Моя проблема в том, что я не получаю правильные значения, и я не получаю согласованные значения, когда начинаю менять плоскости отсечения zNear и zFar на камере. Итак, мой вопрос: как я могу это сделать? В конце концов, я собираюсь создать новую часть геометрии и поместить ее на z-плоскость, соответствующую тому, где пользователь нажал.
Заранее благодарим за помощь.
Ответы
Ответ 1
Типичные буферы глубины в контуре 3D-графики не являются линейными. Перспективное подразделение приводит к тому, что глубины в нормализованных координатах устройства будут в другом масштабе. (См. также здесь.)
Таким образом, z-координата, которую вы загружаете в unprojectPoint
, на самом деле не та, которую вы хотите.
Как же найти координату нормализованной глубины, соответствующую плоскости в мировом пространстве? Ну, это помогает, если эта плоскость ортогональна камере - что есть у вас. Тогда вам нужно всего лишь проецировать точку на эту плоскость:
let projectedOrigin = gameView.projectPoint(SCNVector3Zero)
Теперь вы располагаете мировым происхождением в 3D-вид + пространство с нормализованной глубиной. Чтобы отобразить другие точки в пространстве 2D-вида на эту плоскость, используйте z-координату из этого вектора:
let vp = gestureRecognizer.locationInView(scnView)
let vpWithZ = SCNVector3(x: vp.x, y: vp.y, z: projectedOrigin.z)
let worldPoint = gameView.unprojectPoint(vpWithZ)
Это дает вам точку в мировом пространстве, которая отображает местоположение щелчка/нажатия на плоскость z = 0, подходящую для использования в качестве position
для node, если вы хотите показать это местоположение пользователю.
(Обратите внимание, что этот подход работает только до тех пор, пока вы переводите на плоскость, перпендикулярную направлению просмотра камеры. Если вы хотите сопоставить координаты вида на другой поверхности, значение нормированной глубины в vpWithZ
не будет постоянным.)
Ответ 2
После некоторых экспериментов, здесь мы разработали, чтобы спроектировать точку касания к данной точке сцены для некоторой произвольной глубины.
Требуется модификация, чтобы вычислить пересечение плоскости Z = 0 с этой линией, и это будет ваша точка.
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
}