[SWIFT] Optische Tarnung mit ARKit + SceneKit + Metal ②

In der Fortsetzung des vorherigen "ARKit + SceneKit + Optische Metalltarnung ①" habe ich versucht, das Gefühl auszudrücken, dass die optische Tarnung nicht in gutem Zustand ist.

demo2.gif

Wie zeichnet man eine Rauschtextur?

① Generieren Sie mit dem Compute Shader eine Blockrauschtextur (2) Ein Zeichenpfad für Zeichen wurde hinzugefügt, wobei (1) als Material verwendet wurde. ③ Hinzufügen ② zum zuletzt erstellten Bilderzeugungsprozess

Wenn Sie beim Ausführen der App "GPU-Frame erfassen" in Xcode ausführen, können Sie den Rendering-Pfad wie folgt überprüfen (überprüfen Sie dies mit Xcode12). Diesmal habe ich den handgeschriebenen roten Linienteil hinzugefügt. xcode2.png Dies ist praktisch, da Sie überprüfen können, welche Farbe und Tiefe für jeden Pfad ausgegeben werden. Tippen Sie beim Debuggen auf das Kamerasymbol, um einen "Capture GPU Frame" zu erstellen. xcode1.png

Blockieren Sie die Rauschgenerierung durch Compute Shader und setzen Sie auf SCNNode

Alles, was Sie brauchen, um eine Rauschtextur zu erzeugen, sind die zeitlich variierenden Informationen "timeParam" und xy-Koordinaten. Der Wert von "timeParam", der jedes Mal erhöht wird, wenn er gezeichnet wird, wird an den Shader übergeben, und der Shader bestimmt die Rauschfarbe basierend auf diesen Informationen und den xy-Koordinaten. Das Timing der Rauschgenerierung ist "Renderer (_ Renderer: SCNSceneRenderer, updateAtTime time: TimeInterval)".

・ Shader

shader.metal


//Zufällige Generierung
float rand(float2 co) {
    return fract(sin(dot(co.xy, float2(12.9898, 78.233))) * 43758.5453);
}

//Shader zur Erzeugung von Rauschbildern
kernel void blockNoise(const device float& time [[buffer(0)]],
                       texture2d<float, access::write> out [[texture(0)]],
                       uint2 id [[thread_position_in_grid]]) {
    //8px Block
    float2 uv = float2(id.x / 8, id.y / 8);
    float noise = fract(rand(rand(float2(float(uv.x * 50 + time), float(uv.y * 50 + time) + time))));
    float4 color = float4(0.0, noise, 0.0, 1.0);
    
    out.write(color, id);
}

・ Swift (Shader-Call-Teil)

ViewController.swift


private func setupMetal() {
(Weggelassen)
    //Berechnen Sie den Shader für die Geräuschentwicklung
    let noiseShader = library.makeFunction(name: "blockNoise")!
    self.computeState = try! self.device.makeComputePipelineState(function: noiseShader)
    //Puffer mit Zeitinformationen, die an den Shader übergeben werden sollen
    self.timeParamBuffer = self.device.makeBuffer(length: MemoryLayout<Float>.size, options: .cpuCacheModeWriteCombined)
    self.timeParamPointer = UnsafeMutableRawPointer(self.timeParamBuffer.contents()).bindMemory(to: Float.self, capacity: 1)
    //Thread-Gruppenraster
    self.threadgroupSize = MTLSizeMake(16, 16, 1)
    let threadCountW = (noiseTetureSize + self.threadgroupSize.width - 1) / self.threadgroupSize.width
    let threadCountH = (noiseTetureSize + self.threadgroupSize.height - 1) / self.threadgroupSize.height
    self.threadgroupCount = MTLSizeMake(threadCountW, threadCountH, 1)
}

func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
    //Inkrement für jede Zeichnung
    self.timeParam += 1;
    self.timeParamPointer.pointee = self.timeParam
    
    let commandBuffer = self.commandQueue.makeCommandBuffer()!
    let computeEncoder = commandBuffer.makeComputeCommandEncoder()!
    
    computeEncoder.setComputePipelineState(computeState)
    computeEncoder.setBuffer(self.timeParamBuffer, offset: 0, index: 0)
    computeEncoder.setTexture(noiseTexture, index: 0)
    computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
    computeEncoder.endEncoding()
    commandBuffer.commit()
    commandBuffer.waitUntilCompleted()
}

Die Ausgabe des Shaders wird von MTLTexture empfangen. Der Punkt ist, wie die empfangene Textur als Material des Charakters übergeben wird.

ViewController.swift


//Textur zum Schreiben von Rauschen
let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .rgba8Unorm,
                                                                 width: noiseTetureSize,
                                                                 height: noiseTetureSize,
                                                                 mipmapped: false)
textureDescriptor.usage = [.shaderWrite, .shaderRead]
self.noiseTexture = device.makeTexture(descriptor: textureDescriptor)!
//Stellen Sie die Rauschstruktur als Material des Knotens ein, das für die optische Tarnung vorgesehen ist
let node = self.rootNode.childNode(withName: "CamouflageNode", recursively: true)!
let material = SCNMaterial()
material.diffuse.contents = self.noiseTexture!
material.emission.contents = self.noiseTexture! //Schatten verhindern
node.geometry?.materials = [material]

Alles, was Sie tun müssen, ist, das erzeugte Rauschbild (Textur) auf "diffuse.contents" von "SCNMaterial" zu setzen und es auf die Geometrie des Zeichenknotens zu setzen. SceneKit erledigt den Rest. Ich habe versucht, das SCN-Programm zu verwenden, aber die Methode ist in diesem Artikel beschrieben.

Multi-Pass-Rendering

Ersetzen Sie den im vorherigen Artikel ausgegebenen optischen Tarnungsteil durch den diesmal zu zeichnenden Teil (Zeichen mit Rauschstruktur) (schalten Sie die Anzeige nach dem Zufallsprinzip um, um das Flackern auszudrücken).

Der zu "SCNTechnique" hinzugefügte Pfad lautet wie folgt.

technique.json


"pass_noise_node" : {
    "draw" : "DRAW_NODE",
    "includeCategoryMask" : 2,
    "outputs" : {
        "color" : "noise_color_node"
    }
},

Das liegt daran, dass ich den Charakter nur mit der Rauschstruktur zeichne. Farbinformationen werden an "color": "Noise_color_node" ausgegeben.

Der endgültige Shader für die Bilderzeugung wurde wie folgt geändert. Die Ausgabe von NoiseColorNode im obigen Pfad wird dem Argument hinzugefügt.

//Geräuschgenerierung Timing-Generierung
bool spike(float time) {
    float flickering = 0.3;     //Flackernder Zustand. Durch Erhöhen wird das Flackern erleichtert
    float piriod = -0.8;        //Eine flackernde Zeit. Je kleiner es ist, desto länger dauert das Flackern
    if (rand(time * 0.1) > (1.0 - flickering) && sin(time) > piriod) {
        return true;
    } else {
        return false;
    }
}

//Fragment-Shader zum Zusammensetzen der gesamten Szenen- und Knotennormalen
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> depthScene [[texture(1)]],
                            texture2d<float, access::sample> colorNode [[texture(2)]],
                            depth2d<float,   access::sample> depthNode [[texture(3)]],
                            texture2d<float, access::sample> noiseColorNode [[texture(4)]])
{
    float ds = depthScene.sample(s, vert.uv);    //Tiefe beim Zeichnen der gesamten Szene
    float dn = depthNode.sample(s, vert.uv);     //Tiefe beim Zeichnen eines Knotens
    
    float4 fragment_color;
    if (dn > ds) {
        if (spike(scn_frame.time)) {
            //Übernehmen Sie die Rauschtexturfarbe für das Rausch-Timing
            fragment_color = noiseColorNode.sample(s, fract(vert.uv));
            
        } else {
            //Da sich das Objekt, das optisch getarnt werden soll, vor dem in der Szene gezeichneten Objekt befindet, erzeugt es einen optischen Tarneffekt.
(Weggelassen)
        }

Zufällige Richtig / Falsch-Informationen werden mit "spike ()" erstellt, und die Anzeigefarbe wird zwischen einem verrauschten Zeichen und einem optischen Tarnzeichen umgeschaltet.

Ganzer Quellcode

・ Multi-Pass-Rendering-Definition

technique.json


{
    "targets" : {
        "color_scene" : { "type" : "color" },
        "depth_scene" : { "type" : "depth" },
        "color_node"  : { "type" : "color" },
        "depth_node"  : { "type" : "depth" },
        "noise_color_node"  : { "type" : "color" }
    },
    "passes" : {
        "pass_scene" : {
            "draw" : "DRAW_SCENE",
            "excludeCategoryMask" : 2,
            "outputs" : {
                "color" : "color_scene",
                "depth" : "depth_scene"
            },
            "colorStates" : {
                "clear" : true,
                "clearColor" : "sceneBackground"
            },
            "depthStates" : {
                "clear" : true,
                "func" : "less"
            }
        },
        "pass_node" : {
            "draw" : "DRAW_NODE",
            "includeCategoryMask" : 2,
            "metalVertexShader" : "node_vertex",
            "metalFragmentShader" : "node_fragment",
            "outputs" : {
                "color" : "color_node",
                "depth" : "depth_node"
            },
            "depthStates" : {
                "clear" : true,
                "func" : "less"
            }
        },
        "pass_noise_node" : {
            "draw" : "DRAW_NODE",
            "includeCategoryMask" : 2,
            "outputs" : {
                "color" : "noise_color_node"
            }
        },
        "pass_mix" : {
            "draw" : "DRAW_QUAD",
            "inputs" : {
                "colorScene" : "color_scene",
                "depthScene" : "depth_scene",
                "colorNode"  : "color_node",
                "depthNode"  : "depth_node",
                "noiseColorNode" : "noise_color_node"
            },
            "metalVertexShader" : "mix_vertex",
            "metalFragmentShader" : "mix_fragment",
            "outputs" : {
                "color" : "COLOR"
            },
            "colorStates" : {
                "clear" : "true"
            }
        }
    },
    "sequence" : [
        "pass_scene",
        "pass_node",
        "pass_noise_node",
        "pass_mix"
    ]
}

・ Shader

#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
    float2 texCoords [[attribute(SCNVertexSemanticTexcoord0)]]; //Texturkoordinaten
    float2 normal [[attribute(SCNVertexSemanticNormal)]];       //Normal
};

// SceneKit ->Shader-Lieferart(Für jeden Knoten)
//Die Definition lautet https://developer.apple.com/documentation/scenekit/Siehe scnprogram
struct PerNodeBuffer {
    float4x4 modelViewProjectionTransform;
};

struct NodeColorInOut {
    float4 position [[position]];
    float4 normal;
};

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

//Zufällige Generierung
float rand(float2 co) {
    return fract(sin(dot(co.xy, float2(12.9898, 78.233))) * 43758.5453);
}

//Geräuschgenerierung Timing-Generierung
bool spike(float time) {
    float flickering = 0.3;     //Flackernder Zustand. Durch Erhöhen wird das Flackern erleichtert
    float piriod = -0.8;        //Eine flackernde Zeit. Je kleiner es ist, desto länger dauert das Flackern
    if (rand(time * 0.1) > (1.0 - flickering) && sin(time) > piriod) {
        return true;
    } else {
        return false;
    }
}

//Vertex-Shader für Knoten
vertex NodeColorInOut node_vertex(VertexInput in [[stage_in]],
                                  constant SCNSceneBuffer& scn_frame [[buffer(0)]],  //Rahmeninformationen zeichnen
                                  constant PerNodeBuffer& scn_node [[buffer(1)]])    //Informationen für jeden Knoten
{
    NodeColorInOut out;
    out.position = scn_node.modelViewProjectionTransform * in.position;
    out.normal = scn_node.modelViewProjectionTransform * float4(in.normal, 1.0);
    return out;
}

//Fragment-Shader für Knoten
fragment half4 node_fragment(NodeColorInOut vert [[stage_in]])
{
    //Die normale Verwendung ist x,y nur. Weil es als Farbinformation behandelt wird-1.0 ~ 1.0 -> 0.0 ~ 1.In 0 konvertieren
    float4 color =  float4((vert.normal.x + 1.0) * 0.5 , (vert.normal.y + 1.0) * 0.5, 0.0, 0.0);
    return half4(color);        //Gibt normale Linien als Farbinformationen aus. Diese Informationen verzerren den Hintergrund des optischen Tarnziels
}

//Vertex-Shader zum Zusammensetzen der gesamten Szenen- und Knotennormalen
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,    // clamp_to_edge/clamp_to_border(iOS14)Nein.
                              filter::nearest);

//Fragment-Shader zum Zusammensetzen der gesamten Szenen- und Knotennormalen
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> depthScene [[texture(1)]],
                            texture2d<float, access::sample> colorNode [[texture(2)]],
                            depth2d<float,   access::sample> depthNode [[texture(3)]],
                            texture2d<float, access::sample> noiseColorNode [[texture(4)]])
{
    float ds = depthScene.sample(s, vert.uv);    //Tiefe beim Zeichnen der gesamten Szene
    float dn = depthNode.sample(s, vert.uv);     //Tiefe beim Zeichnen eines Knotens
    
    float4 fragment_color;
    if (dn > ds) {
        if (spike(scn_frame.time)) {
            //Übernehmen Sie die Rauschtexturfarbe für das Rausch-Timing
            fragment_color = noiseColorNode.sample(s, fract(vert.uv));
            
        } else {
            //Da sich das Objekt, das optisch getarnt werden soll, vor dem in der Szene gezeichneten Objekt befindet, erzeugt es einen optischen Tarneffekt.
            float3 normal_map = colorNode.sample(s, vert.uv).rgb;
            // 0.0 ~ 1.0 -> -1.0 ~ 1.Kehren Sie zu 0 zurück, damit es als Koordinate verwendet werden kann
            normal_map.xy = normal_map.xy * 2 - 1.0;
            //Die Position der zu übernehmenden Hintergrundfarbe liegt in der normalen Richtung des Knotens.(xy Flugzeug)Machen Sie einen verzerrten Hintergrund, um ein wenig versetzt zu werden
            float2 uv = vert.uv + normal_map.xy * 0.1;
            if (uv.x > 1.0 ||  uv.x < 0.0) {
                //Vermeiden Sie es, Farben außerhalb des Bildschirms zu verwenden(Ich wollte es mit der Adressierung des Samplers lösen, aber es funktionierte nicht)
                fragment_color = colorScene.sample(s, fract(vert.uv));
            } else {
                fragment_color = colorScene.sample(s, fract(uv));
            }
        }
    } else {
        //Da sich das Objekt, das optisch getarnt werden soll, hinter dem in der Szene gezeichneten Objekt befindet, wird die Farbe auf der Szenenseite unverändert übernommen
        fragment_color = colorScene.sample(s, fract(vert.uv));
    }
    
    return half4(fragment_color);
}

//Shader zur Erzeugung von Rauschbildern
kernel void blockNoise(const device float& time [[buffer(0)]],
                       texture2d<float, access::write> out [[texture(0)]],
                       uint2 id [[thread_position_in_grid]]) {
    //8px Block
    float2 uv = float2(id.x / 8, id.y / 8);
    float noise = fract(rand(rand(float2(float(uv.x * 50 + time), float(uv.y * 50 + time) + time))));
    float4 color = float4(0.0, noise, 0.0, 1.0);
    
    out.write(color, id);
}

・ Schnell

ViewController.swift


import ARKit
import SceneKit

class ViewController: UIViewController, ARSCNViewDelegate {

    @IBOutlet weak var scnView: ARSCNView!
    
    private var rootNode: SCNNode!

    private let device = MTLCreateSystemDefaultDevice()!
    private var commandQueue: MTLCommandQueue!
    
    private var computeState: MTLComputePipelineState! = nil
    private var noiseTexture: MTLTexture! = nil
    
    private let noiseTetureSize = 256
    private var threadgroupSize: MTLSize!
    private var threadgroupCount: MTLSize!
    private var timeParam: Float = 0
    private var timeParamBuffer: MTLBuffer!
    private var timeParamPointer: UnsafeMutablePointer<Float>!
    
    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/max.scn"),
              let rootNode = scene.rootNode.childNode(withName: "root", recursively: true) else { return }
        self.rootNode = rootNode
        self.rootNode.isHidden = true
        
        //Metallaufbau
        self.setupMetal()
        //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])
    }
    
    private func setupMetal() {
        self.commandQueue = self.device.makeCommandQueue()!
        let library = self.device.makeDefaultLibrary()!
        //Textur zum Schreiben von Rauschen
        let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .rgba8Unorm,
                                                                         width: noiseTetureSize,
                                                                         height: noiseTetureSize,
                                                                         mipmapped: false)
        textureDescriptor.usage = [.shaderWrite, .shaderRead]
        self.noiseTexture = device.makeTexture(descriptor: textureDescriptor)!
        //Stellen Sie die Rauschstruktur als Material des Knotens ein, das für die optische Tarnung vorgesehen ist
        let node = self.rootNode.childNode(withName: "CamouflageNode", recursively: true)!
        let material = SCNMaterial()
        material.diffuse.contents = self.noiseTexture!
        material.emission.contents = self.noiseTexture! //Schatten verhindern
        node.geometry?.materials = [material]
        //Berechnen Sie den Shader für die Geräuschentwicklung
        let noiseShader = library.makeFunction(name: "blockNoise")!
        self.computeState = try! self.device.makeComputePipelineState(function: noiseShader)
        //Puffer mit Zeitinformationen, die an den Shader übergeben werden sollen
        self.timeParamBuffer = self.device.makeBuffer(length: MemoryLayout<Float>.size, options: .cpuCacheModeWriteCombined)
        self.timeParamPointer = UnsafeMutableRawPointer(self.timeParamBuffer.contents()).bindMemory(to: Float.self, capacity: 1)
        //Thread-Gruppenraster
        self.threadgroupSize = MTLSizeMake(16, 16, 1)
        let threadCountW = (noiseTetureSize + self.threadgroupSize.width - 1) / self.threadgroupSize.width
        let threadCountH = (noiseTetureSize + self.threadgroupSize.height - 1) / self.threadgroupSize.height
        self.threadgroupCount = MTLSizeMake(threadCountW, threadCountH, 1)
    }
    
    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
    }
    
    func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
        //Inkrement für jede Zeichnung
        self.timeParam += 1;
        self.timeParamPointer.pointee = self.timeParam
        
        let commandBuffer = self.commandQueue.makeCommandBuffer()!
        let computeEncoder = commandBuffer.makeComputeCommandEncoder()!
        
        computeEncoder.setComputePipelineState(computeState)
        computeEncoder.setBuffer(self.timeParamBuffer, offset: 0, index: 0)
        computeEncoder.setTexture(noiseTexture, index: 0)
        computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
        computeEncoder.endEncoding()
        commandBuffer.commit()
        commandBuffer.waitUntilCompleted()
    }
    
    func renderer(_: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
        guard let planeAnchor = anchor as? ARPlaneAnchor, self.rootNode.isHidden else { return }
        self.rootNode.simdPosition = planeAnchor.center
        self.rootNode.isHidden = false
        DispatchQueue.main.async {
            //Zeigen Sie das Objekt in der erkannten Ebene an
            node.addChildNode(self.rootNode)
        }
    }
}

Recommended Posts

Optische Tarnung mit ARKit + SceneKit + Metal ①
Optische Tarnung mit ARKit + SceneKit + Metal ②
Surfen im Internet mit ARKit + SceneKit + Metal
Bodenkollaps mit ARKit + SceneKit
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