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.
① 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.
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
.
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.
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.
・ 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