Как правильно использовать 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 указывают
Отказ от проецирования точки, координата которой равна 0,0, возвращает точку на ближней плоскости отсечения; Отказ от проекции точки, координата которой равна 1,0, возвращает точку на дальней плоскости отсечения.
Хотя в нем конкретно не говорится, что для промежуточных точек существует линейная зависимость между ближней и дальней плоскостями, я сделал это предположение и вычислил значение screenZ как процентное расстояние между ближней и дальней плоскостями, что z = 0 самолет находится. Чтобы проверить мой ответ, я могу щелкнуть около углов треугольника, потому что я знаю, где они находятся в глобальных координатах.
Моя проблема в том, что я не получаю правильные значения и не получаю непротиворечивые значения, когда начинаю менять плоскости отсечения zNear и zFar на камере. Итак, мой вопрос, как я могу это сделать? В конце я собираюсь создать новый фрагмент геометрии и поместить его в z-плоскость, соответствующую тому, где пользователь щелкнул.
Заранее спасибо за помощь.
3 ответа
Типичные буферы глубины в конвейере трехмерной графики не являются линейными. Деление перспективы приводит к тому, что глубины в нормализованных координатах устройства имеют другой масштаб. ( Смотрите также здесь.)
Итак, координата z, в которую вы вводите unprojectPoint
на самом деле не тот, который вы хотите.
Как же тогда найти нормированную глубину, совпадающую с плоскостью в мировом пространстве? Ну, это помогает, если эта плоскость ортогональна к камере - которая ваша. Тогда все, что вам нужно сделать, это спроецировать точку на эту плоскость:
let projectedOrigin = gameView.projectPoint(SCNVector3Zero)
Теперь у вас есть местоположение начала мира в 3D-виде + пространство нормализованной глубины. Чтобы отобразить другие точки в двухмерном пространстве вида на эту плоскость, используйте координату 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
узла, если вы хотите показать это местоположение пользователю.
(Обратите внимание, что этот подход работает только до тех пор, пока вы отображаете плоскость, перпендикулярную направлению обзора камеры. Если вы хотите отобразить координаты вида на поверхности с различной ориентацией, значение нормализованной глубины в vpWithZ
не будет постоянным.)
После некоторых экспериментов, вот что мы разработали, чтобы спроецировать точку касания на заданную точку сцены на некоторую произвольную глубину.
Модификация, в которой вы нуждаетесь, состоит в том, чтобы вычислить пересечение плоскости 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
}
Это мое решение для получения точной точки в 3D-пространстве.
// 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);