Je vois souvent des modèles 3D apparaître des murs et des sols en RA, alors j'ai lancé un défi. Le thème était «Laputa, le château dans le ciel». Une scène dans laquelle Musca et Theta apparaissent du plafond devant le général Mouro dans la salle d'observation de Laputa. (Je pense qu'une pièce avec un trou dans le sol est une "salle d'observation", mais elle suit l'expression sur le wiki)
(Mis à part le modèle étant un petit panda,) Image terminée Le problème avec la reproduction était la partie jaune clair de la frontière entre le personnage et le plafond (la technologie de Laputa peut ou non rendre la frontière jaune clair, mais ici elle est unifiée au jaune clair). La méthode de reproduction est expliquée ci-dessous.
① Animer les nœuds Musca et Theta de haut en bas (2) Créez des informations de profondeur (ci-après dénommées profondeur) pour créer une surface jaune pâle sur la limite du plafond. Faites les trois suivants. ・ Profondeur du plan limite du plafond -Profondeur lors du dessin d'un personnage avec cullMode = back -Profondeur lors du dessin avec cullMode = devant du personnage ③ Jugez la surface frontière et la section transversale du caractère à partir des informations de ② et ajoutez du jaune clair à l'image de ①.
** Chemin de rendu (Xcode Capture GPU Frame) ** Regardons-les individuellement ci-dessous.
Ceci est défini dans l'éditeur de scène de Xcode.
-Arranger les personnages (modèle de personnage emprunté à la démo de SceneKit de la WWDC 2017). Le caractère se bloque sous le nœud de coordination char_parent
.
-Placez le nœud de surface frontière slice_plane
dans la même ligne que char_parent
. Ce nœud d'interface ne s'anime pas.
→ Rendre la couleur presque transparente. → Diminuez la valeur de l'ordre de rendu pour dessiner avant le personnage afin que le personnage ne soit pas dessiné en arrière-plan. Le masque de bits de catégorie est défini ici. Utilisé plus tard pour faire la distinction entre l'interface et le caractère lors de la génération de profondeur. Définissez 4 pour la surface frontière et 2 pour le caractère.
→ Réglez l'animation
Les informations de profondeur sur la face avant (face avant) du personnage et les informations de profondeur sur la face arrière (face invisible) du personnage sont acquises, et le caractère réel est obtenu par la différence.
Les informations de profondeur pour chacun des trois ci-dessus sont générées par un rendu multi-passes par SCNTechnique. La définition du rendu multi-passes est la suivante.
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"
]
}
Regardons cela petit à petit.
"pass_scene" : {
"draw" : "DRAW_SCENE",
"outputs" : {
"color" : "color_scene"
}
},
C'est la définition du dessin de la scène entière. En spécifiant DRAW_SCENE
pour draw
, l'image de capture de la caméra + le caractère est dessiné. Le résultat du dessin est uniquement des informations de couleur et est stocké dans un tampon nommé color_scene
.
"pass_slice" : {
"draw" : "DRAW_NODE",
"includeCategoryMask" : 4,
"outputs" : {
"depth" : "depth_slice"
},
"depthStates" : {
"clear" : true,
"func" : "less"
}
},
Il s'agit d'un dessin de la surface limite du plafond. 4
est spécifié dans includeCategoryMask
, et il est défini pour dessiner uniquement le plan de délimitation. Aucune information de couleur n'est requise pour ce dessin, seule la profondeur est stockée dans un tampon nommé depth_slice
.
"pass_cullback" : {
"draw" : "DRAW_NODE",
"includeCategoryMask" : 2,
"cullMode" : "back",
"outputs" : {
"depth" : "depth_cullback"
},
"depthStates" : {
"clear" : true,
"func" : "less"
}
},
Il s'agit d'une définition permettant d'acquérir les informations de profondeur du personnage vu de face. «2» est spécifié dans «includeCategoryMask», et seul le caractère est défini pour être dessiné. Back
est spécifié pour cullMode
, et la partie visible est dessinée, et l'invisible (face arrière) n'est pas dessinée (la valeur par défaut est back
). Aucune information de couleur n'est requise pour ce dessin, seule la profondeur est stockée dans un tampon nommé depth_cullback
.
"pass_cullfront" : {
"draw" : "DRAW_NODE",
"includeCategoryMask" : 2,
"cullMode" : "front",
"outputs" : {
"depth" : "depth_cullfront"
},
"depthStates" : {
"clear" : true,
"func" : "less"
}
},
Il s'agit d'une définition pour obtenir les informations de profondeur au verso du personnage. Similaire à "pass_cullback", mais cullMode
spécifie front
.
"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"
}
}
C'est la définition qui affiche enfin la section transversale capture caméra + personnage + caractère.
Le résultat de sortie (informations de couleur, profondeur) de chaque chemin de dessin spécifié dans inputs
est combiné avec le shader de fragment mix_fragment
(décrit plus loin) spécifié dans metalFragmentShader
pour créer l'image finale. Il est dessiné à l'écran en spécifiant «COLOR» pour «color» des «sorties».
Ceci est fait avec le shader mix_fragment
mentionné ci-dessus.
Le contenu de traitement est tel que décrit dans le commentaire de la source, et il est déterminé s'il faut afficher du jaune clair dans les informations de profondeur et l'ajouter à la couleur de la scène entière.
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> depthSlice [[texture(1)]],
depth2d<float, access::sample> depthCullBack [[texture(2)]],
depth2d<float, access::sample> depthCullFront [[texture(3)]])
{
//Profondeur de l'interface de plafond
float ds = depthSlice.sample(s, vert.uv);
//Profondeur du polygone face à vous du point de vue
float db = depthCullBack.sample(s, vert.uv);
//Profondeur du polygone faisant face à l'opposé du point de vue
float df = depthCullFront.sample(s, vert.uv);
float4 sliceColor = float4(0.0, 0.0, 0.0, 0.0);
if (df < ds) {
//La surface de délimitation se trouve devant l'arrière du personnage
if (ds < db) {
//De plus, la surface de délimitation se trouve derrière la face avant du personnage
sliceColor = float4(0.5, 0.5, 0.0, 0.0); //Jaune clair
}
}
//Ajoutez une couleur de bordure à l'image de toute la scène, y compris l'image capturée par la caméra
float4 fragment_color = colorScene.sample(s, fract(vert.uv));
fragment_color += sliceColor; //Je pense que c'est un processus approximatif, mais je ne suis pas familier avec la gestion des couleurs, donc je vais le revoir si j'en ai l'occasion.
return half4(fragment_color);
}
C'est la fin de l'explication.
Je n'ai pas trouvé de moyen de colorer la géométrie et la section de contact de la géométrie par google. Cette fois, je pense que je peux le voir d'une manière ou d'une autre par essais et erreurs, mais comme cette méthode ne crée que deux informations de profondeur de l'avant et de l'arrière du personnage en plus de la profondeur de la surface limite, un autre personnage est derrière le personnage. Si tel est le cas, la profondeur du caractère derrière est écrasée par la profondeur du caractère devant, et il y a un problème que la surface frontière n'est pas dessinée. Je pense qu'il existe d'autres bons moyens, alors faites-le moi savoir si vous le savez. Voici le contenu que j'ai étudié et essayé au cours d'essais et d'erreurs.
Faites beaucoup de tests pour explorer la forme du personnage à l'interface
Trouvez la position de la surface du personnage en disposant 100 hitTestWithSegment (de: à: options:)
de SCNNode
côte à côte sur la surface de délimitation et testez-les à l'avant → arrière et arrière → avant du personnage. J'ai fait une coupe transversale.
→ La précision de hitTestWithSegment n'était pas au niveau attendu et le résultat était légèrement différent de la forme de la géométrie, il ne pouvait donc pas être utilisé. Surtout dans les petites zones telles que les oreilles et les pieds, la position du résultat du coup différait considérablement de l'apparence. Je ne pense pas que ce soit une utilisation complètement différente de l'objectif initial.
Créer une géométrie de surface limite en temps réel -Aplatir la géométrie du personnage à la limite et rendre cette partie jaune clair. → Par exemple, lorsque la géométrie et la surface limite sont en contact l'une avec l'autre comme un pied, il semble assez gênant d'aplatir la géométrie sur chacun du pied droit et du pied gauche. Je n'ai pas essayé. De plus, si la géométrie est low poly, elle semble cliquetante, il peut donc être nécessaire de diviser la géométrie par tessellation (?). -Créez une nouvelle géométrie plane au niveau de la partie qui touche la surface frontière avec la géométrie du personnage et placez-la sur la surface frontière. → Encore une fois, même si vous pouvez obtenir les sommets de la géométrie près de la surface frontière, il semble difficile de créer une géométrie plane fermée à partir de celle-ci (pouvez-vous faire de votre mieux avec des informations normales ??) → J'ai trouvé quelques méthodes en faisant le tour du "mesh slicing", mais cela m'a paru difficile et j'ai arrêté. ・ Algorithme ou logiciel pour trancher un maillage → UE4 semble être capable de faire Mesh Slice en temps réel. Cela ne semble pas être dans SceneKit. ・ Https://unrealengine.hatenablog.com/entry/2016/09/12/002115
· Rapide
ViewController.swift
class ViewController: UIViewController, ARSCNViewDelegate {
@IBOutlet weak var scnView: ARSCNView!
private let device = MTLCreateSystemDefaultDevice()!
private var charNode: SCNNode!
private var isTouching = false //Détection tactile
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/scene.scn"),
let charNode = scene.rootNode.childNode(withName: "char_node", recursively: true) else { return }
self.charNode = charNode
self.charNode.isHidden = true
//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])
}
//
//Appelé image par image
//
func renderer(_ renderer: SCNSceneRenderer, updateAtTime _: TimeInterval) {
if isTouching {
//L'écran a été touché
isTouching = false
DispatchQueue.main.async {
//Ignorer si affiché
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 {
//Il n'y a pas de surface plane au centre de l'écran, alors ne faites rien
return
}
//Placez les nœuds Musca et Theta au centre de l'écran
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 }
//Activer le rendu multi-passes
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
}
}
· Métal
#include <metal_stdlib>
using namespace metal;
#include <SceneKit/scn_metal>
// SceneKit ->Type de livraison Shader
//La définition est https://developer.apple.com/documentation/scenekit/Voir scnprogram
struct VertexInput {
float4 position [[attribute(SCNVertexSemanticPosition)]]; //Coordonnées Apex
};
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;
//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,
filter::nearest);
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> depthSlice [[texture(1)]],
depth2d<float, access::sample> depthCullBack [[texture(2)]],
depth2d<float, access::sample> depthCullFront [[texture(3)]])
{
//Profondeur de l'interface de plafond
float ds = depthSlice.sample(s, vert.uv);
//Profondeur du polygone face à vous du point de vue
float db = depthCullBack.sample(s, vert.uv);
//Profondeur du polygone faisant face à l'opposé du point de vue
float df = depthCullFront.sample(s, vert.uv);
float4 sliceColor = float4(0.0, 0.0, 0.0, 0.0);
if (df < ds) {
//La surface de délimitation se trouve devant l'arrière du personnage
if (ds < db) {
//De plus, la surface de délimitation se trouve derrière la face avant du personnage
sliceColor = float4(0.5, 0.5, 0.0, 0.0); //Jaune clair
}
}
//Ajoutez une couleur de bordure à l'image de toute la scène, y compris l'image capturée par la caméra
float4 fragment_color = colorScene.sample(s, fract(vert.uv));
fragment_color += sliceColor; //Je pense que c'est un processus approximatif, mais je ne suis pas familier avec la gestion des couleurs, donc je vais le revoir si j'en ai l'occasion.
return half4(fragment_color);
}
Recommended Posts