Crea una Simulación de Física Interactiva en WebAR con Three.js y MindAR

En este tutorial, vamos a construir desde cero una aplicación de Realidad Aumentada para la web (WebAR) que va más allá de simplemente mostrar un modelo 3D. Crearemos una simulación interactiva: una brillante pelota metálica que rebota dentro de un cubo de cristal.
Este es uno de los ejemplos más básicos de colisiones, pero con tu creatividad puede ser llevado a los límites del desarrollo. Así que no te lo pierdas.
Lo mejor de todo es que lo haremos con herramientas accesibles y poderosas: Three.js para el renderizado 3D y MindAR para el reconocimiento de imágenes. Al final, no solo tendrás una app impresionante, sino que entenderás los conceptos clave detrás de la detección de colisiones, la iluminación de materiales PBR (Physically Based Rendering) y la gestión de eventos en una escena de RA.
¿Qué necesitas para empezar?
- Un editor de código: Visual Studio Code es una excelente opción gratuita.
- Un servidor local: La extensión «Live Server» para VS Code es perfecta.
- Un marcador de RA: Una imagen que MindAR pueda reconocer. Puedes compilar la tuya en el compilador online de este blog: Compilador de Marcadores AR | Crea Realidad Aumentada Gratis.
- Un archivo de sonido: Un sonido corto para el rebote (ejemplo: bounce.mp3).
¿Para qué hacer y saber hacer colisiones en realidad aumentada?
- Video Juegos: 100% de los juegos tiene colisiones. Aqui te dejo un post para que aprendas a hacer un juego en realidad aumentada:
Como crear un video juego en realidad aumentada – Parte Uno
Como crear un juego en realidad aumentada WebAR – Parte Dos
- Aplicaciones de realidad aumentada con alto grado de complejidad (Apps robustas).
- Ayuda a determinar si algún elemento está teniendo una o algunas colisiones con otro objeto en tu escena AR y por lo tanto genera errores a la hora de ser visualizado.
- Lograr interactividad real en tus apps de realidad aumentada.
- Permite realismo e inmersión (Genera un comportamiento esperado y sensación de solidez).
- Puede ser utilizado en simulaciones y entrenamiento profesional.
- Generar retroalimentación del usuario (Respuesta física y sensorial).
En resumen, saber hacer colisiones es saber cómo dar vida a tus experiencias de Realidad Aumentada. Es el puente que te permite pasar de simplemente «mostrar» objetos a crear mundos virtuales interactivos, realistas y útiles que reaccionan a sí mismos y al usuario.
¡Vamos a construir la aplicación!
Estructura HTML
El HTML es la base. Aquí definimos la estructura de la página, importamos las librerías necesarias y preparamos los contenedores para la pantalla de inicio y la escena de RA.
<!DOCTYPE html>
<!-- Define el tipo de documento como HTML5. -->
<html lang="es">
<!-- Define el idioma del contenido como español. -->
<head>
<!-- Contiene metadatos y enlaces a recursos externos. -->
<meta charset="UTF-8"> <!-- Asegura que el navegador interprete correctamente los caracteres especiales (acentos, ñ). -->
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- Configura la vista para que se adapte correctamente a dispositivos móviles. -->
<title>AR: Pelota Interactiva</title> <!-- El título que aparece en la pestaña del navegador. -->
<!-- Importmap: Le dice al navegador dónde encontrar las librerías de Three.js y MindAR por su nombre. -->
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/",
"mindar-image-three": "https://cdn.jsdelivr.net/npm/mind-ar@1.2.5/dist/mindar-image-three.prod.js"
}
}
</script>
<!-- Enlaza la hoja de estilos externa que contiene todo el diseño visual. -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- Contenido visible de la página web. -->
<!-- Contenedor principal donde MindAR renderizará la escena de Realidad Aumentada. -->
<div id="ar-container"></div>
<!-- Pantalla de inicio superpuesta con la animación y el botón. -->
<div id="start-overlay">
<!-- El elemento div que será nuestra pelota animada con CSS. -->
<div id="bouncing-ball"></div>
<!-- El botón que el usuario debe presionar para iniciar la experiencia. -->
<button id="start-button">Iniciar WebAR app</button>
</div>
<!-- Enlaza el archivo JavaScript externo. 'type="module"' es crucial para que funcionen los 'import'. -->
<script type="module" src="main.js"></script>
</body>
</html>El Estilo: Diseño con CSS
El CSS se encarga de que todo se vea bien. Damos estilo al contenedor principal y, lo más importante, creamos la pantalla de inicio con la pelota que rebota, incentivando al usuario a comenzar la experiencia.
/* Elimina el margen por defecto que los navegadores aplican al cuerpo del documento. */
body {
margin: 0;
}
/* Estilo para el contenedor principal de la Realidad Aumentada. */
#ar-container {
width: 100vw; /* Ocupa el 100% del ancho de la ventana del navegador. */
height: 100vh; /* Ocupa el 100% de la altura de la ventana del navegador. */
position: relative; /* Es necesario para que los elementos internos de MindAR se posicionen correctamente. */
overflow: hidden; /* Oculta cualquier contenido que se desborde del contenedor. */
}
/* Estilo para la capa de inicio que se superpone a toda la pantalla. */
#start-overlay {
position: absolute; /* Se posiciona sobre el resto del contenido. */
top: 0; /* Alineado arriba. */
left: 0; /* Alineado a la izquierda. */
width: 100%; /* Ocupa todo el ancho. */
height: 100%; /* Ocupa toda la altura. */
background-color: rgba(0, 0, 0, 0.8); /* Fondo negro con un 80% de opacidad. */
display: flex; /* Activa el modelo de caja flexible (Flexbox) para centrar fácilmente el contenido. */
flex-direction: column; /* Apila los elementos hijos verticalmente (la pelota encima del botón). */
justify-content: center; /* Centra el contenido verticalmente. */
align-items: center; /* Centra el contenido horizontalmente. */
z-index: 10; /* Asegura que esta capa esté por encima del contenedor de RA (que tiene un z-index más bajo). */
}
/* Estilo para la pelota animada en la pantalla de inicio. */
#bouncing-ball {
width: 60px; /* Ancho de la pelota. */
height: 60px; /* Alto de la pelota. */
background-color: #ffa500; /* Color naranja, igual que la pelota 3D. */
border-radius: 50%; /* Hace que el div cuadrado se vea como un círculo perfecto. */
margin-bottom: 40px; /* Añade un espacio entre la pelota y el botón de abajo. */
animation: bounce 1.5s infinite ease-in-out; /* Aplica la animación 'bounce' de forma infinita. */
}
/* Estilo para el botón de inicio. */
#start-button {
padding: 15px 30px; /* Añade relleno interno para que el texto no esté pegado a los bordes. */
font-size: 22px; /* Tamaño de la fuente del texto. */
font-family: sans-serif; /* Usa una fuente genérica y legible. */
color: white; /* Color del texto. */
background-color: #007bff; /* Color de fondo azul. */
border: 2px solid white; /* Borde blanco de 2px de grosor. */
border-radius: 10px; /* Redondea las esquinas del botón. */
cursor: pointer; /* Cambia el cursor a una mano para indicar que es un elemento clickeable. */
transition: background-color 0.3s; /* Añade una transición suave para el cambio de color de fondo. */
}
/* Estilo para el botón cuando el cursor del ratón está encima. */
#start-button:hover {
background-color: #0056b3; /* Cambia a un azul más oscuro para dar retroalimentación visual. */
}
/* Definición de la animación de rebote llamada 'bounce'. */
@keyframes bounce {
/* Estado inicial (0%) y final (100%) de la animación. */
0%, 100% {
transform: translateY(0); /* La pelota está en su posición original. */
}
/* Estado intermedio (50%) de la animación. */
50% {
transform: translateY(-30px); /* La pelota se desplaza 30 píxeles hacia arriba. */
}
}Lógica con JavaScript
Aquí es donde reside toda la inteligencia de nuestra aplicación. Desde configurar la escena 3D hasta controlar la física de la pelota y gestionar los eventos del marcador.
// --- Importaciones ---
// Importa la librería completa de Three.js para poder usar sus funcionalidades 3D.
import * as THREE from 'three';
// Importa el controlador de MindAR específico para Three.js, que conecta la RA con el renderizado 3D.
import { MindARThree } from 'mindar-image-three';
// --- Evento Principal ---
// Espera a que todo el contenido del DOM (la estructura HTML) esté completamente cargado antes de ejecutar el script.
document.addEventListener('DOMContentLoaded', () => {
// --- Referencias a Elementos del DOM ---
// Obtiene una referencia al elemento del botón de inicio para poder añadirle un evento de clic.
const startButton = document.querySelector("#start-button");
// Obtiene una referencia a la capa de inicio para poder ocultarla cuando comience la experiencia.
const startOverlay = document.querySelector("#start-overlay");
// --- Estado de la Simulación ---
// Declara una variable "interruptor" que controlará si la física de la pelota está activa (true) o pausada (false).
let isSimulationRunning = false;
// --- Función Principal ---
// Esta función asíncrona contiene toda la lógica para configurar e iniciar la escena de RA.
const startAR = async () => {
// Oculta la pantalla de inicio para mostrar el contenedor de la RA.
startOverlay.style.display = 'none';
// --- Inicialización de MindAR ---
// Crea una nueva instancia del motor de MindAR.
const mindarThree = new MindARThree({
container: document.querySelector("#ar-container"), // Le indica en qué div debe renderizar la cámara y la escena.
imageTargetSrc: "./targets.mind", // Especifica la ruta a tu archivo de marcador compilado.
});
// Extrae los componentes esenciales de Three.js (renderizador, escena, cámara) que MindAR ha creado y configurado.
const { renderer, scene, camera } = mindarThree;
// --- Configuración de Renderizado e Iluminación ---
// Configura el mapeo de tonos para lograr colores y brillos más realistas y evitar que las zonas muy iluminadas se vean "quemadas".
renderer.toneMapping = THREE.ACESFilmicToneMapping;
// Ajusta la exposición (brillo) general de la escena renderizada.
renderer.toneMappingExposure = 1.2;
// Asegura que los colores se procesen en el espacio de color sRGB, que es el estándar para la mayoría de las pantallas.
renderer.outputEncoding = THREE.sRGBEncoding;
// Crea un generador para mapas de entorno. Estos mapas son cruciales para que los objetos metálicos reflejen algo.
const pmremGenerator = new THREE.PMREMGenerator(renderer);
pmremGenerator.compileEquirectangularShader();
// Genera un mapa de entorno básico a partir de una escena vacía, que simula una iluminación ambiental general.
const envMap = pmremGenerator.fromScene(new THREE.Scene());
// Crea una luz ambiental que ilumina todos los objetos de la escena por igual, evitando que las sombras sean completamente negras.
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambientLight); // Añade la luz a la escena.
// Crea una luz direccional (como el sol) que viene de una dirección específica, creando brillos y sombras nítidas.
const directionalLight1 = new THREE.DirectionalLight(0xffffff, 1.5);
directionalLight1.position.set(5, 10, 7.5); // La posiciona arriba, a la derecha y al frente.
scene.add(directionalLight1); // Añade la luz a la escena.
// Crea una segunda luz direccional de relleno para suavizar las sombras y añadir reflejos adicionales.
const directionalLight2 = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight2.position.set(-5, -5, -5); // La posiciona abajo, a la izquierda y atrás.
scene.add(directionalLight2); // Añade la luz a la escena.
// --- Ancla de Realidad Aumentada ---
// Crea un "ancla" en el primer marcador (índice 0) de tu archivo .mind. Todos los objetos añadidos a este ancla aparecerán sobre el marcador.
const anchor = mindarThree.addAnchor(0);
// --- Gestión de Eventos del Marcador ---
// Asigna una función que se ejecutará automáticamente cuando MindAR detecte el marcador.
anchor.onTargetFound = () => {
console.log("Marcador encontrado, reanudando simulación.");
isSimulationRunning = true; // Activa el interruptor para que la física comience a funcionar.
}
// Asigna una función que se ejecutará automáticamente cuando MindAR pierda de vista el marcador.
anchor.onTargetLost = () => {
console.log("Marcador perdido, pausando simulación.");
isSimulationRunning = false; // Desactiva el interruptor para pausar la física.
}
// --- Creación de Objetos 3D ---
const BOX_SIZE = 0.8; // Define el tamaño (ancho, alto, profundidad) del cubo.
const BALL_RADIUS = 0.1; // Define el radio de la pelota.
const BALL_SPEED = 0.5; // Define la velocidad de la pelota.
// Crea la geometría (la forma) de un cubo.
const boxGeometry = new THREE.BoxGeometry(BOX_SIZE, BOX_SIZE, BOX_SIZE);
// Crea un array para almacenar los materiales de cada una de las 6 caras del cubo.
const faceMaterials = [];
for (let i = 0; i < 6; i++) {
// Crea un material estándar semitransparente para cada cara.
faceMaterials.push(new THREE.MeshStandardMaterial({ color: 0x888888, transparent: true, opacity: 0.1, side: THREE.DoubleSide }));
}
// Crea el objeto 3D (Mesh) del cubo combinando la geometría y los materiales de las caras.
const interactiveBox = new THREE.Mesh(boxGeometry, faceMaterials);
anchor.group.add(interactiveBox); // Añade el cubo al grupo del ancla para que aparezca en la RA.
// Crea una geometría que solo contiene las aristas (bordes) de la geometría del cubo.
const edgesGeometry = new THREE.EdgesGeometry(boxGeometry);
// Crea un material de línea simple de color blanco.
const lineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff });
// Crea el objeto 3D de las aristas.
const wireframeBox = new THREE.LineSegments(edgesGeometry, lineMaterial);
anchor.group.add(wireframeBox); // Añade las aristas al ancla para que se superpongan al cubo.
// Crea la geometría de una esfera.
const ballGeometry = new THREE.SphereGeometry(BALL_RADIUS, 32, 32);
// Crea el material de la pelota: naranja, metálico, brillante y que usa el mapa de entorno para los reflejos.
const ballMaterial = new THREE.MeshStandardMaterial({ color: 0xffa500, metalness: 0.5, roughness: 0.1, envMap: envMap.texture });
// Crea el objeto 3D (Mesh) de la pelota.
const ball = new THREE.Mesh(ballGeometry, ballMaterial);
anchor.group.add(ball); // Añade la pelota al ancla.
// Crea un vector 3D para la velocidad, con una dirección inicial aleatoria.
const ballVelocity = new THREE.Vector3((Math.random() - 0.5), (Math.random() - 0.5), (Math.random() - 0.5)).normalize().multiplyScalar(BALL_SPEED);
// --- Configuración del Sonido ---
const listener = new THREE.AudioListener(); // Crea un "oído" virtual para la escena.
camera.add(listener); // Lo adjunta a la cámara.
const bounceSound = new THREE.Audio(listener); // Crea la fuente de sonido.
const audioLoader = new THREE.AudioLoader(); // Crea el cargador de archivos de audio.
// Carga el archivo de sonido 'bounce.mp3' desde la misma carpeta.
audioLoader.load('./bounce.mp3', (buffer) => {
bounceSound.setBuffer(buffer); // Cuando se carga, asigna los datos del audio a la fuente de sonido.
bounceSound.setVolume(0.5); // Fija el volumen a la mitad.
});
// Inicia el motor de MindAR. Esto solicitará permiso para usar la cámara.
await mindarThree.start();
// --- Bucle de Animación y Física ---
const clock = new THREE.Clock(); // Crea un reloj para medir el tiempo entre fotogramas.
// Establece una función que se ejecutará en un bucle continuo en cada fotograma.
renderer.setAnimationLoop(() => {
// Solo ejecuta la lógica de la física si el interruptor está activado (marcador visible).
if (isSimulationRunning) {
const delta = clock.getDelta(); // Obtiene el tiempo (en segundos) que ha pasado desde el último fotograma.
// Mueve la pelota sumando a su posición actual la velocidad multiplicada por el tiempo delta.
ball.position.add(ballVelocity.clone().multiplyScalar(delta));
const halfBoxSize = BOX_SIZE / 2; // Calcula la mitad del tamaño del cubo para definir los límites.
let hitFaceIndex = -1; // Variable para rastrear qué cara fue golpeada (-1 significa ninguna).
// Lógica de colisión para cada uno de los 6 límites del cubo.
if (ball.position.x + BALL_RADIUS > halfBoxSize) { hitFaceIndex = 0; ballVelocity.x *= -1; ball.position.x = halfBoxSize - BALL_RADIUS; } // Derecha
else if (ball.position.x - BALL_RADIUS < -halfBoxSize) { hitFaceIndex = 1; ballVelocity.x *= -1; ball.position.x = -halfBoxSize + BALL_RADIUS; } // Izquierda
if (ball.position.y + BALL_RADIUS > halfBoxSize) { hitFaceIndex = 2; ballVelocity.y *= -1; ball.position.y = halfBoxSize - BALL_RADIUS; } // Arriba
else if (ball.position.y - BALL_RADIUS < -halfBoxSize) { hitFaceIndex = 3; ballVelocity.y *= -1; ball.position.y = -halfBoxSize + BALL_RADIUS; } // Abajo
if (ball.position.z + BALL_RADIUS > halfBoxSize) { hitFaceIndex = 4; ballVelocity.z *= -1; ball.position.z = halfBoxSize - BALL_RADIUS; } // Frente
else if (ball.position.z - BALL_RADIUS < -halfBoxSize) { hitFaceIndex = 5; ballVelocity.z *= -1; ball.position.z = -halfBoxSize + BALL_RADIUS; } // Atrás
// Si se detectó una colisión en este fotograma (hitFaceIndex ya no es -1)...
if (hitFaceIndex !== -1) {
// Cambia el color y la opacidad de la cara golpeada para crear un "flash" visual.
faceMaterials[hitFaceIndex].color.setHex(0x00ff00);
faceMaterials[hitFaceIndex].opacity = 0.5;
// Usa un temporizador para revertir el cambio después de 150 milisegundos.
setTimeout(() => {
faceMaterials[hitFaceIndex].color.setHex(0x888888);
faceMaterials[hitFaceIndex].opacity = 0.1;
}, 150);
// Si el sonido no se está reproduciendo ya, lo reproduce.
if (!bounceSound.isPlaying) {
bounceSound.play();
}
}
}
// Renderiza la escena 3D en cada fotograma, independientemente de si la simulación está activa o no.
renderer.render(scene, camera);
});
};
// --- Evento de Inicio ---
// Añade un "escuchador" de eventos al botón para que llame a la función startAR cuando el usuario haga clic.
startButton.addEventListener('click', () => {
startAR();
});
});Más Allá de lo Básico
listo!!! Has creado una aplicación de Realidad Aumentada que no solo es visualmente atractiva, sino también interactiva y dinámica. Has aprendido a:
- Configurar una escena WebAR con MindAR y Three.js.
- Iluminar objetos con materiales PBR para un aspecto realista.
- Implementar una simulación de física simple con detección de colisiones.
- Gestionar el ciclo de vida de la RA, pausando la simulación cuando el marcador se pierde.
- Asegurar la compatibilidad con dispositivos móviles mediante la interacción del usuario para activar el audio.
Este proyecto es una base fantástica. A partir de aquí, puedes experimentar cambiando la velocidad, los colores, las formas o incluso añadiendo más objetos a la simulación. ¡El único límite es tu imaginación!