如何在ARKit中将SCNScene模型放置于另一SCNScene模型之上?
问题描述
我正在开发一款AR应用,当APP检测到水平面时会自动生成桌子模型,目标是点击后能在桌子上放置骰子。目前桌子模型已能成功放置,但无法将骰子SCNScene模型直接放置在桌子模型上,仅能在水平面点击生成骰子。
当前实现代码:
import UIKit import SceneKit import ARKit class ViewController: UIViewController, ARSCNViewDelegate { @IBOutlet var sceneView: ARSCNView! var isTableAdded = false var diceArray = [SCNNode]() override func viewDidLoad() { super.viewDidLoad() sceneView.delegate = self sceneView.debugOptions = [ARSCNDebugOptions.showFeaturePoints] sceneView.autoenablesDefaultLighting = true } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) let configuration = ARWorldTrackingConfiguration() configuration.planeDetection = .horizontal sceneView.session.run(configuration) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) sceneView.session.pause() } // MARK: - ADDS DICE override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { print("Tapped") if let touch = touches.first { let touchLocation = touch.location(in: sceneView) let results = sceneView.hitTest(touchLocation, types: .existingPlaneUsingExtent) if let hitResults = results.first { let diceScene = SCNScene(named: "art.scnassets/diceCollada.scn")! if let diceNode = diceScene.rootNode.childNode(withName: "Dice", recursively: true) { diceNode.position = SCNVector3( x: hitResults.worldTransform.columns.3.x, y: hitResults.worldTransform.columns.3.y, z: hitResults.worldTransform.columns.3.z) diceArray.append(diceNode) sceneView.scene.rootNode.addChildNode(diceNode) } } } } // MARK: - RENDERS TABLE AT THE HORIZONTAL PLANE func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { print("plane detected") if isTableAdded == false { let planeAnchor = anchor let x = planeAnchor.transform.columns.3.x let y = planeAnchor.transform.columns.3.y let z = planeAnchor.transform.columns.3.z let idleScene = SCNScene(named: "art.scnassets/Table.scn")! let node = SCNNode() for child in idleScene.rootNode.childNodes{ node.addChildNode(child) } node.position = SCNVector3(x,y,z) node.scale = SCNVector3(0.001,0.001,0.001) sceneView.scene.rootNode.addChildNode(node) isTableAdded = true } print("Model placed") } func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) { guard let planeAnchor = anchor as? ARPlaneAnchor else { return } print("plane updated") } }
解决方案
问题核心是当前点击检测仅针对ARKit识别的水平面(.existingPlaneUsingExtent),未覆盖自定义的桌子模型。修改方案如下:
- 保存桌子节点引用:生成桌子时,将桌子的
SCNNode存入类属性,方便后续点击检测 - 调整点击检测逻辑:优先使用SceneKit的节点命中检测判断是否点击到桌子,未命中时再 fallback 到平面检测
- 封装骰子放置逻辑:抽离重复代码,提升可读性
修改后的完整代码:
import UIKit import SceneKit import ARKit class ViewController: UIViewController, ARSCNViewDelegate { @IBOutlet var sceneView: ARSCNView! var isTableAdded = false var tableNode: SCNNode? // 新增:存储桌子节点引用 var diceArray = [SCNNode]() override func viewDidLoad() { super.viewDidLoad() sceneView.delegate = self sceneView.debugOptions = [ARSCNDebugOptions.showFeaturePoints] sceneView.autoenablesDefaultLighting = true } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) let configuration = ARWorldTrackingConfiguration() configuration.planeDetection = .horizontal sceneView.session.run(configuration) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) sceneView.session.pause() } // MARK: - ADDS DICE override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { print("Tapped") guard let touch = touches.first else { return } let touchLocation = touch.location(in: sceneView) // 优先检测桌子模型的点击 let nodeHitResults = sceneView.hitTest(touchLocation, options: [ SCNHitTestOption.searchMode: SCNHitTestSearchMode.all.rawValue, SCNHitTestOption.ignoreHiddenNodes: true ]) if let tableHit = nodeHitResults.first(where: { $0.node === tableNode || $0.node.isDescendant(of: tableNode!) }) { // 在桌子上放置骰子 placeDice(at: tableHit.worldCoordinates) } else { // 未点击到桌子, fallback 到平面检测 let planeHitResults = sceneView.hitTest(touchLocation, types: .existingPlaneUsingExtent) if let planeHit = planeHitResults.first { placeDice(at: planeHit.worldTransform.columns.3) } } } // 新增:封装骰子放置逻辑 private func placeDice(at position: SCNVector3) { guard let diceScene = SCNScene(named: "art.scnassets/diceCollada.scn"), let diceNode = diceScene.rootNode.childNode(withName: "Dice", recursively: true) else { return } diceNode.position = position diceArray.append(diceNode) sceneView.scene.rootNode.addChildNode(diceNode) } // MARK: - RENDERS TABLE AT THE HORIZONTAL PLANE func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { print("plane detected") guard isTableAdded == false, let planeAnchor = anchor as? ARPlaneAnchor else { print("Model placed") return } let x = planeAnchor.transform.columns.3.x let y = planeAnchor.transform.columns.3.y let z = planeAnchor.transform.columns.3.z let idleScene = SCNScene(named: "art.scnassets/Table.scn")! let tableNode = SCNNode() for child in idleScene.rootNode.childNodes { tableNode.addChildNode(child) } tableNode.position = SCNVector3(x,y,z) tableNode.scale = SCNVector3(0.001,0.001,0.001) sceneView.scene.rootNode.addChildNode(tableNode) self.tableNode = tableNode // 保存桌子节点引用 isTableAdded = true print("Model placed") } func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) { guard let _ = anchor as? ARPlaneAnchor else { return } print("plane updated") } }
修改后,点击桌子表面时骰子会直接生成在桌子上,点击其他水平面区域时仍可在平面生成骰子,兼容原有逻辑。
内容的提问来源于stack exchange,提问作者user19727360
相关产品推荐
相关产品推荐

