[SWIFT] Effondrement du sol avec ARKit + SceneKit

J'ai essayé la combinaison de & Portal pour texturer l'objet virtuel qui met en place la capture de la caméra. Effondrons le sol en tant que sujet.

Image complète demo.pngdemo.gif Si l'image capturée de la caméra peut être collée sur l'objet virtuel de cette manière, il est possible de rendre le sol en marche, d'ouvrir le mur et de l'utiliser comme une porte, et ainsi de suite.

Comment faire s'effondrer le sol

(1) Préparez un modèle 3D en forme de boîte (ci-après «grille») à encastrer dans le sol. Une petite boîte (appelée ci-après «cellule») est étalée en 10x10 sur la partie supérieure de la grille. Assurez-vous que les cellules se réduisent et se rassemblent au bas de la grille. (2) Avec la reconnaissance de plan ARKit activée, effectuez un test de frappe au centre de l'écran au moment où vous appuyez sur l'écran. (3) Obtenez la transformée du plan obtenue dans le test de succès et placez-la dans la grille. La grille correspond à la position et à la posture de frappe. ④ Capturez l'image de la caméra ⑤ Convertissez les sommets (coordonnées mondiales des quatre coins) de chaque surface de cellule en coordonnées d'écran (coordonnées normalisées de l'appareil) ⑥ Puisque ④ est utilisé comme texture de cellule, les coordonnées normalisées du périphérique de ⑤ sont converties en coordonnées uv. ⑦ Définissez ⑥ pour les coordonnées de texture de chaque cellule ⑧ Réglez le corps physique du SCN du nœud de la cellule centrale vers l'extérieur de sorte que la cellule 10x10 s'effondre à partir du centre.

Les points sont décrits ci-dessous.

(1) Préparez un modèle 3D en forme de boîte (ci-après «grille») à encastrer dans le sol.

<Image de la disposition des nœuds> IMG_0006.png La figure ci-dessus est une image de la disposition des nœuds. En fait, cette boîte est spéciale, car l'arrière-plan (image de la caméra) peut être vu tel quel lorsque la boîte est vue de l'extérieur, et l'intérieur de la boîte peut être vu correctement lorsque la boîte est vue de l'intérieur (une telle expression est "porte partout"). Cela semble être appelé un «portail»). Pour savoir comment le faire, utilisez la méthode de cet article «Ordre de rendu SCNNode de SceneKit n'importe où expression de type porte».

"⑤ Convertissez les sommets (coordonnées mondiales des quatre coins) de chaque surface de cellule en coordonnées d'écran (coordonnées de périphérique normalisées)" + "Puisque ⑥④ est utilisé comme texture de cellule, convertissez les coordonnées de périphérique normalisées de ⑤ en coordonnées UV"

Comment utiliser l'image de la caméra capturée comme texture. IMG_0010.png

Pour la texture de chaque cellule, définissez l'image capturée telle qu'elle est, comme illustré dans la figure ci-dessus. La même image est utilisée pour toutes les cellules et les coordonnées UV (points rouges) des quatre coins de la surface de la cellule sont différentes pour chaque cellule. La méthode pour obtenir les coordonnées des sommets des cellules sur l'image est la même que pour le rendu normal d'un modèle 3D. Coordonnées mondiales de chaque sommet de la cellule → conversion de vue → conversion de projection.

//Matrice de transformation de modèle (nœud de surface de cellule dans le système de coordonnées mondial)
let modelTransform = cellFaceNode.simdWorldTransform
//Afficher la matrice de transformation. Matrice inverse du point de vue de la caméra (déplacez tous les sommets de coordonnées du monde vers la position avec la caméra comme origine)
let viewTransform = cameraNode.simdTransform.inverse
//Matrice de transformation de projection
let projectionTransform = simd_float4x4(camera.projectionTransform(withViewportSize: self.scnView.bounds.size))
//Matrice MVP
let mvpTransform = projectionTransform * viewTransform * modelTransform
 
//Conversion des coordonnées de projection
var position = matrix_multiply(mvpTransform, SIMD4<Float>(vertex.x, vertex.y, vertex.z, 1.0))

Mais! , Ceci ne peut pas être utilisé tel quel. J'ai trouvé l'article suivant (aidé) lorsque je cherchais quelque chose qui ne correspondait pas.

Le résultat de la conversion des coordonnées de projection doit être converti en ** «coordonnées de périphérique normalisées» ** de -1,0 à 0,0. Divisez x, y par la composante w du résultat de la conversion de projection de la vue du modèle comme suit. ..

// -1.0~1.Normalisé à une valeur de 0. Divisez par w pour obtenir les "coordonnées normalisées de l'appareil"
position = position / position.w

Maintenant que nous avons un système de coordonnées de -1,0 à 0,0, nous pouvons l'utiliser comme coordonnées de texture en procédant comme suit.

//Convertir en coordonnées UV
let texcordX = CGFloat(position.x + 1.0) / 2.0
let texcordY = CGFloat(-position.y + 1.0) / 2.0
texcoords.append(CGPoint(x: texcordX, y: texcordY))

⑦ Définissez ⑥ pour les coordonnées de texture de chaque cellule

Puisqu'il est nécessaire de définir arbitrairement les coordonnées uv de chaque sommet du nœud de cellule, écrivez-le en Métal. .. .. J'ai pensé, mais je pourrais facilement le réaliser uniquement avec SceneKit en me référant à cet article Comment créer une géométrie personnalisée avec SceneKit + bonus ..

//Générer une géométrie de surface de cellule
let texcoordSource = SCNGeometrySource(textureCoordinates: texcoords)
let cellFaceGeometry = SCNGeometry(sources: [vertex, texcoordSource], elements: [element])
let cellFaceMaterial = SCNMaterial()
cellFaceMaterial.diffuse.contents = captureImage
cellFaceGeometry.materials = [cellFaceMaterial]
cellFaceNode.geometry = cellFaceGeometry    //Remplacer la géométrie

En créant une géométrie personnalisée avec SCNGeometry, vous pouvez spécifier librement les coordonnées de la texture. Étant donné que le nœud de cellule a déjà été généré lorsque l'application est lancée cette fois, la géométrie du nœud de cellule est acquise lorsque l'écran est touché, et seules les coordonnées de texture sont remplacées pour créer une géométrie personnalisée.

Tips

① Option de débogage Cette fois, il était difficile de comprendre l'existence et le délimiteur des nœuds car il y avait des nœuds transparents et les cellules s'étalaient. Dans un tel cas, si vous définissez l'option de débogage suivante, une ligne blanche s'affiche à la limite du nœud, ce qui est facile à comprendre et pratique.

//Option de débogage pouvant être spécifiée pour SCNSceneRenderer
scnView.debugOptions = [.showBoundingBoxes]

② Si le nœud est petit, le jugement physique ne fonctionnera pas. Au début, j'ai commencé à réduire la taille de la grille, 10 cm, mais lorsque la cellule est tombée, elle n'a pas été jugée entrer en collision avec le nœud inférieur, et je n'ai pas pu éviter le phénomène de glisser par le bas. SceneKit a une propriété appelée continueCollisionDetectionThreshold dans SCNPhysicsBody comme contre-mesure contre un tel glissement, et si vous spécifiez la taille du nœud en conflit, il semble qu'il calculera de manière à ne pas glisser, mais cela n'a pas bien fonctionné. Documentation d'Apple dit " La détection de collision continue a un coût de performance et ne fonctionne que pour les formes de physique sphérique, mais fournit des résultats plus précis. En guise de contre-mesure, le fond et les côtés de la grille sont transformés en forme de boîte pour la rendre plus épaisse (bien qu'elle glisse toujours si la taille de la grille est réduite).

Code source complet

ViewController.siwft


import ARKit
import SceneKit

class ViewController: UIViewController {

    @IBOutlet weak var scnView: ARSCNView!
    private let device = MTLCreateSystemDefaultDevice()!
    private let gridSize = 10                               //Nombre de divisions de grille
    private let gridLength: Float = 0.8                     //Taille de la grille[m]
    private let wallThickness: Float = 0.1                  //Épaisseur du côté et du bas de la grille
    private lazy var cellSize = gridLength / Float(gridSize) //Taille de cellule
    private let gridRootNode = SCNNode()                    //Corps carré enterré dans le sol
    private let gridCellParentNode = SCNNode()              //Route des cellules alignées au sol
    //Coordonnées des sommets de la surface de la cellule
    private lazy var vertices = [
        SCNVector3(-cellSize/2, 0.0, -cellSize/2), //Laisser derriere
        SCNVector3( cellSize/2, 0.0, -cellSize/2), //Arrière droit
        SCNVector3(-cellSize/2, 0.0, cellSize/2), //Avant gauche
        SCNVector3( cellSize/2, 0.0, cellSize/2), //Avant droite
    ]
    //Index de sommet de surface cellulaire
    private let indices: [Int32] = [
        0, 2, 1,
        1, 2, 3
    ]
    private var time = 0                //Compteur de dessins
    private var isTouching = false      //Détection tactile
    
    override func viewDidLoad() {
        super.viewDidLoad()
        //Mettre en place une boîte en forme de grille à enterrer dans le sol
        setupGridBox()
        //Session AR démarrée
        self.scnView.delegate = self
        let configuration = ARWorldTrackingConfiguration()
        configuration.planeDetection = [.horizontal]
        self.scnView.session.run(configuration, options: [.removeExistingAnchors, .resetTracking])
    }
}
    
extension ViewController: ARSCNViewDelegate {
    //
    //Ancre ajoutée
    //
    func renderer(_: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
        guard let planeAnchor = anchor as? ARPlaneAnchor else { return }

        //Ajout d'un nœud de géométrie plane
        guard let geometory = ARSCNPlaneGeometry(device: self.device) else { return }
        geometory.update(from: planeAnchor.geometry)
        let planeNode = SCNNode(geometry: geometory)
        planeNode.isHidden = true
        DispatchQueue.main.async {
            node.addChildNode(planeNode)
        }
    }
    //
    //Ancre mise à jour
    //
    func renderer(_: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
        guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
        
        DispatchQueue.main.async {
            for childNode in node.childNodes {
                //Mettre à jour la géométrie du plan
                guard let planeGeometry = childNode.geometry as? ARSCNPlaneGeometry else { continue }
                planeGeometry.update(from: planeAnchor.geometry)
                break
            }
        }
    }
    //
    //Appelé image par image
    //
    func renderer(_ renderer: SCNSceneRenderer, updateAtTime _: TimeInterval) {
        
        if isTouching {
            //L'écran a été touché
            isTouching = false
            DispatchQueue.main.async {
                //Ignorer si la grille est affichée
                guard self.gridRootNode.isHidden else { return }
                //Capturez l'image de la caméra et appliquez-la comme texture sur la surface de la cellule ici
                self.setupCellFaceTexture()
                //Affichage de grille
                self.gridRootNode.isHidden = false
            }
        }
        
        DispatchQueue.main.async {
            //Ne rien faire si la grille est masquée
            guard !self.gridRootNode.isHidden else { return }
            //Effondrement du sol
            self.hourakuAnimation()
        }
    }
}

extension ViewController {
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let _ = touches.first else { return }
        isTouching = true
    }

    private func setupCellFaceTexture() {
        //Hit test au centre de l'écran
        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
        }
        //Capturez l'image de la caméra. Instantané facile car il ne capture pas en continu()utilisation
        let captureImage = self.scnView.snapshot()
        //Acquisition de caméra
        guard let cameraNode = self.scnView.pointOfView,
              let camera = cameraNode.camera else { return }
        //Publier la transformation de l'emplacement de hit à la transformation de la grille
        self.gridRootNode.simdTransform = existingPlaneUsingGeometryResult.worldTransform
        for cellNode in self.gridCellParentNode.childNodes {
            guard let cellFaceNode = cellNode.childNodes.first(where: {$0.name == "face"}) else { continue }
            guard let vertex = cellFaceNode.geometry?.sources.first(where: {$0.semantic == .vertex}) else { continue }
            guard let element = cellFaceNode.geometry?.elements.first else { continue }
            
            //Matrice de transformation de modèle (nœud de surface de cellule dans le système de coordonnées mondial)
            let modelTransform = cellFaceNode.simdWorldTransform
            //Afficher la matrice de transformation. Matrice inverse du point de vue de la caméra (déplacez tous les sommets de coordonnées du monde vers la position avec la caméra comme origine)
            let viewTransform = cameraNode.simdTransform.inverse
            //Matrice de transformation de projection
            let projectionTransform = simd_float4x4(camera.projectionTransform(withViewportSize: self.scnView.bounds.size))
            //Matrice MVP
            let mvpTransform = projectionTransform * viewTransform * modelTransform
            //Convertissez chaque coordonnée de sommet d'une cellule en coordonnées d'écran et convertissez-la en coordonnées UV
            var texcoords: [CGPoint] = []
            for vertex in self.vertices {
                //Conversion des coordonnées de projection
                var position = matrix_multiply(mvpTransform, SIMD4<Float>(vertex.x, vertex.y, vertex.z, 1.0))
                // -1.0~1.Normalisé à une valeur de 0. Divisez par w pour obtenir les "coordonnées normalisées de l'appareil"
                position = position / position.w
                //Convertir en coordonnées UV
                let texcordX = CGFloat(position.x + 1.0) / 2.0
                let texcordY = CGFloat(-position.y + 1.0) / 2.0
                texcoords.append(CGPoint(x: texcordX, y: texcordY))
            }
            //Générer une géométrie de surface de cellule
            let texcoordSource = SCNGeometrySource(textureCoordinates: texcoords)
            let cellFaceGeometry = SCNGeometry(sources: [vertex, texcoordSource], elements: [element])
            let cellFaceMaterial = SCNMaterial()
            cellFaceMaterial.diffuse.contents = captureImage
            cellFaceGeometry.materials = [cellFaceMaterial]
            cellFaceNode.geometry = cellFaceGeometry    //Remplacer la géométrie
        }
    }
    
    private func hourakuAnimation() {
        //Arrêtez l'effondrement dans un certain temps
        guard self.time < 150 else { return }
        self.time += 1
        
        let time = Float(self.time)
        let gridSize = Float(self.gridSize)
        //Réduire pour étendre le cercle à partir du centre
        let x = sin(Float.pi * 2 * time/30.0) * time / 150
        let y = cos(Float.pi * 2 * time/30.0) * time / 150
        let ygrid = Int((y + 1.0) / 2 * gridSize * gridSize) / self.gridSize * self.gridSize
        let xgrid = Int((x + 1.0) / 2 * gridSize) + ygrid
        guard 0 <= xgrid, xgrid < self.gridCellParentNode.childNodes.count else { return }
        let node = self.gridCellParentNode.childNodes[xgrid]
        //Ignorer les nœuds pour lesquels le corps physique a déjà été défini
        guard node.physicsBody == nil else { return }
        //Faites correspondre la taille du jugement physique à la taille de la cellule
        let bodyLength = CGFloat(self.cellSize) * 1.0
        let box = SCNBox(width: bodyLength, height: bodyLength, length: bodyLength, chamferRadius: 0.0)
        let boxShape = SCNPhysicsShape(geometry: box, options: nil)
        let boxBody = SCNPhysicsBody(type: .dynamic, shape: boxShape)
        boxBody.continuousCollisionDetectionThreshold = 0.001 //Je l'ai réglé, mais je n'ai pas l'impression que ça marche
        // TODO:L'origine de la cellule est au centre, mais l'origine de la géométrie définie dans PhysicsBody est au centre, de sorte que les demi-coordonnées de la cellule sont désactivées.
        //Vous pouvez soit personnaliser la géométrie PhysicsBody, soit ramener l'origine de la cellule au centre, ce qui est fastidieux. Corrigez-le si vous en avez l'occasion.
        node.physicsBody = boxBody
    }
    
    private func setupGridBox() {
        gridRootNode.isHidden = true
        //
        //Générer des nœuds de cellule
        //
        //Assurez-vous de spécifier la position de chaque nœud. J'ai rencontré un phénomène selon lequel la coordonnée Y ne répond pas aux attentes à moins qu'elle ne soit initialisée même si c'est la coordonnée centrale.
        gridCellParentNode.simdPosition = SIMD3<Float>(x: 0.0, y: 0.0, z: 0.0)
        gridRootNode.addChildNode(gridCellParentNode)
        gridRootNode.simdPosition = SIMD3<Float>(x: 0.0, y: 0.0, z: 0.0)
        self.scnView.scene.rootNode.addChildNode(gridRootNode)
        //Créer un nœud de cellule de gridSize x gridSize
        let cellFaceGeometry = makeCellFaceGeometry()
        let cellBoxGeometry = makeCellBoxGeometry()
        let cellLeftBackPos = -(gridLength / 2) + cellSize / 2
        for y in 0 ..< gridSize {
            for x in 0 ..< gridSize {
                //Chaque nœud de cellule
                let cellNode = SCNNode()
                cellNode.simdPosition = SIMD3<Float>(x: cellLeftBackPos + (cellSize * Float(x)), y: 0, z: cellLeftBackPos + (cellSize * Float(y)))
                gridCellParentNode.addChildNode(cellNode)
                //Génération de nœuds de plan de surface cellulaire
                let cellFaceNode = SCNNode(geometry: cellFaceGeometry)
                cellFaceNode.name = "face"
                //Déterminez les coordonnées pour que chaque cellule soit étalée
                cellFaceNode.simdPosition = SIMD3<Float>(x: 0.0, y: 0.0, z: 0.0)
                cellNode.addChildNode(cellFaceNode)
                //Générer un nœud de cellule rectangulaire
                let cellBoxNode = SCNNode(geometry: cellBoxGeometry)
                cellBoxNode.simdPosition = SIMD3<Float>(x: 0.0, y: -cellSize/2*1.001, z: 0.0)
                cellNode.addChildNode(cellBoxNode)
            }
        }
        //
        //Nœud côté grille
        //
        let sideOuterBox = makeGridSideOuterGeometry()
        let sideInnerPlane = makeGridSideInnerGeometry()
        //Génération de nœuds côté grille
        for i in 0..<4 {
            let x = sin(Float.pi / 2 * Float(i)) * gridLength / 2.0
            let z = cos(Float.pi / 2 * Float(i)) * gridLength / 2.0
            //Côté (extérieur)
            let outerNode = SCNNode(geometry: sideOuterBox)
            let nodePos = ((gridLength + wallThickness) / 2.0) / (gridLength / 2.0) * 1.001
            outerNode.simdPosition = SIMD3<Float>(x: x * nodePos, y: -gridLength/2.0, z: z * nodePos)
            outerNode.simdRotation = SIMD4<Float>(x: 0.0, y: 1.0, z: 0.0, w: -Float.pi / 2 * Float(i))
            //Faire un mur physique sur le côté
            outerNode.physicsBody = SCNPhysicsBody.static()
            //La paroi extérieure est (presque) transparente et l'ordre de rendu-Mis à 1 et dessiner avant les autres nœuds (écrire d'abord dans le tampon Z)
            //Cela ne dessinera pas les nœuds qui devraient être dessinés derrière le (presque) transparent, résultant en un arrière-plan visible.
            outerNode.renderingOrder = -1
            gridRootNode.addChildNode(outerNode)
            
            //Côté (intérieur)
            let innerNode = SCNNode(geometry: sideInnerPlane)
            innerNode.simdPosition = SIMD3<Float>(x: x, y: -gridLength/2.0, z: z)
            innerNode.simdRotation = SIMD4<Float>(x: 0.0, y: 1.0, z: 0.0, w: -Float.pi / 2 * Float(i))
            gridRootNode.addChildNode(innerNode)
        }
        //
        //Nœud inférieur de la grille
        //
        let bottomBox = makeGridButtomGeometry()
        let bottomNode = SCNNode(geometry: bottomBox)
        bottomNode.simdPosition = SIMD3<Float>(x: 0.0, y: -gridLength+Float(wallThickness), z: 0.0)
        bottomNode.simdRotation = SIMD4<Float>(x: 1.0, y: 0.0, z: 0.0, w: -Float.pi / 2)
        //Faites un mur physique en bas
        bottomNode.physicsBody = SCNPhysicsBody.static()
        gridRootNode.addChildNode(bottomNode)
    }
    
    private func makeCellFaceGeometry() -> SCNGeometry {
        //Géométrie de la surface cellulaire. Collez l'image capturée à l'écran sur cette géométrie en tant que texture
        let cellFaceVertices = SCNGeometrySource(vertices: vertices)
        let cellFaceIndices = SCNGeometryElement(indices: indices, primitiveType: .triangles)
        let cellFaceGeometry = SCNGeometry(sources: [cellFaceVertices], elements: [cellFaceIndices])
        let cellFaceMaterial = SCNMaterial()
        cellFaceMaterial.diffuse.contents = UIColor.clear
        cellFaceGeometry.materials = [cellFaceMaterial]
        return cellFaceGeometry
    }
    
    private func makeCellBoxGeometry() -> SCNGeometry {
        //La petite boîte faisant partie de la cellule.
        let cellBox = SCNBox(width: CGFloat(cellSize), height: CGFloat(cellSize), length: CGFloat(cellSize), chamferRadius: 0.0)
        let cellBoxMaterial = SCNMaterial()
        cellBoxMaterial.diffuse.contents = UIColor.darkGray
        cellBox.materials = [cellBoxMaterial]
        return cellBox
    }
    
    private func makeGridSideOuterGeometry() -> SCNGeometry {
        //Géométrie côté grille (extérieur). Faites une paroi épaisse pour qu'elle ne pénètre pas sur le côté lorsque la cellule tombe.
        let sideOuterBox = SCNBox(width: CGFloat(gridLength) * 1.001, height: CGFloat(gridLength), length: CGFloat(wallThickness), chamferRadius: 0)
        let sideOuterMaterial = SCNMaterial()
        sideOuterMaterial.transparency = 0.001
        sideOuterMaterial.diffuse.contents = UIColor.white
        sideOuterMaterial.isDoubleSided = true
        sideOuterBox.materials = [sideOuterMaterial]
        return sideOuterBox
    }
    
    private func makeGridSideInnerGeometry() -> SCNGeometry {
        //Géométrie côté grille (intérieur)
        let sideInnerPlane = SCNPlane(width: CGFloat(gridLength), height: CGFloat(gridLength))
        let sideInnerMaterial = SCNMaterial()
        sideInnerMaterial.diffuse.contents = UIColor.gray
        sideInnerMaterial.isDoubleSided = true
        sideInnerPlane.materials = [sideInnerMaterial]
        return sideInnerPlane
    }
    
    private func makeGridButtomGeometry() -> SCNGeometry {
        //La géométrie du bas de la grille. Faites une paroi épaisse pour qu'elle ne pénètre pas sur le côté lorsque la cellule tombe.
        let bottomBox = SCNBox(width: CGFloat(gridLength), height: CGFloat(gridLength), length: CGFloat(wallThickness), chamferRadius: 0)
        let bottomMaterial = SCNMaterial()
        bottomMaterial.diffuse.contents = UIColor.black
        bottomBox.materials = [bottomMaterial]
        return bottomBox
    }
}

Recommended Posts

Effondrement du sol avec ARKit + SceneKit
Navigation Web avec ARKit + SceneKit + Metal
Camouflage optique avec ARKit + SceneKit + Metal ②
Affichage du curseur lors du placement d'objets avec ARKit + SceneKit
Comment transformer des figurines ARKit et SceneKit avec Metal Shader
Reproduction de "Vous êtes devant le roi Laputa" avec ARKit + SceneKit + Metal