Как правильно использовать 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);
Другие вопросы по тегам