将 SceneKit 对象定位在 SCNCamera 当前方向的前面

Position a SceneKit object in front of SCNCamera's current orientation

我想在用户点击屏幕时创建一个新的 SceneKit 节点,并让它以设定的距离直接出现在相机前面。为了测试,这将是一个 SCNText reads reads "you tapped here"。它也应该与视线成直角 - 即 "face on" 到相机。

所以,给定 self.camera.orientation SCNVector4(或类似的),我如何:

  1. 生成 SCNVector3,将节点放入相机的 "front" 给定距离?
  2. 生成定位节点的 SCNVector4(或 SCNQuaternion),使其成为 "facing" 相机?

我怀疑 (2) 的答案与 `self.camera.orientation?但是(1)让我有点迷茫。

我看到很多类似的问题,例如 this one,但没有答案。

我是通过拼凑其他几个答案得到的...从这些方法开始:

func pointInFrontOfPoint(point: SCNVector3, direction: SCNVector3, distance: Float) -> SCNVector3 {
    var x = Float()
    var y = Float()
    var z = Float()

    x = point.x + distance * direction.x
    y = point.y + distance * direction.y
    z = point.z + distance * direction.z

    let result = SCNVector3Make(x, y, z)
    return result
}

func calculateCameraDirection(cameraNode: SCNNode) -> SCNVector3 {
    let x = -cameraNode.rotation.x
    let y = -cameraNode.rotation.y
    let z = -cameraNode.rotation.z
    let w = cameraNode.rotation.w
    let cameraRotationMatrix = GLKMatrix3Make(cos(w) + pow(x, 2) * (1 - cos(w)),
                                              x * y * (1 - cos(w)) - z * sin(w),
                                              x * z * (1 - cos(w)) + y*sin(w),

                                              y*x*(1-cos(w)) + z*sin(w),
                                              cos(w) + pow(y, 2) * (1 - cos(w)),
                                              y*z*(1-cos(w)) - x*sin(w),

                                              z*x*(1 - cos(w)) - y*sin(w),
                                              z*y*(1 - cos(w)) + x*sin(w),
                                              cos(w) + pow(z, 2) * ( 1 - cos(w)))

    let cameraDirection = GLKMatrix3MultiplyVector3(cameraRotationMatrix, GLKVector3Make(0.0, 0.0, -1.0))
    return SCNVector3FromGLKVector3(cameraDirection)
}

现在你可以这样称呼它了:

let rectnode = SCNNode(geometry: rectangle)
let dir = calculateCameraDirection(cameraNode: self.camera)
let pos = pointInFrontOfPoint(point: self.camera.position, direction:dir, distance: 18)
rectnode.position = pos
rectnode.orientation = self.camera.orientation
sceneView.scene?.rootNode.addChildNode(rectnode)

距离 18 将其定位在半径为 10 的球体上的网格和半径为 25 的背景图像之间。它看起来 真的 不错,而且我的 5s 的性能出奇的好。

您不需要这么多数学运算 — SceneKit 实际上已经在做所有这些数学运算,所以您只需要向它询问正确的结果。

[How do I] produce the SCNVector3 that puts the node in "front" of the camera at a given distance?

在承载相机的节点的局部坐标系中,"front"是-Z方向。如果你把一些东西放在相对于相机 (0, 0, -10) 的位置,它就直接位于相机视线的中心,距离 10 个单位。有两种方法可以做到这一点:

  1. 将文本节点设为相机的子节点即可

    cameraNode.addChildNode(textNode)
    textNode.position = SCNVector3(x: 0, y: 0, z: -10)
    

    请注意,这也会使它 随着 相机移动:如果相机改变方向或位置,textNode 也会随之移动。 (想象一个反向的自拍杆——在一端转动 phone,另一端随之摆动。)

    由于文本节点位于 camera-node space 中,您可能也不需要其他任何东西来使其面向相机。 (SCNText 相对于其父 space 的默认方向使文本面向相机。)

  2. (iOS 11 / macOS 10.13 / Xcode 9 / 等中的新增功能)SceneKit 现在包含一些方便的访问器和方法,用于执行各种 relative-geometry 数学运算以更少的步骤。这些属性和方法中的大多数都可以在支持 system-wide SIMD 库而不是使用 SCN vector/matrix 类型的新版本中使用——而且 SIMD 库具有不错的功能,例如内置的重载运算符。将它们放在一起,操作像这样变成 one-liner:

    textNode.simdPosition = camera.simdWorldFront * distance
    

    (simdWorldFront是一个长度为1.0的向量,所以你可以通过缩放来改变距离。)

  3. 如果你还在支持iOS10 / macOS 10.12 / 等,你仍然可以使用节点层次结构来进行坐标转换。文本节点可以是场景的子节点(实际上是场景的rootNode),但是你可以找到你想要的camera-space坐标对应的scene-coordinate-space位置。你真的不需要它的功能,但这可能会成功:

    func sceneSpacePosition(inFrontOf node: SCNNode, atDistance distance: Float) -> SCNVector3 {
        let localPosition = SCNVector3(x: 0, y: 0, z: CGFloat(-distance))
        let scenePosition = node.convertPosition(localPosition, to: nil) 
             // to: nil is automatically scene space
        return scenePosition
    }
    textNode.position = sceneSpacePosition(inFrontOf: camera, atDistance: 3)
    

    在这种情况下,文本节点最初是相对于相机定位的,但不会保持 "connected"。如果移动相机,节点将保留在原处。由于文本节点在场景中 space,它的方向也没有连接到相机。

[How do I] produce the SCNVector4 (or SCNQuaternion) that orients the node so it is "facing" the camera?

这部分你要么不需要做太多,要么可以完全交给 SceneKit,这取决于你使用的第一个问题的答案。

  1. 如果您执行了上面的 (1) - 使文本节点成为相机节点的子节点 - 使其面向相机是一件 one-step 事情。正如我所指出的,使用 SCNText 作为您的测试几何,该步骤已经完成。在其局部坐标 space 中,SCNText 是直立的,并且可以从默认相机方向(即,在 -Z 方向上看,+Y 向上,+X 向右)读取。

    如果您有一些其他的几何体想要面向相机,您只需调整一次它的方向,但您必须找出合适的方向。例如,一个 SCNPyramid 的点在 +Y 方向,所以如果你想让这个点朝向相机(注意,它很锐利),你需要围绕它的 [=114= 旋转它] .pi/2。 (您可以使用 eulerAnglesrotationorientationpivot。)

    由于它是相机的子项,因此您给它的任何旋转都将相对于相机,因此如果相机移动,它会保持指向相机。

  2. 如果你做了上面的 (2),你 可以 尝试用 scene-space 术语计算出面对相机需要什么样的旋转,并在节点或相机移动时应用它。但是有一个 API 可以为您做到这一点。实际上,两个 APIs:SCNLookAtConstraint 使任何节点 "face towards" 成为任何其他节点(由受约束节点的 -Z 方向确定),并且 SCNBillboardConstraint 有点像同样的事情,但有一些额外的考虑,假设您不仅要面对其他节点,而且要特别面对 相机

    仅使用默认选项就可以很好地工作:

    textNode.constraints = [ SCNBillboardConstraint() ]
    

额外警告

如果您使用上面的 sceneSpacePosition 函数,并且您的摄像机节点的方向是在渲染时设置的——也就是说,您没有设置 transformeulerAnglesrotationorientation 直接在其上,而是对相机 move/orient 使用约束、动作、动画或物理 — 还有一件事需要考虑。

SceneKit 实际上在任何给定时间都有两个节点层次结构在起作用:"model" 树和 "presentation" 树。模型树是直接设置 transform (等)所做的。 moving/orienting 节点的所有其他方式都使用表示树。因此,如果您想使用 camera-relative space 计算位置,您将需要相机的演示节点:

textNode.position = sceneSpacePosition(inFrontOf: camera.presentation, atDistance: 3)
                                                        ~~~~~~~~~~~~~

(为什么是这样?除其他外,它可以让您拥有隐式动画:在事务中设置 position,它会从旧位置到新位置进行动画处理。在该动画期间,modelposition就是你设置的,presentation节点连续c改变它的 position.)

(Swift 4)

嘿,如果你想将对象放置在相对于另一个节点(例如相机节点)的特定位置并且与参考节点处于相同的方向,你可以使用这个更简单的函数:

func updatePositionAndOrientationOf(_ node: SCNNode, withPosition position: SCNVector3, relativeTo referenceNode: SCNNode) {
    let referenceNodeTransform = matrix_float4x4(referenceNode.transform)

    // Setup a translation matrix with the desired position
    var translationMatrix = matrix_identity_float4x4
    translationMatrix.columns.3.x = position.x
    translationMatrix.columns.3.y = position.y
    translationMatrix.columns.3.z = position.z

    // Combine the configured translation matrix with the referenceNode's transform to get the desired position AND orientation
    let updatedTransform = matrix_multiply(referenceNodeTransform, translationMatrix)
    node.transform = SCNMatrix4(updatedTransform)
}

如果你想把 'node' 2m 放在某个 'cameraNode' 的正前方,你可以这样称呼它:

let position = SCNVector3(x: 0, y: 0, z: -2)
updatePositionAndOrientationOf(node, withPosition: position, relativeTo: cameraNode)

编辑:获取相机节点

要获取相机节点,这取决于您使用的是 SceneKit、ARKit 还是其他框架。以下是 ARKit 和 SceneKit 的示例。

借助 ARKit,您可以使用 ARSCNView 渲染 SCNScene 的 3D 对象与相机内容重叠。您可以从 ARSCNView 的 pointOfView 属性:

获取相机节点
let cameraNode = sceneView.pointOfView

对于 SceneKit,您有一个 SCNView 可以渲染 SCNScene 的 3D 对象。您可以创建相机节点并将它们放置在任何您想要的位置,因此您可以执行以下操作:

let scnScene = SCNScene()
// (Configure scnScene here if necessary)
scnView.scene = scnScene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(0, 5, 10)  // For example
scnScene.rootNode.addChildNode(cameraNode)

设置好相机节点后,您可以像ARKit一样访问当前相机:

let cameraNode = scnView.pointOfView

让一切变得简单:

let constraint = SCNTransformConstraint(inWorldSpace: true) { node, transform -> SCNMatrix4 in
    let distance: Float = 1.0

    let newNode = SCNNode()
    let worldFront = self.sceneView!.pointOfView!.simdWorldFront * distance

    newNode.transform = self.sceneView!.pointOfView!.transform
    newNode.transform = SCNMatrix4Translate(newNode.transform, worldFront.x, worldFront.y, worldFront.z)

    return newNode.transform
}

ARSCNView 有一个 属性 'pointOfView'。您可以将子节点附加到它。

let ball = SCNSphere(radius: 0.02)
ballNode = SCNNode(geometry: ball)
ballNode?.position = SCNVector3Make(0, 0, -0.2)
sceneView.pointOfView?.addChildNode(ballNode!)

无论您走到哪里,节点都会跟随您的相机。