[SWIFT] Surfen im Internet mit ARKit + SceneKit + Metal

Ich dachte, ich möchte UIKit möglicherweise für virtuelle Objekte auf meinem Smartphone oder AR-Glas verwenden, und habe daher versucht, UIView in der Szene von SceneKit anzuzeigen. Da es einfach ist, den Betrieb zu überprüfen, verwenden Sie "WKWebView" als UIView.

Vollständiges Bild demo.pngdemo.gif

Referenzinformationen: ・ Tweet der Operation in dieser Swift-Benutzeroberfläche

So zeigen Sie UIView in der Szene an

① Erfassen Sie "UIView" und konvertieren Sie es in "MTLExutre" ② ① wird als Material von SCNNode von SceneKit festgelegt

nur das. Es funktioniert so, aber hier sind einige Abhängigkeiten.

So scrollen Sie durch das WebView

Um in einer WebView, die ein virtuelles Objekt ist, einen Bildlauf durchzuführen oder darauf zu tippen, muss ein Berührungsereignis an eine beliebige WebView mit "ARSCNView" übergeben werden. Hier habe ich eine Methode entwickelt, um "UIResponder" -Ereignisse wie "touchBegan ()" mit "ARSCNView" zu empfangen und an "touchBegan ()" usw. eines beliebigen WebView zu senden, aber das Scrollen und Tippen funktioniert überhaupt nicht. .. Ist es überhaupt möglich, Aktionen auszuführen, die für UI-Teile spezifisch sind, wie z. B. Scrollen oder Tippen auf Schaltflächen, wenn UIEvent übergeben wird? Ich fühle mich so. Mangel an Wissen hier. .. .. Wenn jemand weiß, lassen Sie es mich bitte wissen. Wenn Sie googeln, wie Sie aus dem Programm scrollen, können Sie sehen, wie Sie es mit "contentOffset" von "UIScrollView" einstellen. Dies sollte jedoch kein komfortables Trägheits-Scrollen ermöglichen. Am Ende konnte ich keine gute Methode finden und realisierte sie mit der folgenden Methode.

Auf diese Weise können Sie durch jedes WebView scrollen. Dieses Verfahren weist jedoch die folgenden Nachteile auf.

Aus dem oben Gesagten geht hervor, dass die Fälle, in denen diese Methode verwendet werden kann, auf die Fälle beschränkt sind, in denen nur einige Informationen angezeigt werden und die Operation nur im virtuellen Objekt auf dem Bildschirm gescrollt wird. Ich werde noch einmal versuchen, wie ich es mit Swift UI wie dem obigen Tweet machen kann.

Die Bildschirmaufnahme ist langsam und das Rendern kann nicht mithalten

Das Capture-Timing ist auf renderer (_: updateAtTime :) von SCNSceneRendererDelegate eingestellt. Da der Bildschirm jedoch im Hauptthread an Capture & SceneKit übergeben werden muss, wird der Capture-Prozess bei dieser Methode in die Hauptwarteschlange eingereiht. .. Wenn die Bildschirmaufnahme schnell genug ist, kann dies ausreichen. Da es sich jedoch um einen harten Prozess beim Erfassen während des 3D-Renderns während der Verfolgung mit AR handelt, wird die Hauptwarteschlange schnell erweitert und der Bildschirm friert ein. Ich bin auf das Phänomen gestoßen. Maßnahmen, um die nächste Erfassung zu verhindern, bis die Erfassung abgeschlossen ist.

func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
    //Nicht erfassen, wenn die Bildschirmaufnahme in der Hauptwarteschlange nicht abgeschlossen ist
    //Der Erfassungsprozess ist langsam, daher wird die Warteschlange ohne ihn nicht eingeblendet und eingefroren.
    guard !isCaptureWaiting else { return }
    isCaptureWaiting = true

Ganzer Quellcode

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)
        //Akzeptieren Sie UI-Ereignisse nicht einmal
        scnView.isUserInteractionEnabled = false
        setUIEnable(webView1: false, webView2: false, webView3: false)
        //ARSCN View ist immer oben
        self.view.bringSubviewToFront(scnView)
        //AR-Sitzung gestartet
        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)
            //Sicherer Texturpuffer
            //WebView-Größe= self.Die Größe der Ansicht
            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)!
            //Site laden
            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")!))
        }
    }
    
    //IsUserInteractionEnabled-Einstellung für jede 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 }
        //Nicht erfassen, wenn die Bildschirmaufnahme in der Hauptwarteschlange nicht abgeschlossen ist
        //Der Erfassungsprozess ist langsam, daher wird die Warteschlange ohne ihn nicht eingeblendet und eingefroren.
        guard !isCaptureWaiting else { return }
        isCaptureWaiting = true
        
        DispatchQueue.main.async {
            //Führen Sie einen Treffertest des Knotens und von WebView in der Mitte des Bildschirms durch(Knoten)Rausfinden
            let bounds = self.scnView.bounds
            let screenCenter =  CGPoint(x: bounds.midX, y: bounds.midY)
            let options: [SCNHitTestOption: Any] = [
                .boundingBoxOnly: true,     //Getestet mit BoundingBox
                .firstFoundOnly: true       //Gibt nur das vorderste Objekt zurück
            ]
            if let hitResult = self.scnView.hitTest(screenCenter, options: options).first,
               let nodeName = hitResult.node.name {
                //Setzen Sie isUserInteractionEnabled der WebView, die dem Knoten in der Mitte des Bildschirms entspricht, auf 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)
            }
            
            //Aktualisieren Sie das Material des Knotens, indem Sie nur die WebView erfassen, die dem Knoten in der Mitte des Bildschirms entspricht
            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 {
    //Machen Sie einen Screenshot von UIView und konvertieren Sie ihn in MTLTexture
    //Referenz-URL: 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 }
        //Verkehrt herum vermeiden
        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

Surfen im Internet mit ARKit + SceneKit + Metal
Optische Tarnung mit ARKit + SceneKit + Metal ①
Optische Tarnung mit ARKit + SceneKit + Metal ②
Bodenkollaps mit ARKit + SceneKit
So transformieren Sie ARKit- und SceneKit-Figuren mit Metal Shader
Schütteln Sie Ihre Hände mit ARKit + Metal
Reproduktion von "Du bist vor König Laputa" mit ARKit + SceneKit + Metal
Cursoranzeige beim Platzieren von Objekten mit ARKit + SceneKit
Jeder ist super Saiyajin mit ARKit + Metal
Testen Sie die Web-API mit junit
Mit Docker erstellte Webanwendung (1)
Einfaches Web-Scraping mit Jsoup