Tutorial iOS ARKit: Desenhando no ar com os dedos nus
Publicados: 2022-03-11Recentemente, a Apple anunciou sua nova biblioteca de realidade aumentada (AR) chamada ARKit. Para muitos, parecia apenas mais uma boa biblioteca de AR, não um disruptor tecnológico para se preocupar. No entanto, se você der uma olhada no progresso da RA nos últimos dois anos, não deve ser muito rápido para tirar essas conclusões.
Neste post, vamos criar um projeto divertido de exemplo ARKit usando iOS ARKit. O usuário colocará os dedos em uma mesa como se estivesse segurando uma caneta, tocará na miniatura e começará a desenhar. Uma vez finalizado, o usuário poderá transformar seu desenho em um objeto 3D, conforme mostra a animação abaixo. O código-fonte completo para nosso exemplo iOS ARKit está disponível no GitHub.
Por que devemos nos preocupar com o iOS ARKit agora?
Todo desenvolvedor experiente provavelmente está ciente do fato de que AR é um conceito antigo. Podemos definir o primeiro desenvolvimento sério de AR até o momento em que os desenvolvedores tiveram acesso a quadros individuais de webcams. Os aplicativos naquela época geralmente eram usados para transformar seu rosto. No entanto, a humanidade não demorou muito para perceber que transformar rostos em coelhos não era uma de suas necessidades mais iminentes, e logo o hype desapareceu!
Acredito que o AR sempre faltou dois avanços tecnológicos importantes para torná-lo útil: usabilidade e imersão. Se você rastreou outros hypes de AR, notará isso. Por exemplo, o hype de AR decolou novamente quando os desenvolvedores obtiveram acesso a quadros individuais de câmeras móveis. Ao lado do forte retorno dos grandes transformadores de coelhinhos, vimos uma onda de aplicativos que soltam objetos 3D em códigos QR impressos. Mas eles nunca decolaram como um conceito. Eles não eram realidade aumentada, mas sim códigos QR aumentados.
Então o Google nos surpreendeu com uma peça de ficção científica, o Google Glass. Dois anos se passaram e, quando se esperava que esse produto incrível ganhasse vida, ele já estava morto! Muitos críticos analisaram as razões do fracasso do Google Glass, culpando desde aspectos sociais até a abordagem maçante do Google no lançamento do produto. No entanto, nos importamos neste artigo por um motivo em particular - imersão no ambiente. Enquanto o Google Glass resolveu o problema de usabilidade, ainda não era nada mais do que uma imagem 2D plotada no ar.
Titãs da tecnologia como Microsoft, Facebook e Apple aprenderam essa dura lição de cor. Em junho de 2017, a Apple anunciou sua bela biblioteca iOS ARKit, tornando a imersão sua principal prioridade. Segurar um telefone ainda é um grande bloqueador da experiência do usuário, mas a lição do Google Glass nos ensinou que o hardware não é o problema.
Acredito que estamos caminhando para um novo pico de hype de AR muito em breve e, com esse novo pivô significativo, ele poderá encontrar seu mercado doméstico, permitindo que mais desenvolvimento de aplicativos de AR se torne mainstream. Isso também significa que todas as empresas de desenvolvimento de aplicativos de realidade aumentada poderão acessar o ecossistema e a base de usuários da Apple.
Mas chega de história, vamos sujar as mãos com código e ver a realidade aumentada da Apple em ação!
Recursos de imersão ARKit
O ARKit fornece dois recursos principais; a primeira é a localização da câmera no espaço 3D e a segunda é a detecção do plano horizontal. Para alcançar o primeiro, o ARKit assume que seu telefone é uma câmera que se move no espaço 3D real, de modo que soltar algum objeto virtual 3D em qualquer ponto será ancorado nesse ponto no espaço 3D real. E para este último, o ARKit detecta planos horizontais como mesas para que você possa colocar objetos nele.
Então, como o ARKit consegue isso? Isso é feito através de uma técnica chamada Odometria Inercial Visual (VIO). Não se preocupe, assim como os empreendedores encontram prazer no número de risadinhas que você ri quando descobre a fonte por trás do nome da startup, os pesquisadores encontram o prazer deles no número de arranhões na cabeça que você faz tentando decifrar qualquer termo que eles inventam quando nomear suas invenções - então vamos permitir que eles se divirtam e siga em frente.
VIO é uma técnica pela qual os quadros da câmera são fundidos com sensores de movimento para rastrear a localização do dispositivo no espaço 3D. O rastreamento do movimento dos quadros da câmera é feito pela detecção de recursos, ou seja, pontos de borda na imagem com alto contraste - como a borda entre um vaso azul e uma mesa branca. Ao detectar o quanto esses pontos se moveram um em relação ao outro de um quadro para outro, pode-se estimar onde o dispositivo está no espaço 3D. É por isso que o ARKit não funcionará corretamente quando colocado de frente para uma parede branca sem recursos ou quando o dispositivo se mover muito rápido, resultando em imagens borradas.
Introdução ao ARKit no iOS
No momento da redação deste artigo, o ARKit faz parte do iOS 11, que ainda está em versão beta. Portanto, para começar, você precisa baixar o iOS 11 Beta no iPhone 6s ou superior e o novo Xcode Beta. Podemos iniciar um novo projeto ARKit em New > Project > Augmented Reality App . No entanto, achei mais conveniente iniciar este tutorial de realidade aumentada com o exemplo oficial do Apple ARKit, que fornece alguns blocos de código essenciais e é especialmente útil para detecção de aviões. Então, vamos começar com este código de exemplo, explicar os pontos principais nele primeiro e depois modificá-lo para nosso projeto.
Primeiro, devemos determinar qual mecanismo vamos usar. ARKit pode ser usado com Sprite SceneKit ou Metal. No exemplo do Apple ARKit, estamos usando o iOS SceneKit, um mecanismo 3D fornecido pela Apple. Em seguida, precisamos configurar uma visualização que renderizará nossos objetos 3D. Isso é feito adicionando uma visualização do tipo ARSCNView
.
ARSCNView
é uma subclasse da visão principal do SceneKit chamada SCNView
, mas estende a visão com alguns recursos úteis. Ele renderiza o feed de vídeo ao vivo da câmera do dispositivo como o plano de fundo da cena, enquanto combina automaticamente o espaço do SceneKit com o mundo real, supondo que o dispositivo seja uma câmera em movimento neste mundo.
ARSCNView
não faz o processamento de AR por conta própria, mas requer um objeto de sessão de AR que gerencie a câmera do dispositivo e o processamento de movimento. Então, para começar, precisamos atribuir uma nova sessão:
self.session = ARSession() sceneView.session = session sceneView.delegate = self setupFocusSquare()
A última linha acima adiciona um indicador visual que auxilia o usuário visualmente na descrição do status da detecção do avião. O Focus Square é fornecido pelo código de exemplo, não pela biblioteca ARKit, e é uma das principais razões pelas quais começamos com este código de exemplo. Você pode encontrar mais sobre isso no arquivo leia-me incluído no código de exemplo. A imagem a seguir mostra um quadrado de foco projetado em uma mesa:
O próximo passo é iniciar a sessão ARKit. Faz sentido reiniciar a sessão toda vez que a visualização aparecer porque não podemos usar as informações da sessão anterior se não estivermos mais rastreando o usuário. Então, vamos iniciar a sessão em viewDidAppear:
override func viewDidAppear(_ animated: Bool) { let configuration = ARWorldTrackingSessionConfiguration() configuration.planeDetection = .horizontal session.run(configuration, options: [.resetTracking, .removeExistingAnchors]) }
No código acima, começamos definindo a configuração da sessão ARKit para detectar planos horizontais. Ao escrever este artigo, a Apple não fornece outras opções além dessas. Mas, aparentemente, sugere a detecção de objetos mais complexos no futuro. Em seguida, começamos a executar a sessão e nos certificamos de redefinir o rastreamento.
Finalmente, precisamos atualizar o Focus Square sempre que a posição da câmera, ou seja, a orientação ou posição real do dispositivo, muda. Isso pode ser feito na função de delegado do renderizador do SCNView, que é chamada toda vez que um novo quadro do mecanismo 3D for renderizado:
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) { updateFocusSquare() }
A essa altura, se você executar o aplicativo, deverá ver o quadrado de foco sobre o fluxo da câmera procurando um plano horizontal. Na próxima seção, explicaremos como os aviões são detectados e como podemos posicionar o quadrado de foco de acordo.
Detectando aviões no ARKit
O ARKit pode detectar novos aviões, atualizar os existentes ou removê-los. Para lidar com os planos de maneira prática, criaremos algum nó do SceneKit fictício que contém as informações da posição do plano e uma referência ao quadrado de foco. Os planos são definidos na direção X e Z, onde Y é a normal da superfície, ou seja, devemos sempre manter nossas posições de nós de desenho dentro do mesmo valor Y do plano se quisermos fazer com que pareça impresso no plano .
A detecção de aviões é feita através de funções de callback fornecidas pelo ARKit. Por exemplo, a seguinte função de retorno de chamada é chamada sempre que um novo avião é detectado:
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() } ... }
A função de retorno de chamada nos fornece dois parâmetros, anchor
e node
. node
é um nó SceneKit normal colocado na posição e orientação exatas do plano. Não tem geometria, por isso é invisível. Nós o usamos para adicionar nosso próprio nó de plano, que também é invisível, mas contém informações sobre a orientação do plano e a posição na anchor
.
Então, como a posição e a orientação são salvas no ARPlaneAnchor
? Posição, orientação e escala são todos codificados em uma matriz 4x4. Se eu tiver a chance de escolher um conceito matemático para você aprender, sem dúvida será matrizes. De qualquer forma, podemos contornar isso descrevendo essa matriz 4x4 da seguinte forma: Uma matriz bidimensional brilhante contendo números de ponto flutuante 4x4. Ao multiplicar esses números de uma certa maneira por um vértice 3D, v1, em seu espaço local, resulta em um novo vértice 3D, v2, que representa v1 no espaço mundial. Então, se v1 = (1, 0, 0) em seu espaço local, e queremos colocá-lo em x = 100 no espaço do mundo, v2 será igual a (101, 0, 0) em relação ao espaço do mundo. Claro, a matemática por trás disso se torna mais complexa quando adicionamos rotações sobre os eixos, mas a boa notícia é que podemos fazer sem entender (recomendo verificar a seção relevante deste excelente artigo para uma explicação detalhada desse conceito ).
checkIfObjectShouldMoveOntoPlane
verifica se já temos objetos desenhados e verifica se o eixo y de todos esses objetos corresponde ao dos planos recém-detectados.

Agora, de volta ao updateFocusSquare()
, descrito na seção anterior. Queremos manter o quadrado de foco no centro da tela, mas projetado no plano detectado mais próximo. O código abaixo demonstra isso:
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
procura por planos do mundo real correspondentes a um ponto 2D na tela, projetando este ponto 2D para o plano inferior mais próximo. result.worldTransform
é uma matriz 4x4 que contém todas as informações de transformação do plano detectado, enquanto result.worldTransform.translation
é uma função útil que retorna apenas a posição.
Agora, temos todas as informações necessárias para soltar um objeto 3D em superfícies detectadas, dado um ponto 2D na tela. Então, vamos começar a desenhar.
Desenho
Vamos primeiro explicar a abordagem de desenhar formas que seguem o dedo de um humano na visão computacional. O desenho de formas é feito detectando cada novo local para o dedo em movimento, soltando um vértice nesse local e conectando cada vértice com o anterior. Os vértices podem ser conectados por uma linha simples ou por uma curva de Bezier se precisarmos de uma saída suave.
Para simplificar, seguiremos uma abordagem um pouco ingênua para desenhar. Para cada nova localização do dedo, deixaremos cair uma caixa muito pequena com cantos arredondados e altura quase zero no plano detectado. Aparecerá como se fosse um ponto. Assim que o usuário terminar de desenhar e selecionar o botão 3D, alteraremos a altura de todos os objetos soltos com base no movimento do dedo do usuário.
O código a seguir mostra a classe PointNode
que representa um ponto:
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) } . . . }
Você notará no código acima que traduzimos a geometria ao longo do eixo y pela metade da altura. A razão para isso é garantir que a parte inferior do objeto esteja sempre em y = 0 , de modo que apareça acima do plano.
Em seguida, na função de callback do renderizador do SceneKit, vamos desenhar algum indicador que age como um ponto de ponta da caneta, usando a mesma classe PointNode
. Vamos soltar um ponto nesse local se o desenho estiver ativado ou elevar o desenho em uma estrutura 3D se o modo 3D estiver ativado:
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
é uma classe que gerencia pontos desenhados. No modo 3D, estimamos a diferença da última posição e aumentamos/diminuímos a altura de todos os pontos com esse valor.
Até agora, estamos desenhando na superfície detectada assumindo que a caneta virtual está no centro da tela. Agora, a parte divertida - detectar o dedo do usuário e usá-lo em vez do centro da tela.
Detectando a ponta do dedo do usuário
Uma das bibliotecas legais que a Apple introduziu no iOS 11 é o Vision Framework. Ele fornece algumas técnicas de visão computacional de uma maneira bastante prática e eficiente. Em particular, vamos usar a técnica de rastreamento de objetos para nosso tutorial de realidade aumentada. O rastreamento de objetos funciona da seguinte forma: primeiro, fornecemos uma imagem e as coordenadas de um quadrado dentro dos limites da imagem para o objeto que queremos rastrear. Depois disso, chamamos alguma função para inicializar o rastreamento. Por fim, alimentamos uma nova imagem na qual a posição desse objeto mudou e o resultado da análise da operação anterior. Dado isso, ele retornará para nós a nova localização do objeto.
Vamos usar um pequeno truque. Pediremos ao usuário que descanse a mão na mesa como se estivesse segurando uma caneta e certifique-se de que sua miniatura esteja voltada para a câmera, após o que deve tocar em sua miniatura na tela. Há dois pontos que precisam ser elaborados aqui. Primeiro, a miniatura deve ter recursos exclusivos suficientes para serem rastreados por meio do contraste entre a miniatura branca, a capa e a mesa. Isso significa que o pigmento da pele mais escuro resultará em um rastreamento mais confiável. Em segundo lugar, como o usuário está apoiando as mãos sobre a mesa, e já que já estamos detectando a mesa como um plano, projetar a localização da miniatura da visualização 2D para o ambiente 3D resultará na localização quase exata do dedo no tabela.
A imagem a seguir mostra pontos de recursos que podem ser detectados pela biblioteca Vision:
Inicializaremos o rastreamento de miniaturas em um gesto de toque da seguinte maneira:
// 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) }
A parte mais complicada acima é como converter o local de toque do espaço de coordenadas UIView para o espaço de coordenadas da imagem. O ARKit nos fornece a matriz displayTransform
que converte do espaço de coordenadas da imagem para um espaço de coordenadas da viewport, mas não o contrário. Então, como podemos fazer o inverso? Usando o inverso da matriz. Eu realmente tentei minimizar o uso de matemática neste post, mas às vezes é inevitável no mundo 3D.
Em seguida, no renderizador, vamos alimentar uma nova imagem para rastrear a nova localização do dedo:
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) } . . . }
Quando o rastreamento de objetos estiver concluído, ele chamará uma função de retorno de chamada na qual atualizaremos o local da miniatura. Normalmente é o inverso do código escrito no reconhecedor de toque:
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) } } }
Finalmente, usaremos self.lastFingerWorldPos
em vez do centro da tela ao desenhar, e pronto.
ARKit e o futuro
Neste post, demonstramos como o AR pode ser imersivo por meio da interação com os dedos do usuário e tabelas da vida real. Com mais avanços na visão computacional e adicionando mais hardware compatível com AR aos gadgets (como câmeras de profundidade), podemos obter acesso à estrutura 3D de mais e mais objetos ao nosso redor.
Embora ainda não tenha sido lançado para as massas, vale a pena mencionar como a Microsoft está muito séria para vencer a corrida de AR através de seu dispositivo Hololens, que combina hardware adaptado para AR com técnicas avançadas de reconhecimento de ambiente 3D. Você pode esperar para ver quem vai ganhar esta corrida, ou você pode fazer parte dela desenvolvendo aplicativos de realidade aumentada imersivos reais agora! Mas, por favor, faça um favor à humanidade e não transforme objetos vivos em coelhos.