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

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>