Подтвердить что ты не робот

Как использовать 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-плоскость, соответствующую тому, где пользователь нажал.

Заранее благодарим за помощь.

4b9b3361

Ответ 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
}