iOS SceneKit 到 UIView 投影问题
iOS SceneKit to UIView projection issue
我的相机根节点前面有一个带有 SCNPlane 几何 SCNNode 的 SCNView。
在 SCNView 上,在 UIView 中,我正在添加 UIImages(标记)- 橙色圆圈。
在 Motion 侦听器中,我尝试以某种方式定位标记,以便它们粘在平面每个边缘的中心。
正确的标记对齐 - 当设备处于直线位置时:
我正在使用从 SceneKit 对象到 UIView 的投影来执行此操作:
//world coordinates
let v1w = sm.node.convertPosition(sm.node.boundingBox.min,
to: self.sceneView.scene?.rootNode)
let v2w = sm.node.convertPosition(sm.node.boundingBox.max,
to: self.sceneView.scene?.rootNode)
//projected coordinates
let v1p = self.sceneView.projectPoint(v1w)
let v2p = self.sceneView.projectPoint(v2w)
//frame rectangle
let rect = CGRect.init(x: CGFloat(v1p.x), y: CGFloat(v2p.y),
width: CGFloat(v2p.x - v1p.x), height: CGFloat(v1p.y - v2p.y))
var frameOld = sm.marker.frame
switch sm.position
{
case .Top:
frameOld.origin.y = rect.minY - frameOld.size.height/2
frameOld.origin.x = rect.midX - frameOld.size.width/2
case .Bottom:
frameOld.origin.y = rect.maxY - frameOld.size.height/2
frameOld.origin.x = rect.midX - frameOld.size.width/2
case .Left:
frameOld.origin.y = rect.midY - frameOld.size.height/2
frameOld.origin.x = rect.minX - frameOld.size.width/2
case .Right:
frameOld.origin.y = rect.midY - frameOld.size.height/2
frameOld.origin.x = rect.maxX - frameOld.size.width/2
}
sm.marker.frame = frameOld
self.view.layoutSubviews()
您可以在此处找到类似的方法:
所以我希望这些标记在设备运动期间粘在平面的边缘。但存在问题:旋转设备时 - 标记从平面边缘漂移
查看问题视频:https://youtu.be/XBgNDDX5ZI8
我在 github 上创建了一个基本项目来重现问题:https://github.com/mgontar/SceneKitProjectionIssue
这里的问题是CGRect
你用来计算中点的是基于边界框的投影坐标。边界框的两个角点使用模型视图投影矩阵进行变换,要获得正确的视图 space 中点坐标,您需要执行相同的变换。
希望代码更清晰一些。
//world coordinates
let v1w = sm.node.convertPosition(sm.node.boundingBox.min, to: self.sceneView.scene?.rootNode)
let v2w = sm.node.convertPosition(sm.node.boundingBox.max, to: self.sceneView.scene?.rootNode)
//calc center of BB in world coordinates
let center = SCNVector3Make(
(v1w.x + v2w.x)/2,
(v1w.y + v2w.y)/2,
(v1w.z + v2w.z)/2)
//calc each mid point
let mp1w = SCNVector3Make(v1w.x, center.y, center.z)
let mp2w = SCNVector3Make(center.x, v2w.y, center.z)
let mp3w = SCNVector3Make(v2w.x, center.y, center.z)
let mp4w = SCNVector3Make(center.x, v1w.y, center.z)
//projected coordinates
let mp1p = self.sceneView.projectPoint(mp1w)
let mp2p = self.sceneView.projectPoint(mp2w)
let mp3p = self.sceneView.projectPoint(mp3w)
let mp4p = self.sceneView.projectPoint(mp4w)
var frameOld = sm.marker.frame
switch sm.position
{
case .Top:
frameOld.origin.y = CGFloat(mp1p.y) - frameOld.size.height/2
frameOld.origin.x = CGFloat(mp1p.x) - frameOld.size.width/2
sm.marker.isHidden = (mp1p.z < 0 || mp1p.z > 1)
case .Bottom:
frameOld.origin.y = CGFloat(mp2p.y) - frameOld.size.height/2
frameOld.origin.x = CGFloat(mp2p.x) - frameOld.size.width/2
sm.marker.isHidden = (mp2p.z < 0 || mp2p.z > 1)
case .Left:
frameOld.origin.y = CGFloat(mp3p.y) - frameOld.size.height/2
frameOld.origin.x = CGFloat(mp3p.x) - frameOld.size.width/2
sm.marker.isHidden = (mp3p.z < 0 || mp3p.z > 1)
case .Right:
frameOld.origin.y = CGFloat(mp4p.y) - frameOld.size.height/2
frameOld.origin.x = CGFloat(mp4p.x) - frameOld.size.width/2
sm.marker.isHidden = (mp4p.z < 0 || mp4p.z > 1)
}
这是一个很酷的小示例项目!
关于 z-clipping 问题的更新
projectPoint
方法returns一个3DSCNVector
,我们知道的x任意y坐标就是屏幕坐标。 z 坐标告诉我们点相对于远近裁剪平面的位置(z = 0 近裁剪平面,z = 1 远裁剪平面)。如果您为近裁剪平面设置负值,则将渲染相机后面的对象。我们没有负近裁剪平面,但我们也没有任何逻辑来说明如果这些投影点位置落在 z far 和 z near 范围之外会发生什么。
我更新了上面的代码以包含此 zNear 和 zFar 检查并相应地切换 UIView 可见性。
tl;dr
相机旋转 180 度时可见的标记位于相机后面,但它们仍被投影到视图平面上。由于我们没有检查他们是否在镜头后面,所以他们仍然显示。
要进行正确的投影,您应该计算世界坐标中的中点,然后将点投影到场景视图
let min = node.boundingBox.min
let max = node.boundingBox.max
//world coordinates
var mp1w = SCNVector3Make((min.x+max.x)/2, max.y, (min.z+max.z)/2) //top
var mp2w = SCNVector3Make((min.x+max.x)/2, min.y, (min.z+max.z)/2) //bottom
var mp3w = SCNVector3Make(min.x, (min.y+max.y)/2, (min.z+max.z)/2) //left
var mp4w = SCNVector3Make(max.x, (min.y+max.y)/2, (min.z+max.z)/2) //right
mp1w = node.convertPosition(mp1w, to: self.sceneView.scene.rootNode)
mp2w = node.convertPosition(mp2w, to: self.sceneView.scene.rootNode)
mp3w = node.convertPosition(mp3w, to: self.sceneView.scene.rootNode)
mp4w = node.convertPosition(mp4w, to: self.sceneView.scene.rootNode)
//projected coordinates
let mp1p = self.sceneView.projectPoint(mp1w) //top
let mp2p = self.sceneView.projectPoint(mp2w) //bottom
let mp3p = self.sceneView.projectPoint(mp3w) //left
let mp4p = self.sceneView.projectPoint(mp4w) //right
我的相机根节点前面有一个带有 SCNPlane 几何 SCNNode 的 SCNView。
在 SCNView 上,在 UIView 中,我正在添加 UIImages(标记)- 橙色圆圈。
在 Motion 侦听器中,我尝试以某种方式定位标记,以便它们粘在平面每个边缘的中心。
正确的标记对齐 - 当设备处于直线位置时:
我正在使用从 SceneKit 对象到 UIView 的投影来执行此操作:
//world coordinates
let v1w = sm.node.convertPosition(sm.node.boundingBox.min,
to: self.sceneView.scene?.rootNode)
let v2w = sm.node.convertPosition(sm.node.boundingBox.max,
to: self.sceneView.scene?.rootNode)
//projected coordinates
let v1p = self.sceneView.projectPoint(v1w)
let v2p = self.sceneView.projectPoint(v2w)
//frame rectangle
let rect = CGRect.init(x: CGFloat(v1p.x), y: CGFloat(v2p.y),
width: CGFloat(v2p.x - v1p.x), height: CGFloat(v1p.y - v2p.y))
var frameOld = sm.marker.frame
switch sm.position
{
case .Top:
frameOld.origin.y = rect.minY - frameOld.size.height/2
frameOld.origin.x = rect.midX - frameOld.size.width/2
case .Bottom:
frameOld.origin.y = rect.maxY - frameOld.size.height/2
frameOld.origin.x = rect.midX - frameOld.size.width/2
case .Left:
frameOld.origin.y = rect.midY - frameOld.size.height/2
frameOld.origin.x = rect.minX - frameOld.size.width/2
case .Right:
frameOld.origin.y = rect.midY - frameOld.size.height/2
frameOld.origin.x = rect.maxX - frameOld.size.width/2
}
sm.marker.frame = frameOld
self.view.layoutSubviews()
您可以在此处找到类似的方法:
所以我希望这些标记在设备运动期间粘在平面的边缘。但存在问题:旋转设备时 - 标记从平面边缘漂移
查看问题视频:https://youtu.be/XBgNDDX5ZI8
我在 github 上创建了一个基本项目来重现问题:https://github.com/mgontar/SceneKitProjectionIssue
这里的问题是CGRect
你用来计算中点的是基于边界框的投影坐标。边界框的两个角点使用模型视图投影矩阵进行变换,要获得正确的视图 space 中点坐标,您需要执行相同的变换。
希望代码更清晰一些。
//world coordinates
let v1w = sm.node.convertPosition(sm.node.boundingBox.min, to: self.sceneView.scene?.rootNode)
let v2w = sm.node.convertPosition(sm.node.boundingBox.max, to: self.sceneView.scene?.rootNode)
//calc center of BB in world coordinates
let center = SCNVector3Make(
(v1w.x + v2w.x)/2,
(v1w.y + v2w.y)/2,
(v1w.z + v2w.z)/2)
//calc each mid point
let mp1w = SCNVector3Make(v1w.x, center.y, center.z)
let mp2w = SCNVector3Make(center.x, v2w.y, center.z)
let mp3w = SCNVector3Make(v2w.x, center.y, center.z)
let mp4w = SCNVector3Make(center.x, v1w.y, center.z)
//projected coordinates
let mp1p = self.sceneView.projectPoint(mp1w)
let mp2p = self.sceneView.projectPoint(mp2w)
let mp3p = self.sceneView.projectPoint(mp3w)
let mp4p = self.sceneView.projectPoint(mp4w)
var frameOld = sm.marker.frame
switch sm.position
{
case .Top:
frameOld.origin.y = CGFloat(mp1p.y) - frameOld.size.height/2
frameOld.origin.x = CGFloat(mp1p.x) - frameOld.size.width/2
sm.marker.isHidden = (mp1p.z < 0 || mp1p.z > 1)
case .Bottom:
frameOld.origin.y = CGFloat(mp2p.y) - frameOld.size.height/2
frameOld.origin.x = CGFloat(mp2p.x) - frameOld.size.width/2
sm.marker.isHidden = (mp2p.z < 0 || mp2p.z > 1)
case .Left:
frameOld.origin.y = CGFloat(mp3p.y) - frameOld.size.height/2
frameOld.origin.x = CGFloat(mp3p.x) - frameOld.size.width/2
sm.marker.isHidden = (mp3p.z < 0 || mp3p.z > 1)
case .Right:
frameOld.origin.y = CGFloat(mp4p.y) - frameOld.size.height/2
frameOld.origin.x = CGFloat(mp4p.x) - frameOld.size.width/2
sm.marker.isHidden = (mp4p.z < 0 || mp4p.z > 1)
}
这是一个很酷的小示例项目!
关于 z-clipping 问题的更新
projectPoint
方法returns一个3DSCNVector
,我们知道的x任意y坐标就是屏幕坐标。 z 坐标告诉我们点相对于远近裁剪平面的位置(z = 0 近裁剪平面,z = 1 远裁剪平面)。如果您为近裁剪平面设置负值,则将渲染相机后面的对象。我们没有负近裁剪平面,但我们也没有任何逻辑来说明如果这些投影点位置落在 z far 和 z near 范围之外会发生什么。
我更新了上面的代码以包含此 zNear 和 zFar 检查并相应地切换 UIView 可见性。
tl;dr
相机旋转 180 度时可见的标记位于相机后面,但它们仍被投影到视图平面上。由于我们没有检查他们是否在镜头后面,所以他们仍然显示。
要进行正确的投影,您应该计算世界坐标中的中点,然后将点投影到场景视图
let min = node.boundingBox.min
let max = node.boundingBox.max
//world coordinates
var mp1w = SCNVector3Make((min.x+max.x)/2, max.y, (min.z+max.z)/2) //top
var mp2w = SCNVector3Make((min.x+max.x)/2, min.y, (min.z+max.z)/2) //bottom
var mp3w = SCNVector3Make(min.x, (min.y+max.y)/2, (min.z+max.z)/2) //left
var mp4w = SCNVector3Make(max.x, (min.y+max.y)/2, (min.z+max.z)/2) //right
mp1w = node.convertPosition(mp1w, to: self.sceneView.scene.rootNode)
mp2w = node.convertPosition(mp2w, to: self.sceneView.scene.rootNode)
mp3w = node.convertPosition(mp3w, to: self.sceneView.scene.rootNode)
mp4w = node.convertPosition(mp4w, to: self.sceneView.scene.rootNode)
//projected coordinates
let mp1p = self.sceneView.projectPoint(mp1w) //top
let mp2p = self.sceneView.projectPoint(mp2w) //bottom
let mp3p = self.sceneView.projectPoint(mp3w) //left
let mp4p = self.sceneView.projectPoint(mp4w) //right