ARKit 添加 UIView xib 文件 Swift 4

ARKit adding UIView xib file Swift 4

当我检测到我的锚点时,我将一个 xib UIView 文件作为 material 添加到我的 SCNBox,但是当我关闭它时 viewcontroller 应用程序冻结,这是我的代码:

var detectedDataAnchor: ARAnchor?
var myView = UIView()

override func viewDidLoad() {
  super.viewDidLoad()
  myView = (Bundle.main.loadNibNamed("ARViewOne", owner: nil, options: nil)![0] as? UIView)!
}



func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {

      if self.detectedDataAnchor?.identifier == anchor.identifier {
        let node = SCNNode()
        let box = SCNBox(width: 0.1, height: 0.1, length: 0.1,
                                 chamferRadius: 0.0)
        let imageMaterial = SCNMaterial()

        imageMaterial.diffuse.contents = myView
        box.materials = [imageMaterial]
        let cube = SCNNode(geometry: box)
        node.addChildNode(cube)
        return node

    }
   return nil
}

 @IBAction func back(_ sender: UIButton) {
     // here the app freezes
     navigationController?.popViewController(animated: true)
  }

当回到我的 VC 我的应用程序不响应任何触摸事件

好的,那么第一件事:

您不能将 UIView 添加为 SCNMaterialPropertycontents。阅读 here.

我在这里猜测,但这可能是你看到的 freeze/crash 的原因,因为当你弹出它所在的控制器并尝试清理 material 时场景被取消实际上是坏的(它会把它当作其他类型)。

一般来说行

myView = (Bundle.main.loadNibNamed("ARViewOne", owner: nil, options: nil)![0] as? UIView)!

在我看来你对此有点陌生(完全没问题!)。你是 force-unwrapping 和 force-casting(奇怪的是,as? UIView! 基本上就是 as! UIView),这总是很危险的。您确定您的 xib 中的第一个顶级 object 是 UIView 吗?此外,最初实例化的 UIView (在 var myView = UIView() 中从未使用过,为什么不在这里使用一个可选的(你可以使用一个隐式解包的,虽然我自己不喜欢这个)。怎么样这样做:

var myViewImage: UIImage?

override func viewDidLoad() {
    super.viewDidLoad()
    let myView = Bundle.main.loadNibNamed("ARViewOne", owner: nil, options: nil).first as? UIView
    myViewImage = myView?.asImage()
}

func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {

    guard let image = myViewImage, self.detectedDataAnchor?.identifier == anchor.identifier else { return nil }

    let node = SCNNode()
    let box = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0.0)
    let imageMaterial = SCNMaterial()
    imageMaterial.diffuse.contents = image
    box.materials = [imageMaterial]
    let cube = SCNNode(geometry: box)
    node.addChildNode(cube)
    return node
}

// this would be outside your controller class, i.e. the top-level of a swift file
extension UIView {
    func asImage() -> UIImage {
        let renderer = UIGraphicsImageRenderer(bounds: bounds)
        return renderer.image { rendererContext in
            layer.render(in: rendererContext.cgContext)
        }
    }
}

UIView 扩展名取自 ,感谢 Naveed J。)

这假设 a) 您的 xib 确实正确并且 b) 您只想要 "view's looks",即节点上的一种 "screenshot"。它将是一个静止图像,显然,您无法将视图的整个行为(附加的手势识别器等)放到这样的节点上。对于这些事情,我建议提出一个新问题(在您满意地完成此操作之后)。

编辑以回应 Fabio 的评论:

我忘记了 renderer(_:nodeFor:) 在另一个 queue/thread 上被调用。正如警告 "UIView.bounds must be used from main thread only" 所说,您确实应该只在主线程上生成视图图像。我更改了上面的代码以反映这一点。请记住 swift 本质上不是线程安全的,上面的工作是因为 viewDidLoad() 将在渲染器开始发送 renderer(_:nodeFor:) 调用之前调用(在主线程上)。因此,您可以确定在检索图像时不会发生访问冲突,但这意味着您不应该在主线程的某处修改所述图像。

如果您需要以某种方式即时更改 image/create 它,还有一个替代方案。我个人会使用委托的其他方法 renderer(_:didAdd:for:),而不是自己在 renderer(_:nodeFor:) 中创建节点。此方法不需要 return 值,因此它看起来像这样:

var myView: UIView?

override func viewDidLoad() {
    super.viewDidLoad()
    myView = Bundle.main.loadNibNamed("ARViewOne", owner: nil, options: nil).first as? UIView
}

func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
    DispatchQueue.main.async {
        self.attachCustomNode(to: node, for: anchor)
    }
}

func attachCustomNode(to node: SCNNode, for anchor: ARAnchor) {
    guard let theView = myView, self.detectedDataAnchor?.identifier == anchor.identifier else { return }

    let box = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0.0)
    let imageMaterial = SCNMaterial()
    imageMaterial.diffuse.contents = theView.asImage()
    box.materials = [imageMaterial]
    let cube = SCNNode(geometry: box)
    node.addChildNode(cube)
    return node
}

通过从您的委托中省略 renderer(_:nodeFor:),ARKit 会为您创建一个空节点。基本上这与您在 renderer(_:nodeFor:) 中所做的相同。然后它调用 renderer(_:didAdd:for:),它不期望 return 值。所以从那里我 "switch to the main queue" 并添加必要的 child。

我很确定这是可行的,IIRC SceneKit 在其渲染线程中自动批处理添加(即它在节点 addChildNode() 中实际执行的操作)。因此,上面的所有 "prep work" 都发生在主线程上,您可以在其中安全地使用视图的 bounds 来创建图像。节点已设置好,当您添加它时,SceneKit 会负责在其渲染线程上正确地执行此操作。

你应该做的是简单地在你的renderer(_:nodeFor:)方法中放置一个DispatchQueue.main.async并改变material的扩散内容那里。那将意味着您从不同的线程修改添加的节点,但您不能确定它在没有冲突的情况下工作。它可能看起来像工作,我不知道 SceneKit 是否以某种方式防止它,但我不会依赖它。无论如何,这是糟糕的风格。

我希望这一切足以使您的项目顺利进行。 :)