Tutorial iOS ARKit: disegnare nell'aria a dita nude

Pubblicato: 2022-03-11

Di recente, Apple ha annunciato la sua nuova libreria di realtà aumentata (AR) denominata ARKit. Per molti, sembrava solo un'altra buona libreria AR, non un distruttore tecnologico di cui preoccuparsi. Tuttavia, se dai un'occhiata ai progressi dell'AR negli ultimi due anni, non dovresti essere troppo veloce nel trarre tali conclusioni.

Illustrazione del tutorial ARKit: Interazione con oggetti virtuali in un'app iOS ARKit

In questo post creeremo un divertente progetto di esempio ARKit utilizzando iOS ARKit. L'utente posizionerà le dita su un tavolo come se avesse in mano una penna, toccherà la miniatura e inizierà a disegnare. Una volta terminato, l'utente potrà trasformare il proprio disegno in un oggetto 3D, come mostrato nell'animazione sottostante. Il codice sorgente completo per il nostro esempio iOS ARKit è disponibile su GitHub.

Dimostrazione dell'utilizzo della nostra app di realtà aumentata di esempio ARKit per iOS

Perché dovremmo preoccuparci di iOS ARKit Now?

Ogni sviluppatore esperto è probabilmente consapevole del fatto che l'AR è un vecchio concetto. Possiamo definire il primo serio sviluppo di AR nel momento in cui gli sviluppatori hanno avuto accesso ai singoli frame dalle webcam. Le app a quel tempo di solito venivano utilizzate per trasformare il tuo viso. Tuttavia, l'umanità non ha impiegato molto a rendersi conto che trasformare i volti in coniglietti non era uno dei loro bisogni più imminenti, e presto il clamore svanì!

Credo che l'AR abbia sempre perso due importanti progressi tecnologici per renderlo utile: usabilità e immersione. Se hai tracciato altri clamore di AR, lo noterai. Ad esempio, il clamore di AR è decollato di nuovo quando gli sviluppatori hanno avuto accesso ai singoli frame da fotocamere mobili. Oltre al forte ritorno dei grandi trasformatori di coniglietti, abbiamo visto un'ondata di app che rilasciano oggetti 3D su codici QR stampati. Ma non sono mai decollati come concept. Non erano realtà aumentata, ma codici QR aumentati.

Poi Google ci ha sorpreso con un pezzo di fantascienza, Google Glass. Passarono due anni e quando ci si aspettava che questo fantastico prodotto prendesse vita, era già morto! Molti critici hanno analizzato le ragioni del fallimento di Google Glass, dando la colpa a qualsiasi cosa, dagli aspetti sociali all'approccio noioso di Google al lancio del prodotto. Tuttavia, ci preoccupiamo in questo articolo per un motivo particolare: l'immersione nell'ambiente. Sebbene Google Glass abbia risolto il problema dell'usabilità, non era altro che un'immagine 2D tracciata nell'aria.

Titani della tecnologia come Microsoft, Facebook e Apple hanno imparato a memoria questa dura lezione. Nel giugno 2017, Apple ha annunciato la sua bellissima libreria iOS ARKit, facendo dell'immersione la sua massima priorità. Tenere in mano un telefono è ancora un grosso ostacolo all'esperienza utente, ma la lezione di Google Glass ci ha insegnato che l'hardware non è il problema.

Credo che ci stiamo dirigendo verso un nuovo picco di clamore AR molto presto e, con questo nuovo importante perno, potrebbe eventualmente trovare il suo mercato interno, consentendo a un maggiore sviluppo di app AR di diventare mainstream. Ciò significa anche che ogni società di sviluppo di app di realtà aumentata là fuori sarà in grado di attingere all'ecosistema e alla base di utenti di Apple.

Ma basta storia, sporchiamoci le mani con il codice e vediamo la realtà aumentata di Apple in azione!

Caratteristiche di immersione di ARKit

ARKit fornisce due caratteristiche principali; il primo è la posizione della telecamera nello spazio 3D e il secondo è il rilevamento del piano orizzontale. Per ottenere il primo, ARKit presuppone che il tuo telefono sia una fotocamera che si muove nello spazio 3D reale in modo tale che la caduta di un oggetto virtuale 3D in qualsiasi punto sarà ancorata a quel punto nello spazio 3D reale. E per quest'ultimo, ARKit rileva i piani orizzontali come i tavoli in modo da poter posizionare oggetti su di esso.

Quindi, come fa ARKit a raggiungere questo obiettivo? Questo viene fatto attraverso una tecnica chiamata Visual Inertial Odometry (VIO). Non preoccuparti, proprio come gli imprenditori trovano il loro piacere nel numero di risatine che ridacchia quando scopri la fonte dietro il nome della loro startup, i ricercatori trovano il loro nel numero di graffi alla testa che fai cercando di decifrare qualsiasi termine si inventano quando dare un nome alle loro invenzioni, quindi consentiamo loro di divertirsi e andare avanti.

VIO è una tecnica mediante la quale i fotogrammi della telecamera vengono fusi con sensori di movimento per tracciare la posizione del dispositivo nello spazio 3D. Il rilevamento del movimento dai fotogrammi della telecamera viene effettuato rilevando le caratteristiche, o, in altre parole, i punti marginali dell'immagine con un contrasto elevato, come il bordo tra un vaso blu e un tavolo bianco. Rilevando quanto questi punti si sono spostati l'uno rispetto all'altro da un fotogramma all'altro, è possibile stimare dove si trova il dispositivo nello spazio 3D. Ecco perché ARKit non funzionerà correttamente se posizionato di fronte a un muro bianco privo di caratteristiche o quando il dispositivo si muove molto velocemente con conseguente sfocatura delle immagini.

Guida introduttiva ad ARKit in iOS

Al momento della stesura di questo articolo, ARKit fa parte di iOS 11, che è ancora in versione beta. Quindi, per iniziare, devi scaricare iOS 11 Beta su iPhone 6s o versioni successive e il nuovo Xcode Beta. Possiamo avviare un nuovo progetto ARKit da Nuovo > Progetto > App di Realtà Aumentata . Tuttavia, ho trovato più conveniente iniziare questo tutorial di realtà aumentata con l'esempio ufficiale di Apple ARKit, che fornisce alcuni blocchi di codice essenziali ed è particolarmente utile per il rilevamento dei piani. Quindi, iniziamo con questo codice di esempio, spieghiamo prima i punti principali e poi lo modifichiamo per il nostro progetto.

Innanzitutto, dovremmo determinare quale motore utilizzeremo. ARKit può essere utilizzato con Sprite SceneKit o Metal. Nell'esempio di Apple ARKit, stiamo utilizzando iOS SceneKit, un motore 3D fornito da Apple. Successivamente, dobbiamo impostare una vista che renderà i nostri oggetti 3D. Questo viene fatto aggiungendo una vista di tipo ARSCNView .

ARSCNView è una sottoclasse della vista principale di SceneKit denominata SCNView , ma estende la vista con un paio di utili funzioni. Rende il feed video live dalla fotocamera del dispositivo come sfondo della scena, mentre abbina automaticamente lo spazio di SceneKit al mondo reale, supponendo che il dispositivo sia una fotocamera in movimento in questo mondo.

ARSCNView non esegue l'elaborazione AR da solo, ma richiede un oggetto sessione AR che gestisca la fotocamera del dispositivo e l'elaborazione del movimento. Quindi, per iniziare, dobbiamo assegnare una nuova sessione:

 self.session = ARSession() sceneView.session = session sceneView.delegate = self setupFocusSquare()

L'ultima riga sopra aggiunge un indicatore visivo che aiuta l'utente a descrivere visivamente lo stato del rilevamento del piano. Focus Square è fornito dal codice di esempio, non dalla libreria ARKit, ed è uno dei motivi principali per cui abbiamo iniziato con questo codice di esempio. Puoi trovare maggiori informazioni a riguardo nel file readme incluso nel codice di esempio. L'immagine seguente mostra un riquadro di messa a fuoco proiettato su un tavolo:

Focus quadrato proiettato su un tavolo utilizzando Apple ARKit

Il prossimo passo è avviare la sessione di ARKit. Ha senso riavviare la sessione ogni volta che viene visualizzata la vista perché non possiamo utilizzare le informazioni sulla sessione precedente se non stiamo più tracciando l'utente. Quindi, inizieremo la sessione in viewDidAppear:

 override func viewDidAppear(_ animated: Bool) { let configuration = ARWorldTrackingSessionConfiguration() configuration.planeDetection = .horizontal session.run(configuration, options: [.resetTracking, .removeExistingAnchors]) }

Nel codice sopra, iniziamo impostando la configurazione della sessione ARKit per rilevare i piani orizzontali. Al momento della stesura di questo articolo, Apple non fornisce opzioni diverse da questa. Ma a quanto pare, suggerisce di rilevare oggetti più complessi in futuro. Quindi, iniziamo a eseguire la sessione e ci assicuriamo di ripristinare il monitoraggio.

Infine, è necessario aggiornare il Focus Square ogni volta che cambia la posizione della fotocamera, ovvero l'orientamento o la posizione effettiva del dispositivo. Questo può essere fatto nella funzione di delegato del renderer di SCNView, che viene chiamata ogni volta che verrà renderizzato un nuovo frame del motore 3D:

 func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) { updateFocusSquare() }

A questo punto, se esegui l'app, dovresti vedere il riquadro di messa a fuoco sul flusso della telecamera alla ricerca di un piano orizzontale. Nella prossima sezione, spiegheremo come vengono rilevati i piani e come possiamo posizionare il quadrato di messa a fuoco di conseguenza.

Rilevamento di aerei in ARKit

ARKit può rilevare nuovi piani, aggiornare quelli esistenti o rimuoverli. Per gestire i piani in modo pratico, creeremo un nodo fittizio SceneKit che contiene le informazioni sulla posizione del piano e un riferimento al riquadro di messa a fuoco. I piani sono definiti nelle direzioni X e Z, dove Y è la normale della superficie, cioè, dovremmo sempre mantenere le posizioni dei nostri nodi di disegno all'interno dello stesso valore Y del piano se vogliamo farlo sembrare come se fosse stampato sul piano .

Il rilevamento dei piani avviene tramite le funzioni di callback fornite da ARKit. Ad esempio, la seguente funzione di callback viene chiamata ogni volta che viene rilevato un nuovo piano:

 var planes = [ARPlaneAnchor: Plane]() func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { if let planeAnchor = anchor as? ARPlaneAnchor { serialQueue.async { self.addPlane(node: node, anchor: planeAnchor) self.virtualObjectManager.checkIfObjectShouldMoveOntoPlane(anchor: planeAnchor, planeAnchorNode: node) } } } func addPlane(node: SCNNode, anchor: ARPlaneAnchor) { let plane = Plane(anchor) planes[anchor] = plane node.addChildNode(plane) } ... class Plane: SCNNode { var anchor: ARPlaneAnchor var focusSquare: FocusSquare? init(_ anchor: ARPlaneAnchor) { self.anchor = anchor super.init() } ... }

La funzione di callback ci fornisce due parametri, anchor e node . node è un normale nodo SceneKit posizionato nella posizione e nell'orientamento esatti del piano. Non ha geometria, quindi è invisibile. Lo usiamo per aggiungere il nostro nodo piano, che è anche invisibile, ma contiene informazioni sull'orientamento e la posizione del piano in anchor .

Quindi, come vengono salvate la posizione e l'orientamento in ARPlaneAnchor ? Posizione, orientamento e scala sono tutti codificati in una matrice 4x4. Se avrò la possibilità di scegliere un concetto di matematica da farti imparare, saranno senza dubbio le matrici. Ad ogni modo, possiamo aggirarlo descrivendo questa matrice 4x4 come segue: Un brillante array bidimensionale contenente numeri in virgola mobile 4x4. Moltiplicando questi numeri in un certo modo per un vertice 3D, v1, nel suo spazio locale, si ottiene un nuovo vertice 3D, v2, che rappresenta v1 nello spazio mondiale. Quindi, se v1 = (1, 0, 0) nel suo spazio locale, e vogliamo metterlo a x = 100 nello spazio mondiale, v2 sarà uguale a (101, 0, 0) rispetto allo spazio mondiale. Naturalmente, la matematica alla base di questo diventa più complessa quando aggiungiamo le rotazioni sugli assi, ma la buona notizia è che possiamo fare a meno di capirlo (consiglio vivamente di controllare la sezione pertinente di questo eccellente articolo per una spiegazione approfondita di questo concetto ).

checkIfObjectShouldMoveOntoPlane controlla se abbiamo già oggetti disegnati e controlla se l'asse y di tutti questi oggetti corrisponde a quello dei piani appena rilevati.

Ora, torniamo a updateFocusSquare() , descritto nella sezione precedente. Vogliamo mantenere la messa a fuoco quadrata al centro dello schermo, ma proiettata sul piano rilevato più vicino. Il codice seguente lo dimostra:

 func updateFocusSquare() { let worldPos = worldPositionFromScreenPosition(screenCenter, self.sceneView) self.focusSquare?.simdPosition = worldPos } func worldPositionFromScreenPosition(_ position: CGPoint, in sceneView: ARSCNView) -> float3? { let planeHitTestResults = sceneView.hitTest(position, types: .existingPlaneUsingExtent) if let result = planeHitTestResults.first { return result.worldTransform.translation } return nil }

sceneView.hitTest ricerca i piani del mondo reale corrispondenti a un punto 2D nella vista dello schermo proiettando questo punto 2D sul piano sottostante più vicino. result.worldTransform è una matrice 4x4 che contiene tutte le informazioni di trasformazione del piano rilevato, mentre result.worldTransform.translation è una pratica funzione che restituisce solo la posizione.

Ora abbiamo tutte le informazioni di cui abbiamo bisogno per rilasciare un oggetto 3D sulle superfici rilevate dato un punto 2D sullo schermo. Quindi, iniziamo a disegnare.

Disegno

Spieghiamo innanzitutto l'approccio del disegno di forme che segue il dito di un essere umano nella visione artificiale. Il disegno delle forme viene eseguito rilevando ogni nuova posizione per il dito in movimento, rilasciando un vertice in quella posizione e collegando ogni vertice con quello precedente. I vertici possono essere collegati da una linea semplice, o attraverso una curva di Bezier se abbiamo bisogno di un output regolare.

Per semplicità, seguiremo un approccio un po' ingenuo al disegno. Per ogni nuova posizione del dito, faremo cadere una casella molto piccola con angoli arrotondati e altezza quasi zero sul piano rilevato. Apparirà come se fosse un punto. Una volta che l'utente ha finito di disegnare e ha selezionato il pulsante 3D, cambieremo l'altezza di tutti gli oggetti lasciati cadere in base al movimento del dito dell'utente.

Il codice seguente mostra la classe PointNode che rappresenta un punto:

 let POINT_SIZE = CGFloat(0.003) let POINT_HEIGHT = CGFloat(0.00001) class PointNode: SCNNode { static var boxGeo: SCNBox? override init() { super.init() if PointNode.boxGeo == nil { PointNode.boxGeo = SCNBox(width: POINT_SIZE, height: POINT_HEIGHT, length: POINT_SIZE, chamferRadius: 0.001) // Setup the material of the point let material = PointNode.boxGeo!.firstMaterial material?.lightingModel = SCNMaterial.LightingModel.blinn material?.diffuse.contents = UIImage(named: "wood-diffuse.jpg") material?.normal.contents = UIImage(named: "wood-normal.png") material?.specular.contents = UIImage(named: "wood-specular.jpg") } let object = SCNNode(geometry: PointNode.boxGeo!) object.transform = SCNMatrix4MakeTranslation(0.0, Float(POINT_HEIGHT) / 2.0, 0.0) self.addChildNode(object) } . . . }

Noterai nel codice sopra che traduciamo la geometria lungo l'asse y di metà dell'altezza. La ragione di ciò è assicurarsi che la parte inferiore dell'oggetto sia sempre a y = 0 , in modo che appaia sopra il piano.

Successivamente, nella funzione di callback del renderer di SceneKit, disegneremo un indicatore che agisce come un punto di punta di una penna, utilizzando la stessa classe PointNode . Rilasceremo un punto in quella posizione se il disegno è abilitato o innalzeremo il disegno in una struttura 3D se è abilitata la modalità 3D:

 func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) { updateFocusSquare() // Setup a dot that represents the virtual pen's tippoint if (self.virtualPenTip == nil) { self.virtualPenTip = PointNode(color: UIColor.red) self.sceneView.scene.rootNode.addChildNode(self.virtualPenTip!) } // Draw if let screenCenterInWorld = worldPositionFromScreenPosition(self.screenCenter, self.sceneView) { // Update virtual pen position self.virtualPenTip?.isHidden = false self.virtualPenTip?.simdPosition = screenCenterInWorld // Draw new point if (self.inDrawMode && !self.virtualObjectManager.pointNodeExistAt(pos: screenCenterInWorld)){ let newPoint = PointNode() self.sceneView.scene.rootNode.addChildNode(newPoint) self.virtualObjectManager.loadVirtualObject(newPoint, to: screenCenterInWorld) } // Convert drawing to 3D if (self.in3DMode ) { if self.trackImageInitialOrigin != nil { DispatchQueue.main.async { let newH = 0.4 * (self.trackImageInitialOrigin!.y - screenCenterInWorld.y) / self.sceneView.frame.height self.virtualObjectManager.setNewHeight(newHeight: newH) } } else { self.trackImageInitialOrigin = screenCenterInWorld } } }

virtualObjectManager è una classe che gestisce i punti disegnati. In modalità 3D, stimiamo la differenza dall'ultima posizione e aumentiamo/diminuiamo l'altezza di tutti i punti con quel valore.

Finora stiamo disegnando sulla superficie rilevata supponendo che la penna virtuale sia al centro dello schermo. Ora la parte divertente: rilevare il dito dell'utente e usarlo al posto del centro dello schermo.

Rilevamento del polpastrello dell'utente

Una delle fantastiche librerie che Apple ha introdotto in iOS 11 è Vision Framework. Fornisce alcune tecniche di visione artificiale in un modo abbastanza pratico ed efficiente. In particolare, utilizzeremo la tecnica del tracciamento degli oggetti per il nostro tutorial di realtà aumentata. Il tracciamento degli oggetti funziona come segue: in primo luogo, gli forniamo un'immagine e le coordinate di un quadrato entro i limiti dell'immagine per l'oggetto che vogliamo tracciare. Successivamente chiamiamo alcune funzioni per inizializzare il tracciamento. Infine, inseriamo una nuova immagine in cui è cambiata la posizione di quell'oggetto e il risultato dell'analisi dell'operazione precedente. Detto questo, ci restituirà la nuova posizione dell'oggetto.

Useremo un piccolo trucco. Chiederemo all'utente di appoggiare la mano sul tavolo come se stesse tenendo una penna e di assicurarsi che la sua miniatura sia rivolta verso la fotocamera, dopodiché dovrebbe toccare la sua miniatura sullo schermo. Ci sono due punti che devono essere elaborati qui. Innanzitutto, la miniatura dovrebbe avere caratteristiche uniche sufficienti per essere tracciata tramite il contrasto tra la miniatura bianca, la pelle e il tavolo. Ciò significa che il pigmento della pelle più scuro si tradurrà in un tracciamento più affidabile. In secondo luogo, poiché l'utente sta appoggiando le mani sul tavolo e poiché stiamo già rilevando il tavolo come un piano, proiettare la posizione della miniatura dalla vista 2D all'ambiente 3D risulterà quasi nella posizione esatta del dito sul tavolo.

L'immagine seguente mostra i punti caratteristici che potrebbero essere rilevati dalla libreria Vision:

Punti funzionalità ARKit iOS rilevati dalla libreria Vision

Inizializzeremo il monitoraggio delle miniature in un gesto di tocco come segue:

 // MARK: Object tracking fileprivate var lastObservation: VNDetectedObjectObservation? var trackImageBoundingBox: CGRect? let trackImageSize = CGFloat(20) @objc private func tapAction(recognizer: UITapGestureRecognizer) { lastObservation = nil let tapLocation = recognizer.location(in: view) // Set up the rect in the image in view coordinate space that we will track let trackImageBoundingBoxOrigin = CGPoint(x: tapLocation.x - trackImageSize / 2, y: tapLocation.y - trackImageSize / 2) trackImageBoundingBox = CGRect(origin: trackImageBoundingBoxOrigin, size: CGSize(width: trackImageSize, height: trackImageSize)) let t = CGAffineTransform(scaleX: 1.0 / self.view.frame.size.width, y: 1.0 / self.view.frame.size.height) let normalizedTrackImageBoundingBox = trackImageBoundingBox!.applying(t) // Transfrom the rect from view space to image space guard let fromViewToCameraImageTransform = self.sceneView.session.currentFrame?.displayTransform(withViewportSize: self.sceneView.frame.size, orientation: UIInterfaceOrientation.portrait).inverted() else { return } var trackImageBoundingBoxInImage = normalizedTrackImageBoundingBox.applying(fromViewToCameraImageTransform) trackImageBoundingBoxInImage.origin.y = 1 - trackImageBoundingBoxInImage.origin.y // Image space uses bottom left as origin while view space uses top left lastObservation = VNDetectedObjectObservation(boundingBox: trackImageBoundingBoxInImage) }

La parte più complicata sopra è come convertire la posizione del tocco dallo spazio delle coordinate UIView allo spazio delle coordinate dell'immagine. ARKit ci fornisce la matrice displayTransform che converte lo spazio delle coordinate dell'immagine in uno spazio delle coordinate del viewport, ma non il contrario. Allora come possiamo fare l'inverso? Usando l'inverso della matrice. Ho davvero cercato di ridurre al minimo l'uso della matematica in questo post, ma a volte è inevitabile nel mondo 3D.

Successivamente, nel renderer, inseriremo una nuova immagine per tracciare la nuova posizione del dito:

 func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) { // Track the thumbnail guard let pixelBuffer = self.sceneView.session.currentFrame?.capturedImage, let observation = self.lastObservation else { return } let request = VNTrackObjectRequest(detectedObjectObservation: observation) { [unowned self] request, error in self.handle(request, error: error) } request.trackingLevel = .accurate do { try self.handler.perform([request], on: pixelBuffer) } catch { print(error) } . . . }

Una volta completato il rilevamento dell'oggetto, chiamerà una funzione di callback in cui aggiorneremo la posizione della miniatura. In genere è l'inverso del codice scritto nel riconoscitore di tap:

 fileprivate func handle(_ request: VNRequest, error: Error?) { DispatchQueue.main.async { guard let newObservation = request.results?.first as? VNDetectedObjectObservation else { return } self.lastObservation = newObservation var trackImageBoundingBoxInImage = newObservation.boundingBox // Transfrom the rect from image space to view space trackImageBoundingBoxInImage.origin.y = 1 - trackImageBoundingBoxInImage.origin.y guard let fromCameraImageToViewTransform = self.sceneView.session.currentFrame?.displayTransform(withViewportSize: self.sceneView.frame.size, orientation: UIInterfaceOrientation.portrait) else { return } let normalizedTrackImageBoundingBox = trackImageBoundingBoxInImage.applying(fromCameraImageToViewTransform) let t = CGAffineTransform(scaleX: self.view.frame.size.width, y: self.view.frame.size.height) let unnormalizedTrackImageBoundingBox = normalizedTrackImageBoundingBox.applying(t) self.trackImageBoundingBox = unnormalizedTrackImageBoundingBox // Get the projection if the location of the tracked image from image space to the nearest detected plane if let trackImageOrigin = self.trackImageBoundingBox?.origin { self.lastFingerWorldPos = self.virtualObjectManager.worldPositionFromScreenPosition(CGPoint(x: trackImageOrigin.x - 20.0, y: trackImageOrigin.y + 40.0), in: self.sceneView) } } }

Infine, useremo self.lastFingerWorldPos invece del centro schermo durante il disegno e il gioco è fatto.

ARKit e il futuro

In questo post, abbiamo dimostrato come l'AR potrebbe essere coinvolgente attraverso l'interazione con le dita dell'utente e le tabelle della vita reale. Con più progressi nella visione artificiale e aggiungendo hardware più compatibile con l'AR ai gadget (come le telecamere di profondità), possiamo accedere alla struttura 3D di un numero sempre maggiore di oggetti intorno a noi.

Sebbene non sia ancora stato rilasciato al pubblico, vale la pena ricordare come Microsoft sia molto seria nel vincere la gara AR attraverso il suo dispositivo Hololens, che combina hardware su misura per AR con tecniche avanzate di riconoscimento dell'ambiente 3D. Puoi aspettare di vedere chi vincerà questa gara, oppure puoi farne parte sviluppando ora vere e proprie app di realtà aumentata immersive! Ma per favore, fate un favore all'umanità e non cambiate oggetti vivi in ​​coniglietti.