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 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.
① 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) ** Schauen wir sie uns unten einzeln an.
Dies wird im Szeneneditor von Xcode festgelegt.
→ Machen Sie die Farbe fast transparent. → Verringern Sie den Wert der Renderreihenfolge, die vor dem Zeichen gezeichnet werden soll, damit das Zeichen nicht im Hintergrund gezeichnet wird. 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
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.
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.
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.
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.
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.
・ 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