如何使用 ARKit 检测触摸并显示新的 SCNPlane?

Posted

技术标签:

【中文标题】如何使用 ARKit 检测触摸并显示新的 SCNPlane?【英文标题】:How to detect touch and show new SCNPlane using ARKit? 【发布时间】:2019-02-05 00:23:02 【问题描述】:

现在我可以在检测到卡时显示不同的SCNPlane。显示SCNPlanes后,用户触摸任意平面显示新的SCNPlane。但现在触摸工作正常,但新的SCNPlane 没有显示。

这是我尝试过的代码:

var cake_1_PlaneNode : SCNNode? = nil

func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) 

    guard let imageAnchor = anchor as? ARImageAnchor else  return 

    if let imageName  = imageAnchor.referenceImage.name             
        print(imageName)

        if imageName == "menu" 

            // Check To See The Detected Size Of Our menu Card (Should By 5cm*3cm)
            let menuCardWidth = imageAnchor.referenceImage.physicalSize.width
            let menuCardHeight =  imageAnchor.referenceImage.physicalSize.height

            print(
                """
                We Have Detected menu Card With Name \(imageName)
                \(imageName)'s Width Is \(menuCardWidth)
                \(imageName)'s Height Is \(menuCardHeight)
                """)

            //raspberry
            //cake 1

            let cake_1_Plane = SCNPlane(width: 0.045, height: 0.045)
            cake_1_Plane.firstMaterial?.diffuse.contents = UIImage(named: "france")
            cake_1_Plane.cornerRadius = 0.01

            let cake_1_PlaneNode = SCNNode(geometry: cake_1_Plane)
            self.cake_1_PlaneNode = cake_1_PlaneNode
            cake_1_PlaneNode.eulerAngles.x = -.pi/2
            cake_1_PlaneNode.runAction(SCNAction.moveBy(x: 0.15, y: 0, z: -0.125, duration: 0.75))

            node.addChildNode(cake_1_PlaneNode)
            self.sceneView.scene.rootNode.addChildNode(node)                
          
          

   override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) 

    let touch = touches.first as! UITouch
    if(touch.view == self.sceneView)
        //print("touch working")
        let viewTouchLocation:CGPoint = touch.location(in: sceneView)
        guard let result = sceneView.hitTest(viewTouchLocation, options: nil).first else 
            return
        

        if let planeNode = cake_1_PlaneNode, cake_1_PlaneNode == result.node
            print("match")
            cake_1() 
        
    


func cake_1() 

    let plane = SCNPlane(width: 0.15  , height: 0.15)
    plane.firstMaterial?.diffuse.contents = UIColor.black.withAlphaComponent(0.75)

    let planeNodee = SCNNode(geometry: plane)
    planeNodee.eulerAngles.x = -.pi / 2
    planeNodee.runAction(SCNAction.moveBy(x: 0.21, y: 0, z: 0, duration: 0))

 //cake_1

点击此链接:Detect touch on SCNNode in ARKit。

【问题讨论】:

请检查我的更新答案:) 【参考方案1】:

查看您的代码,我可以看到几个问题(更不用说变量和方法的命名约定了)。

首先,您正在创建一个 Global Variable,您已声明如下:

var cake_1_PlaneNode : SCNNode? = nil

但是,您在Delegate Callback 中同时使用LocalGlobal Variable 作为cake_1_PlaneNode

let cake_1_PlaneNode = SCNNode(geometry: cake_1_Plane)
self.cake_1_PlaneNode = cake_1_PlaneNode

应该简单地读成这样:

self.cake_1_PlaneNode = SCNNode(geometry: cake_1_Plane)

其次,您将cake_1_PlaneNode 添加到ARSCNViewrootNode,而不是检测到的ARImageAnchor,这可能是您不想做的,因为当检测到ARAnchor 时:

您可以通过附加几何图形为锚点提供视觉内容 (或其他 SceneKit 功能)到此节点或通过添加子节点。

因此,这种方法(除非你真的想这样做)是不必要的。

剩下的问题在于您的 cake_1 函数本身。

首先,您实际上并没有将您的planeNodee 添加到您的sceneHierachy

由于您尚未指定新初始化的planeNode 是否应直接添加到您的ARSCNView 或作为cake_1_planeNodechildNode,您的function 应包括以下内容之一:

self.sceneView.scene.rootNode.addChildNode(planeNodee)    
self.cake_1_planeNode.addChildNode(planeNodee)

此外,可能也不需要旋转planeNodee,因为默认情况下SCNPlane 是垂直渲染的。

由于您尚未规定内容的放置位置,因此可能没有必要使用 -.pi / 2,因为这可能使其肉眼几乎看不见。

另一个可能导致您在实际添加节点时看不到节点的问题是 Z 位置。

如果您将 2 个节点设置在同一位置,您可能会遇到称为 Z-fighting 的问题(您可以阅读有关 here 的更多信息)。因此,您可能应该在添加节点时稍微向前移动添加的节点,例如SCNVector3 (0,0,0.001)对此负责。

基于所有这些观点,我在下面提供了一个完整的工作示例:

import UIKit
import ARKit

//-------------------------
//MARK: - ARSCNViewDelegate
//-------------------------

extension ViewController: ARSCNViewDelegate 

    func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) 

        //1. Check We Have An ARImageAnchor, Then Get It's Reference Image & Name
        guard let imageAnchor = anchor as? ARImageAnchor else  return 

        let detectedTarget = imageAnchor.referenceImage

        guard let detectedTargetName = detectedTarget.name  else  return 

        //2. If We Have Detected Our Virtual Menu Then Add The CakeOnePlane
        if detectedTargetName == "cakeMenu" 

            let cakeOnePlaneGeometry = SCNPlane(width: 0.045, height: 0.045)
            cakeOnePlaneGeometry.firstMaterial?.diffuse.contents = UIColor.cyan
            cakeOnePlaneGeometry.cornerRadius = 0.01

            let cakeOnPlaneNode = SCNNode(geometry: cakeOnePlaneGeometry)
            cakeOnPlaneNode.eulerAngles.x = -.pi/2

            //3. To Allow Us To Easily Keep Track Our Our Currently Added Node We Will Assign It A Unique Name
            cakeOnPlaneNode.name = "Strawberry Cake"

            node.addChildNode(cakeOnPlaneNode)
            cakeOnPlaneNode.runAction(SCNAction.moveBy(x: 0.15, y: 0, z: 0, duration: 0.75))


        
    


class ViewController: UIViewController 

    @IBOutlet var augmentedRealityView: ARSCNView!
    let augmentedRealitySession = ARSession()
    let configuration = ARWorldTrackingConfiguration()

    //------------------
    //MARK: - Life Cycle
    //------------------

    override func viewDidLoad() 
        super.viewDidLoad()

        setupARSession()
    


    //-----------------
    //MARK: - ARSession
    //-----------------

    /// Runs The ARSession
    func setupARSession()

        //1. Load Our Detection Images
        guard let detectionImages = ARReferenceImage.referenceImages(inGroupNamed: "AR Resources", bundle: nil) else  return 

        //2. Configure & Run Our ARSession
        augmentedRealityView.session = augmentedRealitySession
        augmentedRealityView.delegate = self
        configuration.detectionImages = detectionImages
        augmentedRealitySession.run(configuration, options: [.resetTracking, .removeExistingAnchors])

    

    //--------------------
    //MARK: - Interaction
    //--------------------

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) 

        //1. Get The Current Touch Location & Perform An ARSCNHitTest To Check For Any Hit SCNNode's
        guard let currentTouchLocation = touches.first?.location(in: self.augmentedRealityView),
             let hitTestNode = self.augmentedRealityView.hitTest(currentTouchLocation, options: nil).first?.node else  return 

        //2. If We Have Hit Our Strawberry Cake Then We Call Our makeCakeOnNode Function
        if let cakeID = hitTestNode.name, cakeID == "Strawberry Cake"

            makeCakeOnNode(hitTestNode)

        
    


    /// Adds An SCNPlane To A Detected Cake Target
    ///
    /// - Parameter node: SCNNode
    func makeCakeOnNode(_ node: SCNNode)

        let planeGeometry = SCNPlane(width: 0.15  , height: 0.15)
        planeGeometry.firstMaterial?.diffuse.contents = UIColor.black.withAlphaComponent(0.75)

        let planeNode = SCNNode(geometry: planeGeometry)
        planeNode.position = SCNVector3(0, 0, 0.001)
        planeNode.runAction(SCNAction.moveBy(x: 0.21, y: 0, z: 0, duration: 0))
        node.addChildNode(planeNode)

    

在我的设备上产生以下结果:

据您所知,这似乎表明您放置内容的计算已关闭(当然,除非这是所需的结果)。

如您所见,所有内容都正确呈现,但是它们之间的间距很大,因此在进一步测试和开发时,您可能需要稍微平移设备才能看到所有内容。

希望对你有帮助...

【讨论】:

是否可以不用新的类定义? 嘿,我看不到飞机,但代码工作正常。 drive.google.com/file/d/1PnB45-TzJMML6NFlXovSPa12sEBnd7sb/…你能看看这个输出视频吗 试试这个:planeNodee.position = SCNVector3(0, 0, -1.5) 检查新更新的 Cake1 函数。你看不到它,因为你旋转了它! ^______^ 抱歉还是看不到飞机..drive.google.com/file/d/1bZF1RODiYI3aB6IQqz_G5i_xDgZY1I1y/…你能从链接中获取这段代码吗..【参考方案2】:

***请为您的变量和函数使用描述性和清晰的名称,这很难阅读和理解您的代码。您可以在此处阅读有关 swift 样式指南的更多信息:https://github.com/raywenderlich/swift-style-guide#naming

当用户触摸屏幕时,您正在创建一个新平面,但您并未将该平面添加到场景中,因此您的“cake_1()”函数只会创建一个新平面。 当 ARKit 检测到图像时,它会自动创建一个空节点并将其添加到我们的场景中,位于检测到的图像的中心。当检测到图像时,我们必须首先保留对 ARKit 为我们添加的节点的引用。 将此变量添加到类的顶部:

var detectedImageNode: SCNNode?

然后在 func renderer(renderer: didAdd node:, for anchor:) 中添加以下行:

detectedImageNode = node

现在我们有了对节点的引用,我们可以轻松地添加和删除其他节点。 在 cake_1() 的末尾添加以下行:

if let detectedImageNode = detectedImageNode 
    cake_1_PlaneNode?.removeFromParentNode()
    detectedImageNode.addChildNode(planeNodee)

您的最终代码应如下所示:

func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) 

    guard let imageAnchor = anchor as? ARImageAnchor else  return 

    if let imageName  = imageAnchor.referenceImage.name 
        print(imageName)

        if imageName == "menu" 

            let cake_1_Plane = SCNPlane(width: 0.045, height: 0.045)
            cake_1_Plane.firstMaterial?.diffuse.contents = UIImage(named: "france")
            cake_1_Plane.cornerRadius = 0.01

            let cake_1_PlaneNode = SCNNode(geometry: cake_1_Plane)
            self.cake_1_PlaneNode = cake_1_PlaneNode
            cake_1_PlaneNode.eulerAngles.x = -.pi/2
            cake_1_PlaneNode.runAction(SCNAction.moveBy(x: 0.15, y: 0, z: -0.125, duration: 0.75))

            node.addChildNode(cake_1_PlaneNode)
            // No need to add the following line. The node is already added to the scene
            //self.sceneView.scene.rootNode.addChildNode(node)                
            detectedImageNode = node
        
    


func cake_1() 

        let plane = SCNPlane(width: 0.15  , height: 0.15)
        plane.firstMaterial?.diffuse.contents = UIColor.black.withAlphaComponent(0.75)

        let planeNodee = SCNNode(geometry: plane)
        planeNodee.eulerAngles.x = -.pi / 2

        if let detectedImageNode = detectedImageNode 
            cake_1_PlaneNode?.removeFromParentNode()
            detectedImageNode.addChildNode(planeNodee)
            
    

替代解决方案

如果你只是想改变飞机的图像,那么更简单的方法就是改变飞机的纹理。 将 cake_1() 的内容替换为:

if let planeGeometry = cake_1_PlaneNode?.geometry 
    planeGeometry.firstMaterial?.diffuse.contents = UIImage(named: "newImage")

【讨论】:

实际上,我想在用户触摸 cake_1_PlaneNode 时创建新平面.. 从上面的代码它工作正常,它调用 cake_1 函数来创建平面但没有创建平面。 一个新平面被创建并添加到场景中,之前的平面被移除。确保您还更新了 cake_1() 函数。我已经编辑了我的答案以包含完整的代码

以上是关于如何使用 ARKit 检测触摸并显示新的 SCNPlane?的主要内容,如果未能解决你的问题,请参考以下文章

对象检测 ARKit 与 CoreML

取消其他视图的触摸事件

存储 ARKit 点云数据并检索显示

ARKit 图像检测 - 许多图像

如何使用设备指南针将对象或模型放置在统一 ARkit 中?

iOS-ARKit扫描和检测3D对象-Scanning and Detecting 3D Objects