[SWIFT] Reproduktion von "Du bist vor König Laputa" mit ARKit + SceneKit + Metal

Ich sehe oft 3D-Modelle von Wänden und Böden in AR, also habe ich mich herausgefordert. Das Thema war "Laputa, die Burg am Himmel". Eine Szene, in der Musca und Theta von der Decke vor General Mouro in Laputas Beobachtungsraum erscheinen. (Ich denke, dass ein Raum mit einem Loch im Boden ein "Beobachtungsraum" ist, aber er folgt dem Ausdruck im Wiki)

(Abgesehen davon, dass das Modell ein kleinerer Panda ist.) Abgeschlossenes Bild demo.pngdemo.gif Das Problem bei der Reproduktion war der hellgelbe Teil der Grenze zwischen dem Charakter und der Decke (die Grenze kann mit Laputas Technologie hellgelb sein oder nicht, aber hier ist sie zu hellgelb vereinheitlicht). Die Wiedergabemethode wird unten erläutert.

Reproduktionsmethode

① Animieren Sie die Musca- und Theta-Knoten nach oben und unten (2) Erstellen Sie Tiefeninformationen (im Folgenden als Tiefe bezeichnet), um eine hellgelbe Oberfläche an der Deckengrenze zu erstellen. Machen Sie die folgenden drei. ・ Tiefe der Begrenzungsebene der Decke -Tiefe beim Zeichnen eines Zeichens mit cullMode = back -Tiefe beim Zeichnen mit cullMode = vor dem Zeichen ③ Beurteilen Sie die Grenzfläche und den Querschnitt des Zeichens anhand der Informationen in ② und fügen Sie dem Bild in ① hellgelb hinzu.

** Renderpfad (Xcode Capture GPU Frame) ** renderpass.png Schauen wir sie uns unten einzeln an.

① Animieren Sie die Musca- und Theta-Knoten nach oben und unten

Dies wird im Szeneneditor von Xcode festgelegt.

→ Machen Sie die Farbe fast transparent. slice_plane_1.png → Verringern Sie den Wert der Renderreihenfolge, die vor dem Zeichen gezeichnet werden soll, damit das Zeichen nicht im Hintergrund gezeichnet wird. slice_plane_2.png Hier wird die Kategorie-Bitmaske eingestellt. Wird später verwendet, um beim Generieren von Tiefe zwischen der Schnittstelle und dem Zeichen zu unterscheiden. Setze 4 für die Grenzfläche und 2 für das Zeichen.

→ Stellen Sie die Animation ein scene_animation.png

② Erstellen Sie Tiefeninformationen, um eine hellgelbe Oberfläche an der Deckengrenze zu erstellen

Die Tiefeninformationen auf der Vorderseite (Vorderseite) des Zeichens und die Tiefeninformationen auf der Rückseite (unsichtbare Seite) des Zeichens werden erfasst, und das tatsächliche Zeichen wird durch die Differenz erhalten.

  1. Holen Sie sich die Tiefe des hinteren Teils des Charakters → Durch Angabe von cullMode = front (später beschrieben) wird nur die Rückseite gezeichnet. ** Der Querschnitt des Zeichens ist größer als die hier erhaltene Tiefe. ** **.
  2. Ermitteln Sie die Tiefe des vorderen Teils des Charakters → Durch Angabe von cullMode = back (später beschrieben) wird nur die Vorderseite gezeichnet. ** Der Querschnitt des Zeichens ist kleiner als die hier erhaltene Tiefe. ** **.
  3. Ermitteln Sie die Tiefe der Grenzfläche (Deckenebene) ** In den Tiefen von 1) und 2) oben ist der Querschnitt des Zeichens der Bereich der Tiefe dieser Grenzfläche **.

Tiefeninformationen für jede der drei oben genannten werden durch Multi-Pass-Rendering von SCNTechnique generiert. Die Definition des Multi-Pass-Renderings lautet wie folgt.

tequnique.json


{
    "targets" : {
        "color_scene"     : { "type" : "color" },
        "depth_slice"     : { "type" : "depth" },
        "depth_cullback"  : { "type" : "depth" },
        "depth_cullfront" : { "type" : "depth" }
    },
    "passes" : {
        "pass_scene" : {
            "draw"    : "DRAW_SCENE",
            "outputs" : {
                "color" : "color_scene"
            }
        },
        "pass_slice" : {
            "draw"                : "DRAW_NODE",
            "includeCategoryMask" : 4,
            "outputs" : {
                "depth" : "depth_slice"
            },
            "depthStates" : {
                "clear" : true,
                "func" : "less"
            }
        },
        "pass_cullback" : {
            "draw"                : "DRAW_NODE",
            "includeCategoryMask" : 2,
            "cullMode"            : "back",
            "outputs" : {
                "depth" : "depth_cullback"
            },
            "depthStates" : {
                "clear" : true,
                "func" : "less"
            }
        },
        "pass_cullfront" : {
            "draw"                : "DRAW_NODE",
            "includeCategoryMask" : 2,
            "cullMode"            : "front",
            "outputs" : {
                "depth" : "depth_cullfront"
            },
            "depthStates" : {
                "clear" : true,
                "func" : "less"
            }
        },
        "pass_mix" : {
            "draw"   : "DRAW_QUAD",
            "inputs" : {
                "colorScene"     : "color_scene",
                "depthSlice"     : "depth_slice",
                "depthCullBack"  : "depth_cullback",
                "depthCullFront" : "depth_cullfront"
            },
            "metalVertexShader"   : "mix_vertex",
            "metalFragmentShader" : "mix_fragment",
            "outputs" : {
                "color" : "COLOR"
            },
            "colorStates" : {
                "clear"      : "true",
                "clearColor" : "0.0 0.0 0.0 0.0"
            }
        }
    },
    "sequence" : [
        "pass_scene",
        "pass_slice",
        "pass_cullback",
        "pass_cullfront",
        "pass_mix"
    ]
}

Schauen wir es uns nach und nach an.

        "pass_scene" : {
            "draw"    : "DRAW_SCENE",
            "outputs" : {
                "color" : "color_scene"
            }
        },

Dies ist die Definition des Zeichnens der gesamten Szene. Durch Angabe von "DRAW_SCENE" für "Zeichnen" wird das Kameraaufnahmebild + Zeichen gezeichnet. Das Zeichnungsergebnis besteht nur aus Farbinformationen und wird in einem Puffer mit dem Namen "color_scene" gespeichert.

        "pass_slice" : {
            "draw"                : "DRAW_NODE",
            "includeCategoryMask" : 4,
            "outputs" : {
                "depth" : "depth_slice"
            },
            "depthStates" : {
                "clear" : true,
                "func" : "less"
            }
        },

Dies ist eine Zeichnung der Deckengrenzfläche. 4 wird in includeCategoryMask angegeben und ist so eingestellt, dass nur die Grenzebene gezeichnet wird. Für diese Zeichnung sind keine Farbinformationen erforderlich. Nur die Tiefe wird in einem Puffer mit dem Namen "depth_slice" gespeichert.

        "pass_cullback" : {
            "draw"                : "DRAW_NODE",
            "includeCategoryMask" : 2,
            "cullMode"            : "back",
            "outputs" : {
                "depth" : "depth_cullback"
            },
            "depthStates" : {
                "clear" : true,
                "func" : "less"
            }
        },

Dies ist eine Definition zum Erfassen der Tiefeninformationen des Charakters von vorne betrachtet. 2 wird in includeCategoryMask angegeben, und nur das Zeichen soll gezeichnet werden. "Zurück" wird für "cullMode" angegeben, und der sichtbare Teil wird gezeichnet, und das Unsichtbare (Rückseite) wird nicht gezeichnet (Standard ist "Zurück"). Für diese Zeichnung sind keine Farbinformationen erforderlich. Nur die Tiefe wird in einem Puffer mit dem Namen "depth_cullback" gespeichert.

        "pass_cullfront" : {
            "draw"                : "DRAW_NODE",
            "includeCategoryMask" : 2,
            "cullMode"            : "front",
            "outputs" : {
                "depth" : "depth_cullfront"
            },
            "depthStates" : {
                "clear" : true,
                "func" : "less"
            }
        },

Dies ist eine Definition zum Abrufen der Tiefeninformationen auf der Rückseite des Zeichens. Ähnlich wie "pass_cullback", aber "cullMode" gibt "front" an.

        "pass_mix" : {
            "draw"   : "DRAW_QUAD",
            "inputs" : {
                "colorScene"     : "color_scene",
                "depthSlice"     : "depth_slice",
                "depthCullBack"  : "depth_cullback",
                "depthCullFront" : "depth_cullfront"
            },
            "metalVertexShader"   : "mix_vertex",
            "metalFragmentShader" : "mix_fragment",
            "outputs" : {
                "color" : "COLOR"
            },
            "colorStates" : {
                "clear"      : "true",
                "clearColor" : "0.0 0.0 0.0 0.0"
            }
        }

Dies ist die Definition, die schließlich die Kameraerfassung + Zeichen + Zeichenquerschnitt anzeigt. Das Ausgabeergebnis (Farbinformationen, Tiefe) jedes in "Eingaben" angegebenen Zeichnungspfads wird mit dem in "metalFragmentShader" angegebenen "Mix_fragment" -Fragment-Shader (später beschrieben) kombiniert, um das endgültige Bild zu erstellen. Es wird auf dem Bildschirm gezeichnet, indem "FARBE" für "Farbe" der "Ausgänge" angegeben wird.

③ Beurteilen Sie die Grenzfläche und den Querschnitt des Zeichens anhand der Informationen in ② und fügen Sie dem Bild in ① eine hellgelbe Farbe hinzu.

Dies geschieht mit dem oben erwähnten Shader "mix_fragment". Der Verarbeitungsinhalt entspricht dem im Kommentar in der Quelle beschriebenen und es wird festgelegt, ob in den Tiefeninformationen hellgelb angezeigt und der Farbe der gesamten Szene hinzugefügt werden soll.

fragment half4 mix_fragment(MixColorInOut vert [[stage_in]],
                            constant SCNSceneBuffer& scn_frame [[buffer(0)]],  //Rahmeninformationen zeichnen
                            texture2d<float, access::sample> colorScene [[texture(0)]],
                            depth2d<float,   access::sample> depthSlice [[texture(1)]],
                            depth2d<float,   access::sample> depthCullBack [[texture(2)]],
                            depth2d<float,   access::sample> depthCullFront [[texture(3)]])
{
    //Tiefe der Deckenschnittstelle
    float ds = depthSlice.sample(s, vert.uv);
    //Tiefe des Polygons, das vom Standpunkt zu Ihnen zeigt
    float db = depthCullBack.sample(s, vert.uv);
    //Tiefe des Polygons gegenüber dem Blickwinkel
    float df = depthCullFront.sample(s, vert.uv);
    
    float4 sliceColor = float4(0.0, 0.0, 0.0, 0.0);
    if (df < ds) {
        //Die Grenzfläche befindet sich vor der Rückseite des Zeichens
        if (ds < db) {
            //Außerdem befindet sich die Begrenzungsfläche hinter der Vorderseite des Zeichens
            sliceColor = float4(0.5, 0.5, 0.0, 0.0);    //Hellgelb
        }
    }
    //Fügen Sie dem gesamten Szenenbild, einschließlich des von der Kamera aufgenommenen Bildes, eine Rahmenfarbe hinzu
    float4 fragment_color = colorScene.sample(s, fract(vert.uv));
    fragment_color += sliceColor;   //Ich denke, es ist ein rauer Prozess, aber ich bin nicht mit der Handhabung von Farben vertraut, daher werde ich ihn überprüfen, wenn ich eine Chance habe.
    
    return half4(fragment_color);
}

Dies ist das Ende der Erklärung.

Ich konnte keine Möglichkeit finden, die Geometrie und den Kontaktbereich der Geometrie mit Google einzufärben. Dieses Mal denke ich, dass ich es irgendwie durch Versuch und Irrtum sehen kann, aber da diese Methode zusätzlich zur Tiefe der Grenzfläche nur zwei Tiefeninformationen der Vorder- und Rückseite des Zeichens erzeugt, befindet sich hinter dem Zeichen ein anderes Zeichen. In diesem Fall wird die Tiefe des hinteren Zeichens durch die Tiefe des vorderen Zeichens überschrieben, und es besteht das Problem, dass die Grenzfläche nicht gezeichnet wird. Ich denke, es gibt andere gute Möglichkeiten, also lassen Sie es mich bitte wissen, wenn Sie es wissen. Das Folgende sind die Inhalte, die ich im Prozess des Versuchs und Irrtums untersucht und ausprobiert habe.

Methode zur Reproduktion versucht

  1. Führen Sie viele Treffer-Tests durch, um die Form des Charakters auf der Benutzeroberfläche zu untersuchen Finden Sie die Oberflächenposition des Charakters, indem Sie 100 "hitTestWithSegment" (von: bis: options :) "von" SCNNode "nebeneinander auf der Grenzfläche anordnen und sie auf der Vorderseite → Rückseite und Rückseite → Vorderseite des Charakters testen. Ich habe einen Querschnitt gemacht. → Die Genauigkeit von hitTestWithSegment lag nicht auf dem erwarteten Niveau, und das Ergebnis unterschied sich geringfügig von der Form der Geometrie, sodass es nicht verwendet werden konnte. Insbesondere in kleinen Bereichen wie Ohren und Füßen weicht die Position des Trefferergebnisses stark vom Erscheinungsbild ab. Ich denke nicht, dass es eine völlig andere Verwendung als der ursprüngliche Zweck ist.

  2. Erstellen Sie eine Grenzflächengeometrie in Echtzeit -Flache die Geometrie des Charakters an der Grenze und mache diesen Teil hellgelb. → Wenn beispielsweise die Geometrie und die Grenzfläche wie ein Fuß miteinander in Kontakt stehen, scheint es ziemlich mühsam zu sein, die Geometrie sowohl am rechten als auch am linken Fuß zu glätten. Ich habe es nicht versucht. Wenn die Geometrie niedrig poly ist, scheint sie zu rasseln, so dass es erforderlich sein kann, die Geometrie durch Tessellation (?) Zu teilen.

Ganzer Quellcode

・ Schnell

ViewController.swift


class ViewController: UIViewController, ARSCNViewDelegate {

    @IBOutlet weak var scnView: ARSCNView!
    
    private let device = MTLCreateSystemDefaultDevice()!
    private var charNode: SCNNode!
    private var isTouching = false      //Berührungserkennung
    
    override func viewDidLoad() {
        super.viewDidLoad()
        //Laden von Zeichen. Ausgeliehene WWDC2017 SceneKit-Demo https://developer.apple.com/videos/play/wwdc2017/604/
        guard let scene = SCNScene(named: "art.scnassets/scene.scn"),
              let charNode = scene.rootNode.childNode(withName: "char_node", recursively: true) else { return }
        self.charNode = charNode
        self.charNode.isHidden = true
        //Einrichtung der Szenentechnik
        self.setupSCNTechnique()
        //AR-Sitzung gestartet
        self.scnView.delegate = self
        let configuration = ARWorldTrackingConfiguration()
        configuration.planeDetection = [.horizontal]
        self.scnView.session.run(configuration, options: [.removeExistingAnchors, .resetTracking])
    }
    //
    //Bild für Bild aufgerufen
    //
    func renderer(_ renderer: SCNSceneRenderer, updateAtTime _: TimeInterval) {
        
        if isTouching {
            //Der Bildschirm wurde berührt
            isTouching = false
            DispatchQueue.main.async {
                //Überspringen, falls angezeigt
                guard self.charNode.isHidden else { return }
                
                let bounds = self.scnView.bounds
                let screenCenter = CGPoint(x: bounds.midX, y: bounds.midY)
                let results = self.scnView.hitTest(screenCenter, types: [.existingPlaneUsingGeometry])
                guard let existingPlaneUsingGeometryResult = results.first(where: { $0.type == .existingPlaneUsingGeometry }),
                      let _ = existingPlaneUsingGeometryResult.anchor as? ARPlaneAnchor else {
                    //In der Mitte des Bildschirms befindet sich keine flache Oberfläche. Tun Sie also nichts
                    return
                }
                //Platzieren Sie die Musca- und Theta-Knoten in der Mitte des Bildschirms
                let position = existingPlaneUsingGeometryResult.worldTransform.columns.3
                self.scnView.scene.rootNode.addChildNode(self.charNode)
                self.charNode.simdPosition = SIMD3<Float>(position.x, position.y, position.z)
                self.charNode.isHidden = false
            }
        }
    }
    
    private func setupSCNTechnique() {
        guard let path = Bundle.main.path(forResource: "technique", ofType: "json") else { return }
        let url = URL(fileURLWithPath: path)
        guard let techniqueData = try? Data(contentsOf: url),
              let dict = try? JSONSerialization.jsonObject(with: techniqueData) as? [String: AnyObject] else { return }
        //Aktivieren Sie das Rendern in mehreren Durchgängen
        let technique = SCNTechnique(dictionary: dict)
        scnView.technique = technique
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let _ = touches.first else { return }
        isTouching = true
    }
}

· Metall

#include <metal_stdlib>
using namespace metal;
#include <SceneKit/scn_metal>

// SceneKit ->Shader-Lieferart
//Die Definition lautet https://developer.apple.com/documentation/scenekit/Siehe scnprogram
struct VertexInput {
    float4 position [[attribute(SCNVertexSemanticPosition)]];   //Apex-Koordinaten
};

struct MixColorInOut {
    float4 position [[position]];
    float2 uv;
};

vertex MixColorInOut mix_vertex(VertexInput in [[stage_in]],
                                        constant SCNSceneBuffer& scn_frame [[buffer(0)]])
{
    MixColorInOut out;
    out.position = in.position;
    //Koordinatensystem-1.0 ~ 1.0 -> 0.0 ~ 1.In 0 konvertieren. Die y-Achse ist invertiert.
    out.uv = float2((in.position.x + 1.0) * 0.5 , (in.position.y + 1.0) * -0.5);
    return out;
}

constexpr sampler s = sampler(coord::normalized,
                              address::repeat,
                              filter::nearest);

fragment half4 mix_fragment(MixColorInOut vert [[stage_in]],
                            constant SCNSceneBuffer& scn_frame [[buffer(0)]],  //Rahmeninformationen zeichnen
                            texture2d<float, access::sample> colorScene [[texture(0)]],
                            depth2d<float,   access::sample> depthSlice [[texture(1)]],
                            depth2d<float,   access::sample> depthCullBack [[texture(2)]],
                            depth2d<float,   access::sample> depthCullFront [[texture(3)]])
{
    //Tiefe der Deckenschnittstelle
    float ds = depthSlice.sample(s, vert.uv);
    //Tiefe des Polygons, das vom Standpunkt zu Ihnen zeigt
    float db = depthCullBack.sample(s, vert.uv);
    //Tiefe des Polygons gegenüber dem Blickwinkel
    float df = depthCullFront.sample(s, vert.uv);
    
    float4 sliceColor = float4(0.0, 0.0, 0.0, 0.0);
    if (df < ds) {
        //Die Grenzfläche befindet sich vor der Rückseite des Zeichens
        if (ds < db) {
            //Außerdem befindet sich die Begrenzungsfläche hinter der Vorderseite des Zeichens
            sliceColor = float4(0.5, 0.5, 0.0, 0.0);    //Hellgelb
        }
    }
    //Fügen Sie dem gesamten Szenenbild, einschließlich des von der Kamera aufgenommenen Bildes, eine Rahmenfarbe hinzu
    float4 fragment_color = colorScene.sample(s, fract(vert.uv));
    fragment_color += sliceColor;   //Ich denke, es ist ein rauer Prozess, aber ich bin nicht mit der Handhabung von Farben vertraut, daher werde ich ihn überprüfen, wenn ich eine Chance habe.
    
    return half4(fragment_color);
}

Recommended Posts

Reproduktion von "Du bist vor König Laputa" mit ARKit + SceneKit + Metal
Optische Tarnung mit ARKit + SceneKit + Metal ①
Optische Tarnung 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
So transformieren Sie ARKit- und SceneKit-Figuren mit Metal Shader
Bodenkollaps mit ARKit + SceneKit
Ist die von Ihnen verwendete Version von Elasticsearch mit Java 11 kompatibel?