Toca la Realidad Aumentada: Usa tus manos en apps WebAR

Cómo Tocar lo Virtual: Una Guía de Interacción con Manos en Realidad Aumentada

ĀæLa realidad aumentada se puede tocar? Aprende como hacerlo con MindAR y MediaPipe

La Realidad Aumentada (AR) ha dejado de ser una tecnología del futuro para convertirse en una herramienta accesible y poderosa que podemos experimentar directamente en nuestro navegador. ¿Y si te dijera que no solo puedes ver objetos 3D en tu entorno, sino también interactuar con ellos usando tus propias manos? En este artículo, exploraremos cómo la combinación de tecnologías web de vanguardia nos permite, literalmente, tocar la realidad aumentada.

La Magia de la Realidad Aumentada en la Web (WebAR)

La WebAR es la capacidad de ejecutar aplicaciones de realidad aumentada sin necesidad de instalar nada, directamente desde el navegador de tu teléfono. Esto elimina la principal barrera de entrada para los usuarios y abre un mundo de posibilidades para la educación, el marketing y el entretenimiento. Frameworks como A-Frame nos permiten construir escenas 3D y de AR usando HTML, haciendo que el desarrollo sea sorprendentemente sencillo.

MindAR: El Puente Hacia Nuestro Mundo

Para que la AR funcione, la computadora necesita «anclajes» en el mundo real. Aquí es donde entra MindAR, una potente librería de código abierto para WebAR. Su especialidad es el reconocimiento de imÔgenes, también conocido como marcadores.

A diferencia de los antiguos códigos QR en blanco y negro, MindAR funciona de maravilla con marcadores a color. Puedes usar una foto, la portada de un libro o una ilustración personalizada. Cuando apuntas la cÔmara de tu teléfono a la imagen, MindAR la reconoce y la usa como el escenario perfecto para colocar tu modelo 3D, asegurando que se mantenga fijo y estable en el entorno.

MediaPipe: Dando a la Web Ojos y Manos

Una vez que nuestro modelo 3D estÔ en el mundo, ¿cómo interactuamos con él? La respuesta es MediaPipe, una increíble colección de herramientas de Machine Learning de Google. Específicamente, su modelo de Detección de Manos (Hand Pose Detection) es capaz de identificar y rastrear 21 puntos clave en cada mano en tiempo real, directamente desde el video de la cÔmara.

MediaPipe nos da las coordenadas precisas de cada nudillo y la punta de cada dedo. Con esta información, podemos programar gestos complejos:

  • Saber si una mano estĆ” abierta o cerrada.
  • Detectar un gesto de pellizco entre el pulgar y el Ć­ndice.
  • Contar cuĆ”ntos dedos estĆ”n levantados.

Tocar lo Intangible: La Fusión de Mundos

Aquƭ es donde la verdadera magia ocurre. Combinamos el mundo 3D de A-Frame y MindAR con los datos de MediaPipe. La tƩcnica se llama Raycasting:

  • MediaPipe nos da la coordenada en la pantalla (2D) de la punta de nuestro dedo.
  • El código traza una lĆ­nea invisible (un Ā«rayoĀ») desde la cĆ”mara virtual, a travĆ©s de ese punto en la pantalla, hacia el espacio 3D.
  • Si ese rayo Ā«chocaĀ» con nuestro modelo 3D, Ā”hemos detectado un toque!

Este evento de «toque» puede usarse para activar cualquier cosa. En nuestro caso, lo usamos para controlar la animación del modelo.

La Animación: Dando Vida al Modelo 3D

Un modelo 3D no es solo una estatua digital; puede contener múltiples animaciones. Al tocarlo con el índice derecho, podemos activar una animación de «Saludo». Al hacerlo con el izquierdo, una de «Correr». Y si cerramos el puño, una de «Ataque». Esto crea una mascota virtual con la que podemos tener una conexión mucho mÔs profunda y personal.

La combinación de estas tecnologías nos permite crear experiencias inmersivas y memorables que antes estaban reservadas para aplicaciones nativas costosas. Hoy, con un marcador a color y tu teléfono, puedes literalmente alcanzar y tocar un nuevo universo digital.

Iniciemos con el HTML

<!DOCTYPE html>
<html>
<head>
<!-- Define la codificación de caracteres del documento -->
<meta charset="utf-8">
<!-- Configura la ventana grÔfica para que el sitio sea responsivo en dispositivos móviles -->
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Tƭtulo que aparece en la pestaƱa del navegador -->
<title>AR Pet Interaction - Gestos</title>

<!-- 1. Carga las librerĆ­as de A-Frame y MindAR, que son la base de la Realidad Aumentada -->
<!-- Carga el framework A-Frame para crear escenas 3D y WebVR/AR con HTML -->
<script src="https://aframe.io/releases/1.5.0/aframe.min.js"></script>
<!-- Carga la librerƭa MindAR especƭfica para A-Frame, para el reconocimiento de imƔgenes -->
<script src="https://cdn.jsdelivr.net/npm/mind-ar@1.2.5/dist/mindar-image-aframe.prod.js"></script>
<!-- Carga extras para A-Frame, crucial para manejar animaciones de modelos GLB -->
<script src="https://cdn.jsdelivr.net/gh/c-frame/aframe-extras@7.2.0/dist/aframe-extras.min.js"></script>

<!-- 2. Enlaza nuestra hoja de estilos CSS externa -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- Canvas para dibujar el esqueleto de la mano (modo debug) -->
<canvas id="debugCanvas"></canvas>

<!-- Define la escena principal de A-Frame donde vivirĆ” toda la experiencia AR -->
<!-- mindar-image: Activa el sistema de MindAR para reconocer marcadores -->
<!-- imageTargetSrc: Ruta al archivo .mind compilado que contiene nuestro marcador -->
<!-- maxTrack: Número mÔximo de marcadores que se pueden rastrear simultÔneamente -->
<a-scene mindar-image="imageTargetSrc: ./targets.mind; maxTrack: 2;" color-space="sRGB" renderer="colorManagement: true, physicallyCorrectLights" vr-mode-ui="enabled: false" device-orientation-permission-ui="enabled: false">

<!-- Contenedor para pre-cargar todos los assets (modelos 3D, imƔgenes, etc.) -->
<a-assets>
<!-- Define un asset, en este caso nuestro modelo 3D, y le da un ID para usarlo despuƩs -->
<a-asset-item id="wolfModel" src="./Wolf.glb"></a-asset-item>
</a-assets>

<!-- Define la cƔmara virtual a travƩs de la cual vemos la escena -->
<a-camera position="0 0 0" look-controls="enabled: false"></a-camera>

<!-- Entidad que representa nuestro marcador. El contenido dentro de ella solo serĆ” visible cuando el marcador sea detectado -->
<!-- mindar-image-target: Le dice a esta entidad que se active con el marcador de Ć­ndice 0 (el primero en nuestro archivo .mind) -->
<a-entity mindar-image-target="targetIndex: 0">

<!-- Entidad que carga y muestra nuestro modelo 3D (formato .glb) -->
<a-gltf-model id="wolf"
<!-- src: Le dice quƩ asset usar (el lobo que pre-cargamos) -->
src="#wolfModel"
<!-- position: Coordenadas x, y, z relativas al marcador -->
position="0 0 0"
<!-- scale: Cambia el tamaƱo del modelo en los ejes x, y, z -->
scale="0.3 0.3 0.3"
<!-- rotation: Rotación inicial del modelo en grados (ejes x, y, z) -->
rotation="0 -90 0"
<!-- animation-mixer: Inicia el modelo con una animación por defecto -->
animation-mixer="clip: Idle"
<!-- hand-interaction: Asigna nuestro componente JavaScript personalizado a este modelo -->
hand-interaction="
idleAnim: Idle;
rightIndexAnim: Hello;
leftIndexAnim: Run;
rightOpenAnim: Pray;
rightClosedAnim: Punch;
debug: true">
</a-gltf-model>
</a-entity>
</a-scene>

<!-- 3. Carga nuestro script de JavaScript al final para asegurar que todo el HTML estƩ listo -->
<script src="script.js" defer></script>
</body>
</html>

Si quieres ver tus modelos Glb con tus animaciones, te recomiendo este website: glTF Viewer

Creando un videojuego en realidad aumentada: Pasos iniciales

Mira tambiƩn este post: Mascotas virtuales: Tamagotchi en realidad aumentada WebAR

Y ahora miremos lo mÔs lindo de esta aplicación que es el Javascript.

<script>// Registra un nuevo componente personalizado en A-Frame llamado 'hand-interaction'
AFRAME.registerComponent('hand-interaction', {
// Define las propiedades que podemos configurar desde el HTML (los atributos)
schema: {
idleAnim: {type: 'string', default: 'Idle'}, // Nombre de la animación de reposo
rightIndexAnim: {type: 'string', default: 'Hello'}, // Animación al tocar con el índice derecho
leftIndexAnim: {type: 'string', default: 'Run'}, // Animación al tocar con el índice izquierdo
rightOpenAnim: {type: 'string', default: 'Pray'}, // Animación con la mano derecha abierta
rightClosedAnim: {type: 'string', default: 'Punch'}, // Animación con la mano derecha cerrada
debug: {type: 'boolean', default: true} // Activa/desactiva las ayudas visuales
},

// Función que se ejecuta una sola vez cuando el componente se inicializa
init: function () {
// Guarda una referencia al modelo 3D (la entidad a la que se adjunta este componente)
this.model = this.el;
// Guarda una referencia a la escena principal de A-Frame
this.sceneEl = this.el.sceneEl;
// Guarda una referencia a la entidad del marcador (el "padre" del modelo)
this.targetEntity = this.el.parentNode;

// Variable para saber si el detector de manos estĆ” listo
this.detectorReady = false;
// Variable para almacenar el detector de MediaPipe
this.handDetector = null;
// Objeto de Three.js para lanzar rayos y detectar colisiones (toques)
this.raycaster = new THREE.Raycaster();

// Objeto para almacenar el estado actual de cada mano (si toca o quƩ gesto hace)
this.handStates = {
Left: { touching: false },
Right: { touching: false, gesture: 'none' }
};
// Variable para guardar la animación que se estÔ reproduciendo actualmente
this.activeAnimation = null;

// Referencia al canvas HTML donde dibujaremos el esqueleto de la mano
this.canvas = document.querySelector("#debugCanvas");
// Contexto 2D del canvas, necesario para poder dibujar en Ʃl
this.ctx = this.canvas.getContext("2d");

// Escucha el evento 'loaded' de la escena. Se dispara cuando A-Frame estĆ” completamente listo.
this.sceneEl.addEventListener('loaded', () => {
// Cuando la escena estĆ” lista, procedemos a cargar los scripts de MediaPipe
this.loadHandDetectionScripts();
});
},

// Función asíncrona para cargar las librerías de MediaPipe y TensorFlow de forma dinÔmica
loadHandDetectionScripts: async function() {
// Lista de URLs de los scripts que necesitamos
const scripts = [
"https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-core",
"https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-backend-webgl",
"https://cdn.jsdelivr.net/npm/@mediapipe/hands",
"https://cdn.jsdelivr.net/npm/@tensorflow-models/hand-pose-detection"
];
// Función auxiliar para cargar un script y esperar a que termine
const loadScript = (src) => new Promise((resolve, reject) => {
// Crea un nuevo elemento <script> en el documento
const script = document.createElement('script');
// Asigna la URL del script
script.src = src;
// Cuando el script se carga con Ʃxito, resuelve la promesa
script.onload = resolve;
// Si hay un error, rechaza la promesa
script.onerror = reject;
// AƱade el script al <head> del documento para que se cargue
document.head.appendChild(script);
});
// Bloque try/catch para manejar posibles errores de carga
try {
// Itera sobre la lista de scripts y espera a que cada uno se cargue en orden
for (const src of scripts) { await loadScript(src); }
// Una vez cargados todos, lo notifica en la consola
console.log("LibrerĆ­as de Hand Pose cargadas exitosamente.");
// Llama a la siguiente función para configurar el detector
this.setupHandDetection();
} catch (error) {
// Si algĆŗn script falla, muestra un error en la consola
console.error("Error cargando los scripts:", error);
}
},

// Función asíncrona para configurar e inicializar el detector de manos
setupHandDetection: async function() {
// Busca el elemento <video> que MindAR crea automƔticamente
this.video = document.querySelector('video');
// Si no encuentra el video, detiene la ejecución
if (!this.video) return;
// Ajusta el tamaƱo del canvas de debug para que coincida con el del video
this.canvas.width = this.video.videoWidth;
this.canvas.height = this.video.videoHeight;

// Define el modelo de MediaPipe que queremos usar
const model = handPoseDetection.SupportedModels.MediaPipeHands;
// Configuración del detector
const detectorConfig = {
runtime: 'mediapipe', // Especifica que usaremos el motor de MediaPipe
solutionPath: 'https://cdn.jsdelivr.net/npm/@mediapipe/hands', // Ruta a los archivos del modelo
modelType: 'full', // 'full' para mayor precisión, 'lite' para mayor rendimiento
maxHands: 2 // Le decimos que detecte un mƔximo de 2 manos
};

// Bloque try/catch para manejar errores durante la creación del detector
try {
// Crea el detector con el modelo y la configuración especificados
this.handDetector = await handPoseDetection.createDetector(model, detectorConfig);
// Marca el detector como listo
this.detectorReady = true;
// Notifica en la consola que todo estĆ” listo
console.log("”Detector de manos listo para 2 manos!");
// Inicia el bucle de detección
this.detectHandsLoop();
} catch (error) {
// Si falla la creación, muestra un error
console.error("Error al crear el detector:", error);
}
},

// Bucle principal de detección que se ejecuta en cada frame
detectHandsLoop: async function() {
// Si el detector no estĆ” listo, no hace nada
if (!this.detectorReady) return;

// Llama al detector para que estime las posiciones de las manos en el video actual
const hands = await this.handDetector.estimateHands(this.video, {flipHorizontal: false});

// Borra el canvas de debug para dibujar el nuevo frame
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Resetea los estados de las manos en cada frame
this.handStates.Left.touching = false;
this.handStates.Right.touching = false;
this.handStates.Right.gesture = 'none';

// Solo procesa las manos si el marcador es visible
if (this.targetEntity.getAttribute('visible')) {
// Itera sobre cada mano detectada
for (const hand of hands) {
// Si el modo debug estĆ” activo, dibuja el esqueleto de la mano
if (this.data.debug) this.drawHand(hand);
// Procesa la mano para detectar toques y gestos
this.processHand(hand);
}
}

// Actualiza la animación del modelo basÔndose en los nuevos estados de las manos
this.updateAnimation();
// Vuelve a llamar a esta función en el siguiente frame de animación del navegador
requestAnimationFrame(this.detectHandsLoop.bind(this));
},

// Procesa una mano individual para detectar interacciones
processHand: function(hand) {
// Obtiene la lateralidad de la mano ('Left' o 'Right')
const handedness = hand.handedness;
// Si no se puede determinar la lateralidad, no hace nada
if (!handedness) return;

// Obtiene la lista de los 21 puntos clave de la mano
const keypoints = hand.keypoints;
// Busca el punto especĆ­fico de la punta del dedo Ć­ndice
const indexTip = keypoints.find(k => k.name === 'index_finger_tip');

// --- 1. Detección de Toque (con el dedo índice) ---
if (indexTip) {
// Convierte las coordenadas del dedo (en pĆ­xeles) a coordenadas de pantalla normalizadas (-1 a 1)
const screenX = (indexTip.x / this.video.videoWidth) * 2 - 1;
const screenY = -(indexTip.y / this.video.videoHeight) * 2 + 1;
const screenCoords = new THREE.Vector2(screenX, screenY);
// Obtiene la cƔmara de A-Frame
const camera = this.sceneEl.camera;
// Configura el raycaster para que lance un rayo desde la cƔmara a travƩs de la punta del dedo
this.raycaster.setFromCamera(screenCoords, camera);
// Calcula si el rayo intersecta (choca) con el modelo 3D
const intersects = this.raycaster.intersectObject(this.model.getObjectD('mesh'), true);
// Si hay alguna intersección, significa que estamos "tocando" el modelo
if (intersects.length > 0) {
// Actualiza el estado de la mano correspondiente
this.handStates[handedness].touching = true;
}
}

// --- 2. Detección de Gestos (solo para la mano derecha) ---
if (handedness === 'Right') {
// Define los puntos clave que necesitamos para la detección de gestos
const fingerTips = {
index: keypoints[8], middle: keypoints[12], ring: keypoints[16], pinky: keypoints[20]
};
const fingerKnuckles = {
index: keypoints[5], middle: keypoints[9], ring: keypoints[13], pinky: keypoints[17]
};

// Contadores para dedos extendidos y doblados
let extendedFingers = 0;
let foldedFingers = 0;

// Itera sobre los 4 dedos (ƭndice, medio, anular, meƱique)
for (const fingerName in fingerTips) {
const tip = fingerTips[fingerName];
const knuckle = fingerKnuckles[fingerName];
// Compara la posición vertical (Y) de la punta y el nudillo
// En coordenadas de pantalla, un valor Y mƔs bajo significa que estƔ mƔs arriba
if (tip.y < knuckle.y) {
extendedFingers++;
}
if (tip.y > knuckle.y) {
foldedFingers++;
}
}

// Si los 4 dedos estƔn extendidos, detectamos el gesto de "mano abierta"
if (extendedFingers === 4) {
this.handStates.Right.gesture = 'open';
if (this.data.debug) console.log("Gesto detectado: MANO DERECHA ABIERTA");
}
// Si los 4 dedos estƔn doblados, detectamos el gesto de "mano cerrada"
else if (foldedFingers === 4) {
this.handStates.Right.gesture = 'closed';
if (this.data.debug) console.log("Gesto detectado: MANO DERECHA CERRADA");
}
}
},

// Decide qué animación reproducir basÔndose en la prioridad de los estados
updateAnimation: function() {
// Por defecto, la animación es la de reposo
let newAnimation = this.data.idleAnim;

// Prioridad 1: Toque (tiene preferencia sobre los gestos)
if (this.handStates.Left.touching) {
newAnimation = this.data.leftIndexAnim;
} else if (this.handStates.Right.touching) {
newAnimation = this.data.rightIndexAnim;
// Prioridad 2: Gestos (solo se comprueban si no hay toque)
} else if (this.handStates.Right.gesture === 'open') {
newAnimation = this.data.rightOpenAnim;
} else if (this.handStates.Right.gesture === 'closed') {
newAnimation = this.data.rightClosedAnim;
}

// Para optimizar, solo cambiamos la animación si es diferente a la actual
if (newAnimation !== this.activeAnimation) {
// Guardamos la nueva animación como la activa
this.activeAnimation = newAnimation;
// Llamamos a la función que reproduce la animación
this.playAnimation(newAnimation);
}
},

// Reproduce una animación en el modelo 3D
playAnimation: function(clipName) {
// Usa el componente 'animation-mixer' de A-Frame para cambiar el clip de animación
this.model.setAttribute('animation-mixer', { clip: clipName, loop: 'repeat' });
},

// Dibuja el esqueleto de la mano en el canvas de debug
drawHand: function(hand) {
const handedness = hand.handedness || 'Unknown';
// Dibuja la mano izquierda en rojo y la derecha en verde lima
const color = handedness === 'Left' ? 'red' : 'lime';
this.ctx.strokeStyle = color;
this.ctx.lineWidth = 4;
// Define las conexiones entre los 21 puntos para dibujar el esqueleto
const connections = [ [0, 1], [1, 2], [2, 3], [3, 4], [0, 5], [5, 6], [6, 7], [7, 8], [5, 9], [9, 10], [10, 11], [11, 12], [9, 13], [13, 14], [14, 15], [15, 16], [13, 17], [0, 17], [17, 18], [18, 19], [19, 20], ];
// Dibuja cada línea de conexión
for (const conn of connections) { const [start, end] = conn; this.ctx.beginPath(); this.ctx.moveTo(hand.keypoints[start].x, hand.keypoints[start].y); this.ctx.lineTo(hand.keypoints[end].x, hand.keypoints[end].y); this.ctx.stroke(); }
// Dibuja los puntos de las articulaciones en color cian
this.ctx.fillStyle = "aqua";
for (const point of hand.keypoints) { this.ctx.beginPath(); this.ctx.arc(point.x, point.y, 6, 0, 2 * Math.PI); this.ctx.fill(); }
}
});</script>

 

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