如何正确使用 iOS(Swift)SceneKit 的 SCNSceneRenderer.unprojectPoint 方法

19

我正在使用iOS上的SceneKit开发一些代码,在我的代码中,我想确定全局z平面上z为0.0时的x和y坐标,并且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屏幕坐标,并尝试找到z = 0.0时的x和y全局场景坐标。我使用unprojectPoint进行调用。

unprojectPoint的文档指出

将z坐标为0.0的点反投影返回近裁剪平面上的点;将z坐标为1.0的点反投影返回远裁剪平面上的点。

虽然它没有明确说明中间点在近平面和远平面之间存在线性关系,但我已经做出了这样的假设,并计算出screenZ的值为z = 0平面位于近裁剪平面和远裁剪平面之间的百分比距离。为了检查我的答案,我可以在三角形的角附近单击,因为我知道它们在全局坐标中的位置。

我的问题是,我没有获得正确的值,并且当我开始更改相机上的zNear和zFar裁剪平面时,我没有获得一致的值。那么我的问题是,我该如何做?最终,我将创建一个新的几何体,并将其放置在对应于用户单击的z平面上。

提前感谢您的帮助。


你是否遇到过这样一个问题,即unprojectPoint函数返回的X和Y坐标值实际上相同? - Crashalot
你有没有研究过 SCNSceneRenderer.hitTest()unprojectPoint() 的文档甚至提到你应该使用 hitTest() 处理点击事件。 - bio
3个回答

34

在3D图形管道中,典型的深度缓冲区不是线性的。透视除法会导致规范化设备坐标中的深度处于不同的比例尺。(此处也参见。)

因此,您输入到unprojectPoint中的Z坐标实际上并不是您想要的。

那么,如何找到与世界空间中的平面匹配的规范化深度坐标呢?如果该平面与相机正交,则会有所帮助——这一点在您的情况下是成立的。然后,您只需要在该平面上投影一个点:

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,如果您想向用户展示该位置。

(请注意,只有在映射到与相机视角方向垂直的平面时,此方法才有效。如果您想将视图坐标映射到不同方向的表面上,则vpWithZ中的归一化深度值不会是恒定的。)


正是我所需要的。非常感谢您花时间回答这个问题。干杯! - ptoinson
我刚刚在寻找与ptoinon完全相同的问题......什么都找不到......无法弄清楚这是如何工作的。非常感谢rickster!!! - Max
如果您想将一个二维坐标投影到相机前方的任意点(例如,相机前方15个单位),该怎么办? - Crashalot
2
真棒的回答!非常感谢@rickster! 对于未来的谷歌搜索用户,在我的案例中,我需要节点在相机前方10个单位的Z位置。唯一需要调整才能工作的是将SCNVector3Zero替换为SCNVector3Make(0,0,myNode.position.z)-请注意我已经将其放置在所需的深度处- - leandrodemarco
嗨@Crashalot,我想知道这是否适用于从3D网格到2D图像点的面部跟踪。你能帮忙吗?! - swiftlearneer
这个可以工作,但问题在于如果视图被调整大小,位置会出错,因为它属于旧的视图坐标。 - StackUnderflow

4

经过一些试验,我们开发出以下方案,可以将触摸点投射到场景中的任意深度。

你需要修改的是计算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
}

当我使用上述的scenePoint结果向屏幕添加更多节点时,这些节点的位置变得越来越不准确,因为它们离触摸点越来越远。似乎scenePoint开始返回与先前放置节点更接近的位置。 - Alan Scarpa

0

这是我获取三维空间精确点的解决方案。

// 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);

如果你要在unprojectPoint中使用zDepth,那么它不应该是一个从0到1的浮点数吗? - zakdances
我目前使用SceneKit提供的函数,这段代码已不再使用。 - Menio
GLKVector3Add 是什么? - Krishna Datt Shukla

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接