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 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.
(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.
<Image de la disposition des nœuds> 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».
Comment utiliser l'image de la caméra capturée comme texture.
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))
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).
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