首先,你不能将一个UIView
作为SCNMaterialProperty
的contents
添加进去。在这里阅读相关文档:https://developer.apple.com/documentation/scenekit/scnmaterialproperty/1395372-contents。
我猜测,这很可能是你看到的冻结/崩溃的原因,因为当你弹出控制器并尝试清理那个实际上已经损坏的材质时,场景会被释放(它将把它视为其他类型)。
一般来说,这行代码:
myView = (Bundle.main.loadNibNamed("ARViewOne", owner: nil, options: nil)![0] as? UIView)!
看起来你对这个比较新(完全没关系!)。你在强制解包和强制转换(以一种奇怪的方式,“as? UIView!”基本上只是“as! UIView”),这总是很危险的。你确定xib中的第一个顶级对象是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
}
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)您只想要“视图外观”,即节点上的一种“截图”。显然,它将是静止的图像,您无法将视图的整个行为(附加的手势识别器等)放入此类节点中。对于这些内容,我建议在您满意本文后提出新问题。
根据Fabio的评论进行编辑:
我忘记了renderer(_:nodeFor :)
在不同的队列/线程上调用。正如警告“UIView.bounds必须仅从主线程使用”所说,您确实应该仅在主线程上生成视图图像。我更改了上面的代码以反映这一点。请记住,Swift本质上并不是线程安全的,上述工作是有效的,因为viewDidLoad()
将被调用(在主线程上),然后渲染器开始发送 renderer(_:nodeFor :) 调用。因此,您可以确保在检索图像时不会发生访问冲突,但这意味着您不应该在主线程的其他地方修改该图像。
如果您需要以某种方式更改图像/动态创建它,这里有一种替代方法。就我个人而言,我会使用委托的另一种方法renderer(_:didAdd:for :) ,而不是在 renderer(_:nodeFor :) 中自己创建节点。此方法不需要返回值,因此它看起来像这样:
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:)
,它不需要返回值。因此,我"切换到主队列"并添加所需的子节点。
我很确定这个方法是有效的,如果我没记错,SceneKit会自动批量处理添加(即在节点的addChildNode()
内部实际进行的操作)。因此,所有的"预处理工作"都在主线程完成,在那里你可以安全地使用视图的bounds
来创建图像。当节点设置好时,再将其添加,SceneKit会在其渲染线程上妥善地处理。
你不应该简单地在renderer(_:nodeFor:)
方法中放置一个DispatchQueue.main.async
,并在其中更改材质的漫射内容。这意味着你从不同的线程修改了添加的节点,但是你无法确保这样做不会发生冲突。它看起来可能有效,我不知道SceneKit是否以某种方式对此进行了保护,但我不会依赖于它。在我看来,这是不好的习惯。
希望这一切足以使你的项目工作。 :)