[SWIFT] Camouflage optique avec ARKit + SceneKit + Metal ②

Dans la suite du précédent "ARKit + SceneKit + Metal Optical Camouflage ①", j'ai essayé d'exprimer le sentiment que le camouflage optique n'est pas en bon état.

demo2.gif

Comment dessiner une texture de bruit

① Générer une texture de bruit de bloc avec un shader de calcul (2) Ajout d'un chemin de dessin pour les caractères utilisant (1) comme matériau. ③ Ajouter ② au dernier processus de génération d'image créé la dernière fois -Dessiner l'image de camouflage optique ou l'image de bruit de bloc de ② ・ Calendrier de dessin aléatoire

Si vous faites Capture GPU Frame avec Xcode lors de l'exécution de l'application, vous pouvez vérifier le chemin de rendu comme suit (vérifiez avec Xcode12). Cette fois, j'ai ajouté la partie de la ligne rouge manuscrite. xcode2.png C'est pratique car vous pouvez vérifier la couleur et la profondeur de sortie pour chaque chemin. Appuyez sur l'icône de la caméra pendant le débogage pour créer un Capture GPU Frame. xcode1.png

Bloquer la génération de bruit en calculant le shader et en définissant SCNNode

Tout ce dont vous avez besoin pour générer une texture de bruit est l'information variable dans le temps «timeParam» et les coordonnées xy. La valeur de timeParam, qui est incrémentée à chaque fois qu'il est dessiné, est transmise au shader, et le shader détermine la couleur du bruit en fonction de ces informations et des coordonnées xy. Le moment de la génération du bruit est renderer (_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval).

・ Shader

shader.metal


//Génération aléatoire
float rand(float2 co) {
    return fract(sin(dot(co.xy, float2(12.9898, 78.233))) * 43758.5453);
}

//Bloquer le shader de génération d'image de bruit
kernel void blockNoise(const device float& time [[buffer(0)]],
                       texture2d<float, access::write> out [[texture(0)]],
                       uint2 id [[thread_position_in_grid]]) {
    //Bloc 8px
    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 (partie d'appel de shader)

ViewController.swift


private func setupMetal() {
(Omis)
    //Calculer un shader pour la création de bruit
    let noiseShader = library.makeFunction(name: "blockNoise")!
    self.computeState = try! self.device.makeComputePipelineState(function: noiseShader)
    //Tampon des informations de temps à passer au shader
    self.timeParamBuffer = self.device.makeBuffer(length: MemoryLayout<Float>.size, options: .cpuCacheModeWriteCombined)
    self.timeParamPointer = UnsafeMutableRawPointer(self.timeParamBuffer.contents()).bindMemory(to: Float.self, capacity: 1)
    //Grille de groupe de threads
    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) {
    //Incrément pour chaque dessin
    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()
}

La sortie du shader est reçue par MTLTexture. Le point est de savoir comment faire passer la texture reçue comme matériau du personnage.

ViewController.swift


//Texture pour écrire du bruit
let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .rgba8Unorm,
                                                                 width: noiseTetureSize,
                                                                 height: noiseTetureSize,
                                                                 mipmapped: false)
textureDescriptor.usage = [.shaderWrite, .shaderRead]
self.noiseTexture = device.makeTexture(descriptor: textureDescriptor)!
//Définir la texture du bruit comme matériau du nœud ciblé pour le camouflage optique
let node = self.rootNode.childNode(withName: "CamouflageNode", recursively: true)!
let material = SCNMaterial()
material.diffuse.contents = self.noiseTexture!
material.emission.contents = self.noiseTexture! //Empêcher les ombres
node.geometry?.materials = [material]

Tout ce que vous avez à faire est de définir l'image de bruit générée (texture) sur diffuse.contents of SCNMaterial et de la définir sur la géométrie du nœud de caractère. SceneKit fera le reste. J'ai essayé cela dans le sens de l'utilisation du programme SCN, mais la méthode est décrite dans cet article.

Rendu multi-passes

Remplacez la partie de camouflage optique qui était sortie dans l'article précédent par la partie à dessiner cette fois (caractère avec texture de bruit) (basculez l'affichage à un moment aléatoire pour exprimer le scintillement).

Le chemin ajouté à «SCNTechnique» est le suivant.

technique.json


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

C'est tout parce que je dessine uniquement le personnage avec la texture du bruit. Les informations de couleur sont envoyées à "" color ":" noise_color_node "`.

Le shader de génération d'image finale a été modifié comme suit. La sortie noiseColorNode dans le chemin ci-dessus est ajoutée à l'argument.

//Génération de synchronisation de génération de bruit
bool spike(float time) {
    float flickering = 0.3;     //État de scintillement. L'augmenter facilite le scintillement
    float piriod = -0.8;        //Une période vacillante. Plus il est petit, plus il met de temps à clignoter
    if (rand(time * 0.1) > (1.0 - flickering) && sin(time) > piriod) {
        return true;
    } else {
        return false;
    }
}

//Fragment shader pour composer la scène entière et les normales de nœud
fragment half4 mix_fragment(MixColorInOut vert [[stage_in]],
                            constant SCNSceneBuffer& scn_frame [[buffer(0)]],  //Informations sur le cadre de dessin
                            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);    //Profondeur lors du dessin de la scène entière
    float dn = depthNode.sample(s, vert.uv);     //Profondeur lors du dessin d'un nœud
    
    float4 fragment_color;
    if (dn > ds) {
        if (spike(scn_frame.time)) {
            //Adoptez la couleur de la texture du bruit pour la synchronisation du bruit
            fragment_color = noiseColorNode.sample(s, fract(vert.uv));
            
        } else {
            //Étant donné que l'objet à camouflage optique est devant l'objet dessiné dans la scène, il produit un effet de camouflage optique.
(Omis)
        }

Des informations aléatoires vraies / fausses sont créées avec spike (), et la couleur d'affichage est commutée entre un caractère bruyant et un caractère de camouflage optique.

Code source complet

・ Définition du rendu multi-passes

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 ->Type de livraison de shader
//La définition est https://developer.apple.com/documentation/scenekit/Voir scnprogram
struct VertexInput {
    float4 position [[attribute(SCNVertexSemanticPosition)]];   //Coordonnées Apex
    float2 texCoords [[attribute(SCNVertexSemanticTexcoord0)]]; //Coordonnées de texture
    float2 normal [[attribute(SCNVertexSemanticNormal)]];       //Ordinaire
};

// SceneKit ->Type de livraison Shader(Pour chaque nœud)
//La définition est https://developer.apple.com/documentation/scenekit/Voir scnprogram
struct PerNodeBuffer {
    float4x4 modelViewProjectionTransform;
};

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

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

//Génération aléatoire
float rand(float2 co) {
    return fract(sin(dot(co.xy, float2(12.9898, 78.233))) * 43758.5453);
}

//Génération de synchronisation de génération de bruit
bool spike(float time) {
    float flickering = 0.3;     //État de scintillement. L'augmenter facilite le scintillement
    float piriod = -0.8;        //Une période vacillante. Plus il est petit, plus il met de temps à clignoter
    if (rand(time * 0.1) > (1.0 - flickering) && sin(time) > piriod) {
        return true;
    } else {
        return false;
    }
}

//Vertex shader pour les nœuds
vertex NodeColorInOut node_vertex(VertexInput in [[stage_in]],
                                  constant SCNSceneBuffer& scn_frame [[buffer(0)]],  //Informations sur le cadre de dessin
                                  constant PerNodeBuffer& scn_node [[buffer(1)]])    //Informations pour chaque nœud
{
    NodeColorInOut out;
    out.position = scn_node.modelViewProjectionTransform * in.position;
    out.normal = scn_node.modelViewProjectionTransform * float4(in.normal, 1.0);
    return out;
}

//Fragment shader pour les nœuds
fragment half4 node_fragment(NodeColorInOut vert [[stage_in]])
{
    //La normale à utiliser est x,y uniquement. Parce qu'il est traité comme une information de couleur-1.0 ~ 1.0 -> 0.0 ~ 1.Convertir en 0
    float4 color =  float4((vert.normal.x + 1.0) * 0.5 , (vert.normal.y + 1.0) * 0.5, 0.0, 0.0);
    return half4(color);        //Produit des lignes normales sous forme d'informations de couleur Cette information déforme l'arrière-plan de la cible de camouflage optique
}

//Vertex shader pour la composition de la scène entière et des normales de nœud
vertex MixColorInOut mix_vertex(VertexInput in [[stage_in]],
                                        constant SCNSceneBuffer& scn_frame [[buffer(0)]])
{
    MixColorInOut out;
    out.position = in.position;
    //Système de coordonnées-1.0 ~ 1.0 -> 0.0 ~ 1.Convertir en 0. L'axe y est inversé.
    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)Non.
                              filter::nearest);

//Fragment shader pour la composition de la scène entière et des normales de nœud
fragment half4 mix_fragment(MixColorInOut vert [[stage_in]],
                            constant SCNSceneBuffer& scn_frame [[buffer(0)]],  //Informations sur le cadre de dessin
                            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);    //Profondeur lors du dessin de la scène entière
    float dn = depthNode.sample(s, vert.uv);     //Profondeur lors du dessin d'un nœud
    
    float4 fragment_color;
    if (dn > ds) {
        if (spike(scn_frame.time)) {
            //Adoptez la couleur de la texture du bruit pour la synchronisation du bruit
            fragment_color = noiseColorNode.sample(s, fract(vert.uv));
            
        } else {
            //Étant donné que l'objet à camouflage optique est devant l'objet dessiné dans la scène, il produit un effet de camouflage optique.
            float3 normal_map = colorNode.sample(s, vert.uv).rgb;
            // 0.0 ~ 1.0 -> -1.0 ~ 1.Revenir à 0 pour qu'il puisse être utilisé comme coordonnée
            normal_map.xy = normal_map.xy * 2 - 1.0;
            //La position de la couleur de fond à adopter est dans la direction normale du nœud.(avion xy)Faites-en un arrière-plan déformé pour être un peu décalé
            float2 uv = vert.uv + normal_map.xy * 0.1;
            if (uv.x > 1.0 ||  uv.x < 0.0) {
                //Évitez d'adopter des couleurs en dehors de l'écran(Je voulais le résoudre avec l'adressage de l'échantillonneur, mais cela n'a pas fonctionné)
                fragment_color = colorScene.sample(s, fract(vert.uv));
            } else {
                fragment_color = colorScene.sample(s, fract(uv));
            }
        }
    } else {
        //Étant donné que l'objet à camouflage optique se trouve derrière l'objet dessiné dans la scène, la couleur du côté de la scène est adoptée telle quelle
        fragment_color = colorScene.sample(s, fract(vert.uv));
    }
    
    return half4(fragment_color);
}

//Bloquer le shader de génération d'image de bruit
kernel void blockNoise(const device float& time [[buffer(0)]],
                       texture2d<float, access::write> out [[texture(0)]],
                       uint2 id [[thread_position_in_grid]]) {
    //Bloc 8px
    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);
}

· Rapide

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()
        //Chargement des caractères. Emprunté WWDC2017 SceneKit Démo 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
        
        //Configuration en métal
        self.setupMetal()
        //Configuration de la technique de scène
        self.setupSCNTechnique()
        
        //Session AR démarrée
        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()!
        //Texture pour écrire du bruit
        let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .rgba8Unorm,
                                                                         width: noiseTetureSize,
                                                                         height: noiseTetureSize,
                                                                         mipmapped: false)
        textureDescriptor.usage = [.shaderWrite, .shaderRead]
        self.noiseTexture = device.makeTexture(descriptor: textureDescriptor)!
        //Définir la texture du bruit comme matériau du nœud ciblé pour le camouflage optique
        let node = self.rootNode.childNode(withName: "CamouflageNode", recursively: true)!
        let material = SCNMaterial()
        material.diffuse.contents = self.noiseTexture!
        material.emission.contents = self.noiseTexture! //Empêcher les ombres
        node.geometry?.materials = [material]
        //Calculer un shader pour la création de bruit
        let noiseShader = library.makeFunction(name: "blockNoise")!
        self.computeState = try! self.device.makeComputePipelineState(function: noiseShader)
        //Tampon des informations de temps à passer au shader
        self.timeParamBuffer = self.device.makeBuffer(length: MemoryLayout<Float>.size, options: .cpuCacheModeWriteCombined)
        self.timeParamPointer = UnsafeMutableRawPointer(self.timeParamBuffer.contents()).bindMemory(to: Float.self, capacity: 1)
        //Grille de groupe de threads
        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 }
        //Activer le rendu multi-passes
        let technique = SCNTechnique(dictionary: dict)
        scnView.technique = technique
    }
    
    func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
        //Incrément pour chaque dessin
        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 {
            //Afficher l'objet sur le plan détecté
            node.addChildNode(self.rootNode)
        }
    }
}

Recommended Posts

Camouflage optique avec ARKit + SceneKit + Metal ①
Camouflage optique avec ARKit + SceneKit + Metal ②
Navigation Web avec ARKit + SceneKit + Metal
Effondrement du sol avec ARKit + SceneKit
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