Crea una app de geolicalizacion con AR.js y A-frame WebAR

ingcarlosreina reina apuntando con sus indices a dos iconos de GPS y localizacion
Crea una app de geolicalizacion con AR.js y A-frame WebAR

La Realidad Aumentada basada en ubicación (Location-Based AR) (location based augmented reality tutorial) permite superponer contenido digital en coordenadas geográficas específicas del mundo real. A diferencia de la AR basada en marcadores (que requiere escanear un código QR o imagen), esta tecnología utiliza los sensores del dispositivo móvil: GPS para la posición, brújula para la orientación y acelerómetro para la estabilidad.

En este tutorial, exploraremos cómo construir una aplicación web capaz de mostrar modelos 3D en puntos de interés turístico o comercial, manteniendo un tamaño constante en pantalla y permitiendo al usuario interactuar con ellos mediante clics.

Si quieres leer la documentacion de ar.js y su location based: Location Based – AR.js Documentation

Tecnologías y librerías de realidad aumentada utilizadas en el desarrollo de esta aplicación de realidad mixta.

Para lograr una experiencia fluida directamente desde el navegador (sin instalar apps), utilizaremos el siguiente stack tecnológico:

Te invito a que visites este link por si quieres saber más sobre apps de realidad aumentada sin instalar apps en formato WebAR y PWA:

Continuemos con las librerías javascript que usaremos para este tutorial de (augmented reality gps location)

A-Frame (El Motor 3D)

Es un framework web para construir experiencias de realidad virtual y aumentada. Nos permite manejar una «escena» 3D usando etiquetas HTML simples (como <a-entity> o <a-box>). En este proyecto, A-Frame gestiona la renderización de los modelos .glb y el sistema de cámara.

AR.js (El Motor de Realidad Aumentada)

Es la librería que conecta la cámara del móvil con la escena 3D. Específicamente, utilizamos el módulo de Location-Based AR, que traduce las coordenadas de latitud y longitud (GPS) a posiciones X, Y, Z dentro del espacio virtual.

Componente Look-At

Un script esencial que fuerza a los objetos 3D a «mirar» siempre hacia la cámara del usuario. Esto es crucial en las apps de realidad aumentada GPS y localizacion para asegurar que los modelos (o carteles informativos) sean legibles y fáciles de clicar desde cualquier ángulo.

Lógica de Implementación

El núcleo de la aplicación se basa en tres pilares:

  • Jerarquía de Entidades: Para evitar problemas de rotación y posicionamiento, separamos la lógica en contenedores:
    1. Entidad GPS (Abuelo): Solo contiene la coordenada geográfica.
    2. Entidad Visual (Padre): Maneja la rotación (look-at) y la escala.
    3. Modelo y Hitbox (Hijos): Contiene el gráfico 3D y una caja invisible para detectar clics.
  • Escalado Dinámico: Mediante un componente personalizado (fixed-size), calculamos la distancia entre el usuario y el objeto en cada fotograma para ajustar su tamaño, evitando que se vean diminutos a la distancia.
  • Interacción (Raycaster):  Para detectar colisiones con las cajas invisibles (hitboxes) colocadas sobre los modelos los cuales permiten abrir los modales creados en CSS y HTML.

Esta arquitectura garantiza una experiencia estable y profesional, compatible tanto con dispositivos iOS como Android.

Pasemos a la parte más importante de este código que es el javascript. Utilice 4 puntos aleatorios de una ciudad, estos puntos utilizan latitud y longitud y fueron tomados utilizando Google Maps.

Aqui el codigo javascript para que solo lo copies y pegues en tu app.

// --- COMPONENTE PERSONALIZADO: TAMAÑO FIJO ---
// Este componente de A-Frame se ejecuta en cada ciclo de renderizado (tick).
// Su objetivo es contrarrestar la perspectiva natural: si un objeto está lejos,
// lo agranda para que visualmente parezca tener siempre el mismo tamaño en pantalla.
AFRAME.registerComponent('fixed-size', {
schema: { baseScale: { type: 'number', default: 1 } },
tick: function () {
const object3D = this.el.object3D;
const camera = this.el.sceneEl.camera;
if (!camera) return;

// Calculamos la distancia entre la cámara y el objeto 3D
const cameraPos = camera.getWorldPosition(new THREE.Vector3());
const objectPos = object3D.getWorldPosition(new THREE.Vector3());
const distance = cameraPos.distanceTo(objectPos);

if (distance < 1) return;

// Factor de escala: Distancia / 30.
// A mayor distancia, mayor escala.
const factor = distance / 30;
const finalScale = this.data.baseScale * factor;

// Aplicamos la escala calculada
object3D.scale.set(finalScale, finalScale, finalScale);
}
});

// --- DATOS DE LOCALIZACIÓN ---
// Array de objetos JSON con las coordenadas y datos de cada punto de interés.
const LUGARES = [
{ lat: ESCRIBE AQUI LA LATITUD EN NUMEROS - NO BORRES LA COMA QUE SIGUE , lon: ESCRIBE AQUI LA LONGITUD EN NUMEROS - NO BORRES LA COMA QUE SIGUE , nombre: "Supermercado Exito", desc: "Gran superficie comercial." },
{ lat: ESCRIBE AQUI LA LATITUD EN NUMEROS - NO BORRES LA COMA QUE SIGUE, lon: ESCRIBE AQUI LA LONGITUD EN NUMEROS - NO BORRES LA COMA QUE SIGUE , nombre: "Multicentro Mall", desc: "Centro comercial popular." },
{ lat: ESCRIBE AQUI LA LATITUD EN NUMEROS - NO BORRES LA COMA QUE SIGUE, lon: ESCRIBE AQUI LA LONGITUD EN NUMEROS - NO BORRES LA COMA QUE SIGUE , nombre: "Mariposario", desc: "Un lugar para ver mariposas." },
{ lat: ESCRIBE AQUI LA LATITUD EN NUMEROS - NO BORRES LA COMA QUE SIGUE, lon: ESCRIBE AQUI LA LONGITUD EN NUMEROS - NO BORRES LA COMA QUE SIGUE , nombre: "Aqua Mall", desc: "Centro empresarial." }
];

// Referencias al DOM
const debug = document.getElementById('debug');
const startBtn = document.getElementById('start-btn');
const overlay = document.getElementById('start-overlay');
const scene = document.querySelector('a-scene');
const modal = document.getElementById('info-modal');
const modalTitle = document.getElementById('modal-title');
const modalDesc = document.getElementById('modal-desc');

let cachedLat = 0;
let cachedLon = 0;
let modelsCreated = false; // Evita duplicar objetos

// --- LÓGICA DE UI (MODAL) ---
function mostrarModal(lugar) {
modalTitle.innerText = lugar.nombre;
modalDesc.innerText = lugar.desc;
modal.style.display = 'flex';
}

window.cerrarModal = function() {
modal.style.display = 'none';
}

// --- INICIO DE LA APLICACIÓN ---
startBtn.addEventListener('click', () => {
debug.innerText = "Accediendo a GPS...";

// Verificación de soporte GPS en el navegador
if (!navigator.geolocation) { alert("GPS no soportado"); return; }

// Obtener primera posición de alta precisión
navigator.geolocation.getCurrentPosition(
(position) => {
cachedLat = position.coords.latitude;
cachedLon = position.coords.longitude;

// Mostrar distancias iniciales en el panel debug
actualizarDebug();
// Solicitar permisos de sensores (iOS) e iniciar
requestDeviceOrientation();
},
(err) => { alert("Error GPS: " + err.message); },
{ enableHighAccuracy: true, timeout: 8000, maximumAge: 0 }
);
});

// Calcula y muestra la distancia a cada punto en el panel verde
function actualizarDebug() {
let info = "GPS OK.\n";
LUGARES.forEach(lugar => {
const dist = calcularDistancia(cachedLat, cachedLon, lugar.lat, lugar.lon);
info += `${lugar.nombre}: ${dist.toFixed(0)}m\n`;
});
debug.innerText = info;
}

// Manejo de permisos para iOS 13+ (DeviceOrientation)
function requestDeviceOrientation() {
if (typeof DeviceOrientationEvent !== 'undefined' && typeof DeviceOrientationEvent.requestPermission === 'function') {
DeviceOrientationEvent.requestPermission()
.then(response => { iniciarAR(); })
.catch(console.error);
} else {
iniciarAR(); // Android no requiere permiso explícito
}
}

// Inicia el motor de AR.js
function iniciarAR() {
overlay.style.display = 'none';

// Escuchar actualizaciones continuas del GPS de AR.js
window.addEventListener('gps-camera-update-position', (e) => {
if(modelsCreated) return; // Crear modelos solo una vez

cachedLat = e.detail.position.latitude;
cachedLon = e.detail.position.longitude;
crearModelos();
});

// Fallback: Si el evento tarda, iniciar con los datos que ya tenemos
setTimeout(() => { if (!modelsCreated && cachedLat !== 0) crearModelos(); }, 3000);
}

// --- CREACIÓN DE OBJETOS 3D ---
function crearModelos() {
if (modelsCreated) return;
modelsCreated = true;

LUGARES.forEach(lugar => {
// 1. ENTIDAD GPS (Abuelo)
// Se encarga EXCLUSIVAMENTE de la posición geográfica.
const gpsEntity = document.createElement('a-entity');
gpsEntity.setAttribute('gps-entity-place', `latitude: ${lugar.lat}; longitude: ${lugar.lon}`);

// 2. ENTIDAD VISUAL (Padre)
// Se encarga de la orientación (mirar al usuario) y la escala.
// Separarlo evita conflictos de coordenadas al rotar.
const visualEntity = document.createElement('a-entity');
visualEntity.setAttribute('look-at', '[gps-camera]');
visualEntity.setAttribute('fixed-size', 'baseScale: 1');

// 3. MODELO 3D (Hijo A)
const model = document.createElement('a-entity');
model.setAttribute('gltf-model', '#model-asset');
model.setAttribute('animation-mixer', '');
model.setAttribute('position', '0 0 0'); // En el centro

// 4. HITBOX INVISIBLE (Hijo B)
// Una caja invisible para capturar el clic. Es más fiable que clicar el modelo.
const hitbox = document.createElement('a-entity');
hitbox.setAttribute('geometry', 'primitive: box; width: 3; height: 4; depth: 3');
hitbox.setAttribute('material', 'visible: false'); // Invisible
hitbox.classList.add('clickable'); // CLASE CLAVE para el raycaster
hitbox.setAttribute('position', '0 2 0'); // Centrado verticalmente

// Evento de clic
hitbox.addEventListener('click', function() {
console.log("Clic en: " + lugar.nombre);
mostrarModal(lugar);
});

// Construcción de la jerarquía
visualEntity.appendChild(model);
visualEntity.appendChild(hitbox);

gpsEntity.appendChild(visualEntity);

scene.appendChild(gpsEntity);
});

actualizarDebug();
}

// Fórmula de Haversine para calcular distancia en metros entre dos coordenadas
function calcularDistancia(lat1, lon1, lat2, lon2) {
const R = 6371e3; // Radio tierra
const φ1 = lat1 * Math.PI/180;
const φ2 = lat2 * Math.PI/180;
const Δφ = (lat2-lat1) * Math.PI/180;
const Δλ = (lon2-lon1) * Math.PI/180;
const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ/2) * Math.sin(Δλ/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c;
}

Ahora hablemos de algunos puntos super importantes para el buen desarrollo de una app de Geolocalización con realidad aumentada y que se deben de tener en cuenta:

  • El Problema de la Distancia (Raycaster).

Por defecto, los sistemas 3D solo detectan clics en objetos cercanos. Dado que en Geo-AR los objetos pueden estar a kilómetros, configuramos el raycaster (el rayo detector de clics) con una propiedad far elevada (20,000 metros) para alcanzar los puntos distantes.

  • Hitboxes (Cajas de Colisión).

Los modelos 3D complejos a veces tienen huecos en su geometría, lo que dificulta hacer clic en ellos. La solución profesional implementada aquí es colocar una «caja invisible» simple alrededor del modelo. El usuario cree que toca el modelo, pero técnicamente está tocando esta caja invisible optimizada para recibir el clic.

  • Escalado Dinámico.

Para evitar que los objetos lejanos se vean como puntos diminutos, implementamos un algoritmo matemático que calcula la distancia en cada fotograma y ajusta el tamaño del modelo proporcionalmente. Esto crea una ilusión óptica donde el objeto parece tener siempre el mismo tamaño en la pantalla, sin importar si está a 10 metros o a 1 kilómetro.

Si todo sale bien, tu app WebAR deberia estar así:

DESCARGA los archivos de este tutorial completo en mi patreon. Da clic en la imagen de Patreon para ser direccionado al artículo.

logo de realidad aumentada empezando desde cero y patreon
Realidad Aumentada Empezando Desde Cero y Patreon

 

Que incluye el .zip de descarga:

  • index.html unificado HTML, CSS y JavaScript.
  • Modelo 3D en formato GLB.
  • Archivo Índex comentado para tu aprendizaje.
  • Archivo CSS para los estilos comentado.
  • Archivo JavaScript comentado línea por línea para que aprendas a hacer este tipo de aplicación de realidad aumentada.

Deja tu comentario o agrégate a la comunidad de realidad aumentada empezando desde cero – WhatsApp.

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Scroll al inicio
0
Would love your thoughts, please comment.x
()
x