Como desenvolvemos o aplicativo AR para revisar lugares históricos



Recentemente, combinamos tecnologias antigas com tecnologias modernas, o que veio como resultado.

Realidade Aumentada


Aplicativos de realidade aumentada como guias da cidade são um tema bem conhecido e implementado por muitos desenvolvedores. Essa direção do uso da AR foi uma das primeiras, pois permite usar todas as possibilidades óbvias da realidade aumentada: mostrar aos usuários informações sobre edifícios, fornecer informações sobre o trabalho da instituição e conhecer os pontos turísticos. No último hackathon, realizado dentro da empresa, foram apresentados vários projetos usando realidade aumentada, e tivemos a ideia de criar um aplicativo de AR que mostrasse como era um marco ou local histórico no passado. Para fazer isso, combine tecnologias modernas de realidade aumentada com fotografias antigas. Por exemplo, de frente para a Catedral de Santo Isaac, você pode apontar uma câmera para ele e ver seu primeiro edifício de madeira, que foi demolido em 1715.

A mecânica do trabalho é a seguinte: o aplicativo exibe os locais históricos e pontos turísticos da cidade no mapa, exibe informações breves sobre eles, com a ajuda de notificações, notifica o usuário de que ele não está longe de um ponto interessante. Quando uma pessoa se aproxima de um monumento histórico a uma distância de 40 metros, o modo AR fica disponível. Ao mesmo tempo, a câmera é aberta e informações breves sobre os objetos são exibidas diretamente no espaço ao redor do usuário. O último tem a capacidade de interagir com objetos virtuais: tocando no cartão de um local histórico, você pode continuar visualizando o álbum com imagens.

Parece que o aplicativo é muito simples, mas mesmo aqui houve algumas armadilhas. Não vou aborrecê-lo com uma história sobre a implementação de coisas triviais, como baixar dados de um servidor ou exibir pontos em um mapa; irei diretamente às funções que causaram os problemas.

Problema 1. Pontos Flutuantes


Portanto, a primeira coisa a fazer era colocar os marcadores no espaço, de acordo com a localização real dos locais históricos em relação à localização atual e à direção do olhar do usuário.

Para começar, decidimos usar a biblioteca já preparada para iOS: ARKit-CoreLocation . O projeto encontra-se no GitHub em domínio público, contém, além do código das principais classes, exemplos de integração e nos permite concluir a tarefa em que estamos interessados ​​em algumas horas. Só é necessário alimentar a biblioteca com as coordenadas dos pontos e a imagem usada como marcador.

Não surpreendentemente, essa facilidade teve que ser paga. Os marcadores flutuavam constantemente no espaço: eles subiam ao teto ou eram puxados para algum lugar sob os pés. Nem todo usuário concordaria em manter o objeto AR em foco por vários minutos para se familiarizar com as informações que lhe interessam.

Como se viu, muitos enfrentaram esse bug da biblioteca, mas uma solução ainda não foi encontrada. Infelizmente, o código no GitHub não foi atualizado por mais de seis meses, então tive que ignorá-lo.

Tentamos usar a altitude em vez da altitude fixa em coordenadas, que o LocationManager retornou para a posição atual do usuário. No entanto, isso não eliminou completamente o problema. Os dados provenientes do Location Manager começaram a pular com uma extensão de até 60 metros, assim que o dispositivo foi torcido na mão. Como resultado, a imagem era instável, o que, é claro, não nos serviu novamente.

Como resultado, foi decidido abandonar a biblioteca ARKit-CoreLocation e colocar pontos no espaço por conta própria. O artigo ARKit e CoreLocation, escrito por Christopher Web-Orenstein, ajudou muito nisso. Eu tive que gastar um pouco mais de tempo e atualizar alguns aspectos matemáticos em minha memória, mas o resultado valeu a pena: os objetos RA estavam finalmente em seus lugares. Depois disso, resta apenas dispersá-los ao longo do eixo Y para facilitar a leitura dos rótulos e pontos e estabelecer uma correspondência entre a distância da posição atual até o ponto e a coordenada Z do objeto AR, para que as informações sobre os locais históricos mais próximos fiquem em primeiro plano.

Foi necessário calcular a nova posição do SCNNode no espaço, com foco nas coordenadas:

let place = PlaceNode() let locationTransform = MatrixHelper.transformMatrix(for: matrix_identity_float4x4, originLocation: curUserLocation, location: nodeLocation, yPosition: pin.yPos, shouldScaleByDistance: false) let nodeAnchor = ARAnchor(transform: locationTransform) scene.session.add(anchor: nodeAnchor) scene.scene.rootNode.addChildNode(place) 

As seguintes funções foram adicionadas à classe MatrixHelper:

 class MatrixHelper { static func transformMatrix(for matrix: simd_float4x4, originLocation: CLLocation, location: CLLocation, yPosition: Float) -> simd_float4x4 { let distanceToPoint = Float(location.distance(from: originLocation)) let distanceToNode = (10 + distanceToPoint/1000.0) let bearing = GLKMathDegreesToRadians(Float(originLocation.coordinate.direction(to: location.coordinate))) let position = vector_float4(0.0, yPosition, -distanceToNode, 0.0) let translationMatrix = MatrixHelper.translationMatrix(with: matrix_identity_float4x4, for: position) let rotationMatrix = MatrixHelper.rotateAroundY(with: matrix_identity_float4x4, for: bearing) let transformMatrix = simd_mul(rotationMatrix, translationMatrix) return simd_mul(matrix, transformMatrix) } static func translationMatrix(with matrix: matrix_float4x4, for translation : vector_float4) -> matrix_float4x4 { var matrix = matrix matrix.columns.3 = translation return matrix } static func rotateAroundY(with matrix: matrix_float4x4, for degrees: Float) -> matrix_float4x4 { var matrix : matrix_float4x4 = matrix matrix.columns.0.x = cos(degrees) matrix.columns.0.z = -sin(degrees) matrix.columns.2.x = sin(degrees) matrix.columns.2.z = cos(degrees) return matrix.inverse } } 

Para calcular o azimute, adicionou a extensão CLLocationCoordinate2D

 extension CLLocationCoordinate2D { func calculateBearing(to coordinate: CLLocationCoordinate2D) -> Double { let a = sin(coordinate.longitude.toRadians() - longitude.toRadians()) * cos(coordinate.latitude.toRadians()) let b = cos(latitude.toRadians()) * sin(coordinate.latitude.toRadians()) - sin(latitude.toRadians()) * cos(coordinate.latitude.toRadians()) * cos(coordinate.longitude.toRadians() - longitude.toRadians()) return atan2(a, b) } func direction(to coordinate: CLLocationCoordinate2D) -> CLLocationDirection { return self.calculateBearing(to: coordinate).toDegrees() } } 

Problema 2. Objetos RA em Excesso


O próximo problema que encontramos foi uma enorme quantidade de objetos AR. Há muitos lugares históricos e pontos turísticos em nossa cidade, portanto, dados com informações mescladas e rastejaram um para o outro. Seria muito difícil para o usuário distinguir parte das inscrições, e isso poderia causar uma impressão repulsiva. Após a conferência, decidimos limitar o número de objetos AR exibidos simultaneamente, deixando apenas pontos em um raio de 500 metros a partir da localização atual.

No entanto, em algumas áreas, a concentração de pontos ainda era muito alta. Portanto, para aumentar a visibilidade, eles decidiram usar o cluster. Na tela do mapa, esse recurso está disponível por padrão devido à lógica incorporada no MapKit, mas no modo AR era necessário implementá-lo manualmente.

O agrupamento foi baseado na distância da posição atual ao alvo. Assim, se o ponto cair na zona com um raio igual à metade da distância entre o usuário e a atração anterior da lista, ele simplesmente se ocultará e fará parte do cluster. À medida que o usuário se aproximava, a distância diminuía e o raio da zona do cluster diminuía de acordo; portanto, as vistas localizadas nas proximidades não se fundiam em clusters. Para distinguir visualmente os clusters de pontos únicos, decidimos alterar a cor do marcador e exibir o número de objetos em AR em vez do nome do local.

imagem

Para garantir a interatividade dos objetos AR, um UITapGestureRecognizer foi pendurado no ARSCNView e no manipulador, usando o método hitTest, verificou em qual dos objetos SCNNode o usuário clicou. Se fosse uma fotografia de atrações próximas, o aplicativo abriria o álbum correspondente no modo de tela cheia.

Problema 3. Radar


Durante a implementação do aplicativo, foi necessário mostrar os pontos em um pequeno radar. Em teoria, não deveria haver mal-entendidos, porque já calculamos o azimute e a distância até o ponto e conseguimos convertê-los em coordenadas 3D. Restava apenas colocar os pontos no espaço bidimensional na tela.



Para não reinventar a roda, eles se voltaram para a biblioteca Radar , cujo código-fonte aberto foi publicado no GitHub. As visualizações vívidas e as configurações flexíveis do exemplo eram encorajadoras, mas, na realidade, os pontos foram alterados em relação à verdadeira localização no espaço. Depois de passar algum tempo tentando corrigir as fórmulas, passamos à opção menos bonita, porém mais confiável, descrita no iPhone Augmented Reality Toolkit :

 func place(dot: Dot) { var y: CGFloat = 0.0 var x: CGFloat = 0.0 if degree < 0 { degree += 360 } let bearing = dot.bearing.toRadians() let radius: CGFloat = 60.0 // radius of the radar view if (bearing > 0 && bearing < .pi / 2) { //the 1 quadrant of the radar x = radius + CGFloat(cosf(Float((.pi / 2) - bearing)) * Float(dot.distance)) y = radius - CGFloat(sinf(Float((.pi / 2) - bearing)) * Float(dot.distance)) } else if (bearing > .pi / 2.0 && bearing < .pi) { //the 2 quadrant of the radar x = radius + CGFloat(cosf(Float(bearing - (.pi / 2))) * Float(dot.distance)) y = radius + CGFloat(sinf(Float(bearing - (.pi / 2))) * Float(dot.distance)) } else if (bearing > .pi && bearing < (3 * .pi / 2)) { //the 3 quadrant of the radar x = radius - CGFloat(cosf(Float((3 * .pi / 2) - bearing)) * Float(dot.distance)) y = radius + CGFloat(sinf(Float((3 * .pi / 2) - bearing)) * Float(dot.distance)) } else if (bearing > (3 * .pi / 2.0) && bearing < (2 * .pi)) { //the 4 quadrant of the radar x = radius - CGFloat(cosf(Float(bearing - (3 * .pi / 2))) * Float(dot.distance)) y = radius - CGFloat(sinf(Float(bearing - (3 * .pi / 2))) * Float(dot.distance)) } else if (bearing == 0) { x = radius y = radius - CGFloat(dot.distance) } else if (bearing == .pi / 2) { x = radius + CGFloat(dot.distance) y = radius } else if (bearing == .pi) { x = radius y = radius + CGFloat(dot.distance) } else if (bearing == 3 * .pi / 2) { x = radius - CGFloat(dot.distance) y = radius } else { x = radius y = radius - CGFloat(dot.distance) } let newPosition = CGPoint(x: x, y: y) dot.layer.position = newPosition 

Backend


Resta resolver o problema de armazenar pontos e fotos. Para esses fins, foi decidido usar o Contentful e, na implementação atual do projeto, ele nos convinha completamente.


No momento do desenvolvimento do aplicativo móvel, todos os participantes do projeto estavam envolvidos em projetos comerciais, e com conteúdo permitido por várias horas:

  • desenvolvedor móvel - back-end conveniente
  • gerenciador de conteúdo - uma área administrativa conveniente para o preenchimento de dados

A implementação semelhante do back-end foi usada inicialmente pelas equipes que participaram do hackathon (mencionadas no início do artigo), que mais uma vez prova que coisas como hackathons permitem que você evite resolver seus problemas prementes nos projetos, possibilite recriar e tentar algo novinho em folha.

Conclusão



Foi muito interessante desenvolver um aplicativo de AR , no processo, tentamos várias bibliotecas prontas, mas também tivemos que lembrar da matemática e escrever muitas coisas nós mesmos.

Simples, à primeira vista, o projeto exigia muitas horas de trabalho para implementar e refinar os algoritmos, apesar de termos usado o SDK padrão da Apple.

Recentemente, postamos o aplicativo na AppStore . É assim que parece no trabalho.


Até o momento, em nosso banco de dados há pontos apenas para o Taganrog, no entanto, todos podem participar da expansão da “área de cobertura”.

Source: https://habr.com/ru/post/pt421067/


All Articles