Crea realidad aumentada gratis con MindAR, Aframe y three.js

Aprende a desarrollar una increíble escena de Realidad Aumentada con animaciones y colisiones. Te guiamos paso a paso con el código completo en MindAR y A-Frame

 

dos modelos 3d de un automovil de color azul y otro de color rojo con animacion y con fondo de ciudad en realidad aumentada
Animaciones y colisiones en realidad aumentada

Hoy vamos a sumergirnos en un proyecto fascinante que combina el poder de la Realidad Aumentada en la web (WebAR) con la interactividad de las animaciones y la física de colisiones. Crearemos, desde cero, una escena donde dos autos cobran vida sobre un marcador, se mueven uno hacia el otro hasta chocar y activan una animación espectacular en el momento del impacto.

Este tutorial no solo te mostrará cómo hacerlo, sino que también te enseñará por qué estas técnicas son cruciales para crear experiencias de RA que realmente cautiven a los usuarios.

Antes de continuar debo de nombrar y agradecer a las personas que hicieron posible esta augmented reality app pues utilice su contenido (Modelos 3D e imágenes).

La Importancia de la Realidad Aumentada Interactiva

La Realidad Aumentada ya no es ciencia ficción; es una herramienta poderosa que está transformando la manera en que interactuamos con el mundo digital y físico. Usando tecnologías como MindAR y A-Frame, podemos llevar la RA directamente al navegador de cualquier smartphone, sin necesidad de descargar aplicaciones. Esto elimina barreras y hace que la tecnología sea accesible para todos.

Pero ¿qué diferencia una buena experiencia de RA de una inolvidable? La respuesta está en la interactividad. Ver un modelo 3D estático es interesante, pero ver cómo ese modelo reacciona, se mueve y cuenta una historia es lo que genera un impacto duradero.

Animaciones y Colisiones: Dando Vida a tus Modelos

  • Animaciones: Las animaciones convierten objetos inertes en personajes con propósito. En nuestro proyecto, la animación que se activa tras el choque no es solo un efecto visual; es la culminación de la historia que estamos contando.
  • Colisiones: La detección de colisiones es la base de la interactividad física en un mundo virtual. Al hacer que nuestros autos «sepan» cuándo chocan, creamos una experiencia más realista e inmersiva. El usuario siente que las reglas del mundo físico se aplican a esta nueva capa digital.

Combinar estos elementos es clave para cualquier desarrollador que busque crear proyectos de realidad aumentada con Three.js que vayan más allá de la simple visualización.

Ahora, vamos a lo que viniste. Desglosaremos el código en sus tres componentes principales: HTML para la estructura, JavaScript para la lógica y una nota sobre el CSS.

Ya tenemos un post en el blog de colisiones en realidad aumentada: Haciendo colisiones en tus apps de realidad aumentada WebAR

tal vez te pueda interesar como hacer video juegos en realidad aumentada:

Pasemos al HTML

<!DOCTYPE html>
<html>
<head>
<!-- Metadatos esenciales para la visualización en móviles -->
<meta name="viewport" content="width=device-width, initial-scale=1" />

<!-- Título que aparecerá en la pestaña del navegador -->
<title>AR Car Collision (Con Escenario Inclinado) - MindAR</title>

<!-- Importación de la librería A-Frame, el framework base para nuestra escena 3D/AR -->
<script src="https://aframe.io/releases/1.5.0/aframe.min.js"></script>

<!-- Importación de la librería MindAR, que conecta A-Frame con la cámara para la detección de marcadores -->
<script src="https://cdn.jsdelivr.net/npm/mind-ar@1.2.5/dist/mindar-image-aframe.prod.js"></script>

<!-- Enlace a nuestro archivo JavaScript que contendrá toda la lógica de la escena -->
<script src="./main.js"></script>
</head>
<body>
<!-- <a-scene> es el contenedor principal de toda nuestra experiencia 3D y de Realidad Aumentada -->
<a-scene
<!-- El componente mindar-image activa la funcionalidad de RA basada en marcadores -->
mindar-image="imageTargetSrc: ./targets.mind;"

<!-- Desactiva la interfaz de usuario para entrar en modo VR, ya que es una experiencia AR -->
vr-mode-ui="enabled: false"

<!-- Desactiva la ventana emergente que pide permiso para acceder a la orientación del dispositivo -->
device-orientation-permission-ui="enabled: false"

<!-- Define el espacio de color para una representación de colores más precisa -->
color-space="sRGB"

<!-- Optimizaciones del renderizador para mejor calidad visual y luces realistas -->
renderer="colorManagement: true, physicallyCorrectLights"

<!-- Adjuntamos nuestro componente personalizado 'collision-manager' a la escena para que su lógica se ejecute -->
collision-manager
>
<!-- <a-assets> es un lugar para precargar todos los recursos (modelos, texturas, etc.) para un rendimiento óptimo -->
<a-assets>
<!-- Precarga del modelo 3D del primer carro, asignándole un ID para su uso posterior -->
<a-asset id="car1-model" src="./car1.glb"></a-asset>

<!-- Precarga del modelo 3D del segundo carro -->
<a-asset id="car2-model" src="./car2.glb"></a-asset>

<!-- Precarga de la imagen que se usará como textura para la calle -->
<img id="calle-texture" src="./calle.webp">

<!-- Precarga de la imagen que se usará como textura para el fondo -->
<img id="fondo-texture" src="./fondo.webp">
</a-assets>

<!-- <a-camera> define el punto de vista del usuario en la escena. MindAR la controlará automáticamente -->
<a-camera position="0 0 0" look-controls="enabled: false"></a-camera>

<!-- <a-entity mindar-image-target> es el ancla. Todo lo que esté dentro de esta entidad aparecerá sobre el marcador detectado -->
<a-entity mindar-image-target="targetIndex: 0">

<!-- Contenedor usado para aplicar una rotación global a toda la escena y levantarla 45 grados -->
<a-entity rotation="-45 0 0">

<!-- <a-plane> es una superficie plana. La usamos para crear el fondo de la escena -->
<a-plane
src="#fondo-texture"
position="0 0.75 -0.5"
width="4"
height="1.5">
</a-plane>

<!-- Otro plano, esta vez para la calle. Lo rotamos para que sea horizontal -->
<a-plane
src="#calle-texture"
position="0 -0.01 0"
rotation="-90 0 0"
width="4"
height="1.3">
</a-plane>

<!-- Entidad para el primer carro. Le asignamos su modelo 3D, posición, rotación y escala inicial -->
<a-entity id="car1" gltf-model="#car1-model" position="-1 0 0" rotation="0 90 0" scale="0.3 0.3 0.3"></a-entity>

<!-- Entidad para el segundo carro, posicionado al otro extremo y mirando hacia el primero -->
<a-entity id="car2" gltf-model="#car2-model" position="1 0 0" rotation="0 -90 0" scale="0.3 0.3 0.3"></a-entity>

</a-entity> <!-- Fin del contenedor de la escena -->

</a-entity> <!-- Fin del ancla del marcador -->
</a-scene> <!-- Fin de la escena AR -->
</body>
</html>

Ahora el famoso JavaScript

// Usamos AFRAME.registerComponent para crear un nuevo componente de A-Frame llamado 'collision-manager'.
// Este componente contendrá toda nuestra lógica interactiva.
AFRAME.registerComponent('collision-manager', {
// La función 'init' se ejecuta una sola vez, cuando el componente se carga por primera vez.
init: function () {
// Obtenemos una referencia a la entidad del primer carro usando su ID.
this.car1 = document.querySelector('#car1');
// Obtenemos una referencia a la entidad del segundo carro.
this.car2 = document.querySelector('#car2');
// Obtenemos una referencia a la entidad que funciona como ancla del marcador de MindAR.
this.targetEntity = document.querySelector('[mindar-image-target]');

// --- Variables de Estado de la Simulación ---
// 'markerVisible': Un booleano para saber si el marcador está siendo detectado por la cámara.
this.markerVisible = false;
// 'collisionDetected': Un booleano para saber si los carros ya han chocado.
this.collisionDetected = false;
// 'modelsLoaded': Un contador para asegurarnos de que ambos modelos 3D se hayan cargado antes de iniciar.
this.modelsLoaded = 0;
// 'speed': La velocidad a la que se moverán los carros (unidades por segundo).
this.speed = 0.5;
// 'collisionThreshold': La distancia mínima entre los carros para que se considere una colisión.
this.collisionThreshold = 0.5;
// 'clock': Un objeto de Three.js que nos ayuda a medir el tiempo transcurrido entre fotogramas.
this.clock = new THREE.Clock();

// --- Event Listeners para la Carga de Modelos ---
// Esperamos a que el modelo del primer carro se cargue completamente.
this.car1.addEventListener('model-loaded', () => {
// Obtenemos el objeto 3D (mesh) del modelo.
const model = this.car1.getObject3D('mesh');
// Si el modelo existe...
if (model) {
// ...creamos un 'AnimationMixer' de Three.js, que es el encargado de reproducir las animaciones.
this.mixer1 = new THREE.AnimationMixer(model);
// Extraemos la primera animación contenida en el archivo GLB.
this.animation1 = this.car1.components['gltf-model'].model.animations[0];
// Incrementamos el contador de modelos cargados.
this.modelsLoaded++;
}
});

// Hacemos exactamente lo mismo para el segundo carro.
this.car2.addEventListener('model-loaded', () => {
const model = this.car2.getObject3D('mesh');
if (model) {
this.mixer2 = new THREE.AnimationMixer(model);
this.animation2 = this.car2.components['gltf-model'].model.animations[0];
this.modelsLoaded++;
}
});

// --- Event Listeners de MindAR ---
// Esperamos a que MindAR emita el evento 'targetFound', que ocurre cuando detecta el marcador.
this.targetEntity.addEventListener('targetFound', () => {
// Actualizamos nuestro estado para indicar que el marcador está visible.
this.markerVisible = true;
// Llamamos a la función que reinicia la simulación a su estado inicial.
this.resetSimulation();
});

// Esperamos a que MindAR emita el evento 'targetLost', que ocurre cuando el marcador se pierde de vista.
this.targetEntity.addEventListener('targetLost', () => {
// Actualizamos nuestro estado.
this.markerVisible = false;
// Si el reloj está corriendo, lo detenemos para pausar la simulación.
if(this.clock.running) this.clock.stop();
});
},

// 'resetSimulation': Esta función devuelve todo al estado inicial.
resetSimulation: function() {
// Indicamos que aún no ha habido colisión.
this.collisionDetected = false;
// Colocamos el primer carro en su posición de partida.
this.car1.object3D.position.set(-1, 0, 0);
// Colocamos el segundo carro en su posición de partida.
this.car2.object3D.position.set(1, 0, 0);

// Si el mixer de animación del carro 1 existe, detenemos cualquier animación que se estuviera reproduciendo.
if (this.mixer1) this.mixer1.stopAllAction();
// Hacemos lo mismo para el carro 2.
if (this.mixer2) this.mixer2.stopAllAction();

// Si el reloj no está corriendo, lo iniciamos.
if(!this.clock.running) this.clock.start();
},

// La función 'tick' se ejecuta en cada fotograma, es el corazón de nuestra animación.
tick: function () {
// Si el marcador no está visible o si los modelos aún no se han cargado, no hacemos nada.
if (!this.markerVisible || this.modelsLoaded < 2) {
return;
}

// Obtenemos el tiempo (en segundos) que ha pasado desde el último fotograma.
const deltaTime = this.clock.getDelta();

// Actualizamos los mixers de animación con el tiempo transcurrido para que las animaciones avancen.
if (this.mixer1) this.mixer1.update(deltaTime);
if (this.mixer2) this.mixer2.update(deltaTime);

// Si la colisión ya fue detectada, no necesitamos mover más los carros.
if (this.collisionDetected) {
return;
}

// --- Lógica de Movimiento ---
// Movemos el carro 1 hacia la derecha (incrementando su posición en X).
this.car1.object3D.position.x += this.speed * deltaTime;
// Movemos el carro 2 hacia la izquierda (decrementando su posición en X).
this.car2.object3D.position.x -= this.speed * deltaTime;

// --- Lógica de Colisión ---
// Obtenemos la posición actual (vector 3D) del primer carro.
const pos1 = this.car1.object3D.position;
// Obtenemos la posición actual del segundo carro.
const pos2 = this.car2.object3D.position;

// Comprobamos si la distancia entre los dos carros es menor que nuestro umbral de colisión.
if (pos1.distanceTo(pos2) < this.collisionThreshold) {
// Si lo es, marcamos que la colisión ha ocurrido.
this.collisionDetected = true;
// Imprimimos un mensaje en la consola para depuración.
console.log('¡COLISIÓN DETECTADA POR DISTANCIA!');
// Llamamos a la función que reproduce las animaciones de choque.
this.playAnimations();
}
},

// 'playAnimations': Esta función se encarga de reproducir las animaciones de los carros.
playAnimations: function() {
// Si existe una animación para el carro 1...
if (this.animation1) {
// ...le pedimos al mixer que cree una "acción" para esa animación.
const action1 = this.mixer1.clipAction(this.animation1);
// La configuramos para que se reproduzca solo una vez (sin bucle).
action1.setLoop(THREE.LoopOnce);
// Hacemos que la animación se detenga en su último fotograma.
action1.clampWhenFinished = true;
// La reproducimos.
action1.play();
}

// Hacemos exactamente lo mismo para la animación del carro 2.
if (this.animation2) {
const action2 = this.mixer2.clipAction(this.animation2);
action2.setLoop(THREE.LoopOnce);
action2.clampWhenFinished = true;
action2.play();
}
}
});

Este es el resultado:

Como has visto, crear una escena de WebAR dinámica es totalmente posible con las herramientas adecuadas. Hemos aprendido no solo a posicionar modelos 3D sobre un marcador, sino también a darles vida con movimiento, a detectar interacciones clave como las colisiones y a desencadenar animaciones como recompensa.

El próximo paso es tuyo. ¿Qué historias quieres contar? Experimenta con diferentes modelos, animaciones y reglas. El universo de la Realidad Aumentada está a tu alcance, directamente desde el navegador.

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