[SWIFT] Navigation Web avec ARKit + SceneKit + Metal

J'ai pensé que je pourrais vouloir utiliser UIKit pour des objets virtuels sur mon smartphone ou ma vitre AR, j'ai donc essayé d'afficher UIView dans la scène de SceneKit. Puisqu'il est facile de vérifier l'opération, utilisez WKWebView comme UIView.

Image complète demo.pngdemo.gif

Informations de référence: ・ Tweet d'opération dans cette interface utilisateur Swift

Comment afficher UIView dans la scène

① Capturez ʻUIViewet convertissez-le enMTLTexutre ② ① est défini comme le matériau deSCNNode` de SceneKit

seulement ça. Cela fonctionne comme ça, mais voici quelques addictions.

Comment faire défiler la WebView

Pour faire défiler ou taper dans une WebView qui est un objet virtuel, il est nécessaire de passer un événement tactile à n'importe quelle WebView avec ʻARSCNView devant. Ici, j'ai trouvé une méthode pour recevoir des événements ʻUIResponder tels que touchesBegan () avec ʻARSCNView et les envoyer à touchesBegan () ʻetc. De n'importe quel WebView, mais le défilement et le tapotement ne fonctionnent pas du tout. .. En premier lieu, est-il possible d'effectuer des actions spécifiques aux parties de l'interface utilisateur telles que le défilement ou le tapotement de bouton lorsque UIEvent est passé? Je me sens comme ça. Manque de connaissances ici. .. .. Si quelqu'un sait, faites-le moi savoir. Si vous recherchez sur Google comment faire défiler le programme, vous pouvez voir comment le définir avec contentOffset de ʻUIScrollView`, mais cela ne devrait pas permettre un défilement inertiel confortable. Donc, à la fin, je n'ai pas trouvé de bonne méthode et je l'ai réalisée par la méthode suivante.

・ Placez WebView dans la même position avec la même taille que ʻARSCNView -Laisser ʻARSCNView devant et définir ʻisUserInteractionEnabled sur false -Effectuez un test de succès et définissez ʻisUserInteractionEnabled de WebView au centre de l'écran sur true.

Cela vous permet de faire défiler n'importe quelle WebView. Cependant, cette méthode présente les inconvénients suivants.

-Lors du défilement, WebView ne peut être réglé sur aucun format d'image. Il doit s'adapter à la taille de l'écran. -Puisque la position tapée ne peut pas être corrigée à la position de WebView dans la scène, le traitement d'événement tel que le clic de bouton ne peut pas être effectué (inutile).

De ce qui précède, il semble que les cas où cette méthode peut être utilisée sont limités aux cas où certaines informations sont affichées uniquement et l'opération est un défilement uniquement dans l'objet virtuel à l'écran. Je vais essayer à nouveau comment je peux le faire avec Swift UI comme le tweet ci-dessus.

La capture d'écran est lente et le rendu ne peut pas suivre

La synchronisation de la capture est définie sur renderer (_: updateAtTime:) de SCNSceneRendererDelegate, mais comme il est nécessaire de passer l'écran à Capture & SceneKit dans le thread principal, le processus de capture est mis en file d'attente dans la file d'attente principale dans cette méthode. .. Si la capture d'écran est assez rapide, cela peut suffire, mais comme il s'agit d'un processus difficile de capture lors du rendu 3D lors du suivi avec AR, la file d'attente principale se développe rapidement et l'écran se fige. J'ai rencontré le phénomène. Mesures pour empêcher la capture suivante jusqu'à ce que la capture soit terminée.

func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
    //Ne pas capturer si la capture d'écran dans la file d'attente principale n'est pas terminée
    //Le processus de capture est lent, donc sans lui, la file d'attente ne se bloquera pas.
    guard !isCaptureWaiting else { return }
    isCaptureWaiting = true

Code source complet

class ViewController: UIViewController, ARSCNViewDelegate {

    @IBOutlet weak var scnView: ARSCNView!
    @IBOutlet weak var webView1: WKWebView!
    @IBOutlet weak var webView2: WKWebView!
    @IBOutlet weak var webView3: WKWebView!
    
    private var device = MTLCreateSystemDefaultDevice()!
    private var planeNode1: SCNNode!
    private var planeNode2: SCNNode!
    private var planeNode3: SCNNode!

    private var node1Texture: MTLTexture?
    private var node2Texture: MTLTexture?
    private var node3Texture: MTLTexture?

    private var viewWidth: Int = 0
    private var viewHeight: Int = 0

    private var isCaptureWaiting = false

    override func viewDidLoad() {
        super.viewDidLoad()
        scnView.scene = SCNScene(named: "art.scnassets/sample.scn")!
        planeNode1 = scnView.scene.rootNode.childNode(withName: "plane1", recursively: true)
        planeNode2 = scnView.scene.rootNode.childNode(withName: "plane2", recursively: true)
        planeNode3 = scnView.scene.rootNode.childNode(withName: "plane3", recursively: true)
        //N'acceptez pas les événements de l'interface utilisateur une seule fois
        scnView.isUserInteractionEnabled = false
        setUIEnable(webView1: false, webView2: false, webView3: false)
        //ARSCN View est toujours au top
        self.view.bringSubviewToFront(scnView)
        //Session AR démarrée
        self.scnView.delegate = self
        let configuration = ARWorldTrackingConfiguration()
        self.scnView.session.run(configuration, options: [.removeExistingAnchors, .resetTracking])
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        if node1Texture == nil || node2Texture == nil || node3Texture == nil {
            viewWidth = Int(view.bounds.width)
            viewHeight = Int(view.bounds.height)
            //Tampon de texture sécurisé
            //Taille WebView= self.La taille de la vue
            let desc = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .rgba8Unorm,
                                                                width: viewWidth,
                                                                height: viewHeight,
                                                                mipmapped: false)
            node1Texture = device.makeTexture(descriptor: desc)!
            node2Texture = device.makeTexture(descriptor: desc)!
            node3Texture = device.makeTexture(descriptor: desc)!
            //Charge du site
            webView1.load(URLRequest(url: URL(string:"https://qiita.com")!))
            webView2.load(URLRequest(url: URL(string:"https://www.apple.com")!))
            webView3.load(URLRequest(url: URL(string:"https://stackoverflow.com")!))
        }
    }
    
    //Paramètre IsUserInteractionEnabled pour chaque WebView
    func setUIEnable(webView1: Bool, webView2: Bool, webView3: Bool) {
        self.webView1.isUserInteractionEnabled = webView1
        self.webView2.isUserInteractionEnabled = webView2
        self.webView3.isUserInteractionEnabled = webView3
    }
    
    func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
        guard let _node1Texture = node1Texture, let _node2Texture = node2Texture, let _node3Texture = node3Texture else { return }
        //Ne pas capturer si la capture d'écran dans la file d'attente principale n'est pas terminée
        //Le processus de capture est lent, donc sans lui, la file d'attente ne se bloquera pas.
        guard !isCaptureWaiting else { return }
        isCaptureWaiting = true
        
        DispatchQueue.main.async {
            //Effectuer un hit test sur le nœud et WebView au centre de l'écran(nœud)Trouver
            let bounds = self.scnView.bounds
            let screenCenter =  CGPoint(x: bounds.midX, y: bounds.midY)
            let options: [SCNHitTestOption: Any] = [
                .boundingBoxOnly: true,     //Testé avec boundingBox
                .firstFoundOnly: true       //Renvoie uniquement l'objet le plus en avant
            ]
            if let hitResult = self.scnView.hitTest(screenCenter, options: options).first,
               let nodeName = hitResult.node.name {
                //Définissez isUserInteractionEnabled de la WebView correspondant au nœud au centre de l'écran sur true
                switch nodeName {
                case "plane1":
                    self.setUIEnable(webView1: true, webView2: false, webView3: false)
                case "plane2":
                    self.setUIEnable(webView1: false, webView2: true, webView3: false)
                case "plane3":
                    self.setUIEnable(webView1: false, webView2: false, webView3: true)
                default:
                    self.setUIEnable(webView1: false, webView2: false, webView3: false)
                }
            } else {
                self.setUIEnable(webView1: false, webView2: false, webView3: false)
            }
            
            //Mettre à jour le matériau du nœud en capturant uniquement la WebView correspondant au nœud au centre de l'écran
            let setNodeMaterial: (UIView, SCNNode, MTLTexture) -> () = { captureView, node, texture in
                let material = SCNMaterial()
                material.diffuse.contents = captureView.takeTextureSnapshot(device: self.device,
                                                                            textureWidth: self.viewWidth,
                                                                            textureHeight: self.viewHeight,
                                                                            textureBuffer: texture)
                node.geometry?.materials = [material]
            }

            if self.webView1.isUserInteractionEnabled {
                setNodeMaterial(self.webView1, self.planeNode1, _node1Texture)
            } else if self.webView2.isUserInteractionEnabled {
                setNodeMaterial(self.webView2, self.planeNode2, _node2Texture)
            } else if self.webView3.isUserInteractionEnabled {
                setNodeMaterial(self.webView3, self.planeNode3, _node3Texture)
            }
            
            self.isCaptureWaiting = false
        }
    }
}

extension UIView {
    //Prenez une capture d'écran de n'importe quel UIView et convertissez-le en MTLTexture
    //URL de référence: https://stackoverflow.com/questions/61724043/render-uiview-contents-into-mtltexture
    func takeTextureSnapshot(device: MTLDevice, textureWidth: Int, textureHeight: Int, textureBuffer: MTLTexture) -> MTLTexture? {
        
        guard let context = CGContext(data: nil,
                                   width: textureWidth,
                                   height: textureHeight,
                                   bitsPerComponent: 8,
                                   bytesPerRow: 0,
                                   space: CGColorSpaceCreateDeviceRGB(),
                                   bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { return nil }
        //Éviter la tête en bas
        context.translateBy(x: 0, y: CGFloat(textureHeight))
        context.scaleBy(x: 1, y: -1)
        
        guard let data = context.data else { return nil }
        layer.render(in: context)
        
        textureBuffer.replace(region: MTLRegionMake2D(0, 0, textureWidth, textureHeight),
                              mipmapLevel: 0,
                              withBytes: data,
                              bytesPerRow: context.bytesPerRow)
        return textureBuffer
    }
}

Recommended Posts

Navigation Web avec ARKit + SceneKit + Metal
Camouflage optique avec ARKit + SceneKit + Metal ①
Camouflage optique avec ARKit + SceneKit + Metal ②
Effondrement du sol avec ARKit + SceneKit
Comment transformer des figurines ARKit et SceneKit avec Metal Shader
Essayez de vous serrer la main avec ARKit + Metal
Reproduction de "Vous êtes devant le roi Laputa" avec ARKit + SceneKit + Metal
Affichage du curseur lors du placement d'objets avec ARKit + SceneKit
Tout le monde est super Saiyan avec ARKit + Metal
Tester l'API Web avec junit
Application Web construite avec docker (1)
Scraping Web facile avec Jsoup