Cómo desarrollamos la aplicación AR para revisar lugares históricos



Recientemente, combinamos tecnologías antiguas con tecnologías modernas, lo que surgió de la lectura bajo el corte.

Realidad aumentada


Las aplicaciones de realidad aumentada como guías de la ciudad son un tema muy conocido e implementado por muchos desarrolladores. Esta dirección de usar AR fue una de las primeras, ya que le permite utilizar todas las posibilidades obvias de la realidad aumentada: mostrar a los usuarios información sobre edificios, dar información sobre el trabajo de la institución y familiarizarse con las vistas. En el último hackathon, que se llevó a cabo dentro de la empresa, se presentaron varios proyectos que utilizan la realidad aumentada, y se nos ocurrió la idea de crear una aplicación AR que mostrara cómo era un lugar histórico o histórico en el pasado. Para hacer esto, combine las tecnologías modernas de realidad aumentada con fotografías antiguas. Por ejemplo, frente a la Catedral de San Isaac, puedes apuntarle con la cámara de un teléfono inteligente y ver su primer edificio de madera, que fue demolido en 1715.

La mecánica del trabajo es la siguiente: la aplicación muestra los lugares históricos y lugares de interés de la ciudad en el mapa, muestra información breve sobre ellos y, con la ayuda de notificaciones, notifica al usuario que no está lejos de un punto interesante. Cuando una persona se acerca a un monumento histórico a una distancia de 40 metros, el modo AR está disponible. Al mismo tiempo, la cámara se abre y se muestra información breve sobre los objetos directamente en el espacio que rodea al usuario. Este último tiene la capacidad de interactuar con objetos virtuales: al tocar la tarjeta de un lugar histórico, puede proceder a ver el álbum con imágenes.

Parece que la aplicación es muy simple, pero incluso aquí hubo algunas dificultades. No te aburriré con una historia sobre la implementación de cosas triviales como descargar datos de un servidor o mostrar puntos en un mapa, iré directamente a las funciones que causaron los problemas.

Problema 1. Puntos flotantes


Por lo tanto, lo primero que debía hacer era colocar puntos marcadores en el espacio de acuerdo con la ubicación real de los lugares históricos en relación con la ubicación actual y la dirección de la mirada del usuario.

Para comenzar, decidimos usar la biblioteca ya preparada para iOS: ARKit-CoreLocation . El proyecto se encuentra en GitHub en el dominio público, contiene, además del código de las clases principales, ejemplos de integración y nos permite completar la tarea que nos interesa en un par de horas. Solo es necesario alimentar a la biblioteca con las coordenadas de los puntos y la imagen utilizada como marcador.

No es sorprendente que esta facilidad tuviera que pagarse. Los puntos marcadores flotaban constantemente en el espacio: subían al techo o eran arrastrados a algún lugar bajo los pies. No todos los usuarios estarían de acuerdo en captar el objeto AR en foco durante varios minutos para familiarizarse con la información que le interesa.

Al final resultó que, muchos se enfrentaron a este error de la biblioteca, pero aún no se ha encontrado una solución. El código en GitHub, desafortunadamente, no se ha actualizado durante más de seis meses, por lo que tuve que omitirlo.

Intentamos usar la altitud en lugar de la altitud fija en las coordenadas, que el LocationManager devolvió para la posición actual del usuario. Sin embargo, esto no eliminó completamente el problema. Los datos provenientes de Location Manager comenzaron a saltar con una extensión de hasta 60 metros, tan pronto como el dispositivo se retorció en la mano. Como resultado, la imagen era inestable, lo que, por supuesto, no volvió a adaptarse.

Como resultado, se decidió abandonar la biblioteca ARKit-CoreLocation y colocar los puntos en el espacio por su cuenta. El artículo ARKit y CoreLocation, escrito por Christopher Web-Orenstein, ayudó mucho en esto. Tuve que pasar un poco más de tiempo y actualizar algunos aspectos matemáticos en mi memoria, pero el resultado valió la pena: los objetos AR finalmente estaban en su lugar. Después de eso, solo queda dispersarlos a lo largo del eje Y para que las etiquetas y los puntos sean más fáciles de leer, y establecer una correspondencia entre la distancia desde la posición actual hasta el punto y la coordenada Z del objeto AR para que la información sobre los lugares históricos más cercanos esté en primer plano.

Era necesario calcular la nueva posición de SCNNode en el espacio, centrándose en las 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) 

Las siguientes funciones se agregaron a la clase 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 el acimut se agregó la extensión 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. Exceso de objetos AR


El siguiente problema que encontramos fue una gran cantidad de objetos AR. Hay muchos lugares históricos y lugares de interés en nuestra ciudad, por lo tanto, los dados con la información se fusionaron y se arrastraron uno encima del otro. Sería muy difícil para el usuario distinguir parte de las inscripciones, y esto podría causar una impresión repulsiva. Después de consultar, decidimos limitar el número de objetos AR mostrados simultáneamente, dejando solo puntos dentro de un radio de 500 metros desde la ubicación actual.

Sin embargo, en algunas áreas, la concentración de puntos todavía era demasiado alta. Por lo tanto, para aumentar la visibilidad, decidieron usar el agrupamiento. En la pantalla del mapa, esta función está disponible de forma predeterminada debido a la lógica incorporada en MapKit, pero en el modo AR fue necesario implementarla manualmente.

La agrupación se basó en la distancia desde la posición actual hasta el objetivo. Por lo tanto, si el punto cayó en la zona con un radio igual a la mitad de la distancia entre el usuario y la atracción anterior de la lista, simplemente se ocultó y formó parte del grupo. A medida que el usuario se acercaba, la distancia disminuía y el radio de la zona del grupo disminuía en consecuencia, por lo que las vistas cercanas no se fusionaron en grupos. Para distinguir visualmente los grupos de puntos individuales, decidimos cambiar el color del marcador y mostrar el número de objetos en AR en lugar del nombre del lugar.

imagen

Para garantizar la interactividad de los objetos AR, se colgó un UITapGestureRecognizer en el ARSCNView y en el controlador utilizando el método hitTest comprobamos en qué objetos SCNNode hizo clic el usuario. Si resultó ser una fotografía de atracciones cercanas, la aplicación abrió el álbum correspondiente en modo de pantalla completa.

Problema 3. Radar


Durante la implementación de la aplicación, fue necesario mostrar los puntos en un pequeño radar. En teoría, no debería haber malentendidos, porque ya calculamos el acimut y la distancia al punto, incluso logramos convertirlos en coordenadas 3D. Solo quedaba colocar los puntos en un espacio bidimensional en la pantalla.



Para no reinventar la rueda, recurrieron a la biblioteca Radar , cuyo código fuente abierto se publicó en GitHub. Las vistas previas vívidas y la configuración flexible del ejemplo fueron alentadoras, pero en realidad los puntos se cambiaron en relación con la ubicación real en el espacio. Después de pasar un tiempo tratando de corregir las fórmulas, recurrimos a la opción menos bella pero más confiable descrita en el Kit de herramientas de realidad aumentada de iPhone :

 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


Queda por resolver el problema del almacenamiento de puntos y fotos. Para estos fines, se decidió utilizar Contentful, y en la implementación actual del proyecto nos satisfizo completamente.


En el momento del desarrollo de la aplicación móvil, todos los bekenders estaban involucrados en proyectos comerciales y se les permitía contentar durante varias horas:

  • desarrollador móvil - backend conveniente
  • administrador de contenido: un área de administración conveniente para llenar datos

La implementación similar del backend fue utilizada originalmente por los equipos que participaron en el hackathon (mencionado al comienzo del artículo), lo que demuestra una vez más que cosas como los hackathons te permiten escapar de resolver tus problemas urgentes en los proyectos, hacen posible recrear e intentar algo Nuevo a estrenar.

Conclusión



Fue muy interesante desarrollar una aplicación AR , en el proceso probamos varias bibliotecas listas para usar, pero también tuvimos que recordar las matemáticas y escribir muchas cosas nosotros mismos.

Simple, a primera vista, el proyecto requirió muchas horas de trabajo para implementar y refinar los algoritmos, a pesar de que usamos el SDK estándar de Apple.

Recientemente publicamos la aplicación en la AppStore . Así es como se ve en el trabajo.


Hasta ahora, en nuestra base de datos hay puntos solo para Taganrog, sin embargo, todos pueden participar en la expansión del "área de cobertura".

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


All Articles