You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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),未覆盖自定义的桌子模型。修改方案如下:

  1. 保存桌子节点引用:生成桌子时,将桌子的SCNNode存入类属性,方便后续点击检测
  2. 调整点击检测逻辑:优先使用SceneKit的节点命中检测判断是否点击到桌子,未命中时再 fallback 到平面检测
  3. 封装骰子放置逻辑:抽离重复代码,提升可读性

修改后的完整代码:

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 10:15:43