[SWIFT] Reproduction de "Vous êtes devant le roi Laputa" avec ARKit + SceneKit + Metal

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 demo.pngdemo.gif 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.

Méthode de reproduction

① 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) ** renderpass.png Regardons-les individuellement ci-dessous.

① Animer les nœuds Musca et Theta de haut en bas

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. slice_plane_1.png → 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. slice_plane_2.png 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 scene_animation.png

② Créez des informations de profondeur pour créer une surface jaune pâle sur la limite du plafond

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.

  1. Obtenez la profondeur de la partie arrière du personnage → Seul le verso est dessiné en spécifiant cullMode = front (décrit plus loin). ** La section transversale du personnage sera plus grande que la profondeur obtenue ici. ** **
  2. Obtenez la profondeur de la partie avant du personnage → Seul le recto est dessiné en spécifiant cullMode = back (décrit plus loin). ** La section transversale du personnage sera inférieure à la profondeur obtenue ici. ** **
  3. Obtenir la profondeur de la surface limite (plan du plafond) ** Dans les profondeurs 1) et 2) ci-dessus, la section transversale du caractère est la plage de profondeur de cette surface limite **.

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».

③ Jugez la surface limite et la section transversale du caractère à partir des informations de ②, et ajoutez une couleur jaune clair à l'image de ①.

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.

Méthode essayée pour la reproduction

  1. 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.

  2. 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

Code source complet

· 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

Reproduction de "Vous êtes devant le roi Laputa" avec ARKit + SceneKit + Metal
Camouflage optique avec ARKit + SceneKit + Metal ①
Camouflage optique avec ARKit + SceneKit + Metal ②
Camouflage optique avec ARKit + SceneKit + Metal ①
Camouflage optique avec ARKit + SceneKit + Metal ②
Effondrement du sol avec ARKit + SceneKit
Comment transformer des figurines ARKit et SceneKit avec Metal Shader
Essayez de vous serrer la main avec ARKit + Metal
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
Tester l'API Web avec junit
Application Web construite avec docker (1)
Scraping Web facile avec Jsoup
Comment transformer des figurines ARKit et SceneKit avec Metal Shader
Effondrement du sol avec ARKit + SceneKit
La version d'Elasticsearch que vous utilisez est-elle compatible avec Java 11?