Descubre cómo la simulación de gravedad transforma las apps de Realidad Aumentada. Aprende a crear física realista con nuestro tutorial y código de ejemplo A-Frame.

¡Hola, innovadores y creadores de realidad aumentada! Hoy vamos a hablar de uno de los ingredientes secretos que convierten una buena experiencia de Realidad Aumentada (RA) en una experiencia verdaderamente inmersiva: la gravedad.
Cuando colocamos un objeto digital a través de la pantalla del móvil, nuestro cerebro sabe que es un truco. Pero ¿y si ese objeto reacciona como si realmente estuviera allí? ¿Si al inclinar el marcador sobre el que reposa, el objeto se desliza, choca y rebota de forma convincente? Ahí es donde la simulación de físicas, y en especial la gravedad, se convierte en la heroína silenciosa de la inmersión.
¿Por Qué Obsesionarnos con la Gravedad?
La gravedad es más que «hacer que las cosas caigan». En RA, simularla correctamente consigue que los objetos virtuales tengan «peso» y presencia en el mundo real. Ancla lo digital a nuestra realidad, haciendo que la interacción se sienta natural e intuitiva. Es la diferencia entre un sticker flotante y un objeto con el que realmente puedes jugar.
Hoy no solo hablaremos de ello, ¡lo haremos! Te guiaré a través de un ejemplo práctico usando MindAR y A-Frame para crear una escena donde una esfera metálica rebota dentro de un cubo de cristal, reaccionando a la gravedad del mundo real sin importar cómo rotes el marcador.
Ahora que estas por aprender el efecto de la gravedad tal vez te interese saber cómo funcionan las colisiones en javascript y realidad aumentada:
- Haciendo colisiones en tus apps de realidad aumentada WebAR
- Crea realidad aumentada gratis con MindAR, Aframe y three.js
¿Quieres ir un paso más allá? Agrégalo a tus video juegos AR (Augmented Reality videogames):
- Como crear un video juego en realidad aumentada – Parte Uno
- Como crear un juego en realidad aumentada WebAR – Parte Dos
Dejemos de un lado los anuncios y trabajemos en el código. ¡Manos a la Obra!
Aquí tienes el código completo. Lo he comentado línea por línea para que no te pierdas ni un solo detalle del truco. ¡Es más fácil de lo que parece!
Demostración de la Augmented Reality WebAR:
<!DOCTYPE html>
<html>
<head>
<!-- Configuración básica de la página web -->
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>AR Cubo con Gravedad</title>
<!-- Librería principal de A-Frame para crear escenas 3D/VR/AR en HTML -->
<script src="[https://aframe.io/releases/1.5.0/aframe.min.js](https://aframe.io/releases/1.5.0/aframe.min.js)"></script>
<!-- Librería de MindAR que integra el reconocimiento de imágenes con A-Frame -->
<script src="[https://cdn.jsdelivr.net/npm/mind-ar@1.2.5/dist/mindar-aframe.prod.js](https://cdn.jsdelivr.net/npm/mind-ar@1.2.5/dist/mindar-aframe.prod.js)"></script>
<style>
/* Estilos básicos para que la app ocupe toda la pantalla y para la caja de información */
body {
margin: 0;
overflow: hidden;
}
.info-box {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 12px 20px;
border-radius: 12px;
font-family: sans-serif;
font-size: 14px;
text-align: center;
max-width: 90%;
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
}
</style>
<script>
// Registramos un nuevo 'componente' en A-Frame. Los componentes son bloques de código reutilizables.
AFRAME.registerComponent('gravity-box', {
// 'schema' define las propiedades que podemos configurar desde el HTML, con sus valores por defecto.
schema: {
outerSize: {type: 'number', default: 1.6}, // Tamaño del cubo contenedor.
innerRadius: {type: 'number', default: 0.15}, // Radio de la esfera interior.
gravity: {type: 'number', default: -9.8} // Valor de la aceleración gravitacional.
},
// La función 'init' se ejecuta una sola vez cuando el componente es creado.
init: function () {
// 'el' es una referencia a la entidad de A-Frame a la que se adjunta este componente.
const el = this.el;
// 'data' contiene los valores de las propiedades definidas en el schema.
const data = this.data;
// Creamos un 'Group' de Three.js. Es como una carpeta para organizar nuestros objetos 3D.
this.sceneContainer = new THREE.Group();
// --- Creación de los objetos 3D ---
// 1. Geometría del cubo exterior. Define la forma y el tamaño.
const outerGeometry = new THREE.BoxGeometry(data.outerSize, data.outerSize, data.outerSize);
// 1.1 Material para las caras del cubo. Lo hacemos transparente.
const outerMaterial = new THREE.MeshBasicMaterial({
color: 0xffffff, // Color base blanco.
transparent: true, // Permite la transparencia.
opacity: 0.20, // Nivel de opacidad (muy transparente).
side: THREE.DoubleSide // Renderiza ambas caras de los polígonos (interior y exterior).
});
// Creamos el objeto 3D (Mesh) combinando la geometría y el material.
this.outerBox = new THREE.Mesh(outerGeometry, outerMaterial);
// Añadimos el cubo a nuestro contenedor de escena.
this.sceneContainer.add(this.outerBox);
// 1.2 Geometría para las aristas del cubo. Extrae solo los bordes de la geometría principal.
const edges = new THREE.EdgesGeometry(outerGeometry);
// Material para las líneas. Será una línea básica de color azul.
const lineMaterial = new THREE.LineBasicMaterial({ color: 0x0077ff });
// Creamos el objeto 3D de líneas (LineSegments).
const lineSegments = new THREE.LineSegments(edges, lineMaterial);
// Añadimos las aristas a nuestro contenedor.
this.sceneContainer.add(lineSegments);
// 2. Geometría para la esfera interior.
const innerGeometry = new THREE.SphereGeometry(data.innerRadius, 32, 32);
// Material estándar para la esfera, que reacciona a la luz para un look metálico.
const innerMaterial = new THREE.MeshStandardMaterial({
color: 0x9966cc, // Color base morado.
metalness: 0.9, // Propiedad que lo hace parecer metal (0 a 1).
roughness: 0.1 // Propiedad que define cuán pulida está la superficie (0 es un espejo).
});
// Creamos el objeto 3D (Mesh) de la esfera.
this.innerSphere = new THREE.Mesh(innerGeometry, innerMaterial);
// La añadimos al contenedor.
this.sceneContainer.add(this.innerSphere);
// 3. Luces para que el material 'Standard' pueda brillar.
// Luz ambiental: ilumina todos los objetos de la escena de manera uniforme.
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
this.sceneContainer.add(ambientLight);
// Luz direccional: simula la luz del sol, viene de una dirección específica.
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 2, 3); // Posicionamos la fuente de luz.
this.sceneContainer.add(directionalLight);
// 4. Propiedades para la simulación física.
// Creamos un vector 3D para almacenar la velocidad (x, y, z) de la esfera. Inicia en cero.
this.velocity = new THREE.Vector3(0, 0, 0);
// Finalmente, adjuntamos nuestro contenedor de Three.js a la entidad de A-Frame.
el.setObject3D('mesh', this.sceneContainer);
},
// La función 'tick' se ejecuta en cada fotograma (es el bucle principal de la animación).
tick: function (time, timeDelta) {
// Si el marcador no está visible, detenemos la simulación para ahorrar recursos.
if (!this.el.object3D.visible) return;
// 'timeDelta' es el tiempo en milisegundos desde el último fotograma. Lo convertimos a segundos.
const deltaSeconds = timeDelta / 1000;
// Accedemos a las propiedades de nuevo.
const data = this.data;
// --- LÓGICA DE FÍSICA ---
// 1. Definimos el vector de gravedad en el "mundo real" (siempre apunta hacia abajo en el eje Y).
const worldGravity = new THREE.Vector3(0, data.gravity, 0);
// 2. Obtenemos la rotación actual del marcador en el espacio 3D.
const markerWorldQuaternion = this.el.object3D.getWorldQuaternion(new THREE.Quaternion());
// 3. ¡LA MAGIA OCURRE AQUÍ! Convertimos la gravedad del mundo al sistema de coordenadas del marcador.
// Aplicamos la rotación INVERSA del marcador a la gravedad. Esto nos dice "hacia dónde es abajo" desde la perspectiva del marcador.
const localGravity = worldGravity.clone().applyQuaternion(markerWorldQuaternion.invert());
// 4. Actualizamos la velocidad de la esfera, aplicando la aceleración de la gravedad.
this.velocity.add(localGravity.multiplyScalar(deltaSeconds));
// 5. Actualizamos la posición de la esfera basándonos en su velocidad actual.
this.innerSphere.position.add(this.velocity.clone().multiplyScalar(deltaSeconds));
// --- LÓGICA DE COLISIÓN CON EL CUBO ---
// Calculamos la distancia máxima desde el centro a la que puede llegar la esfera.
const maxDistance = (data.outerSize / 2) - data.innerRadius;
// Creamos alias para acceder más fácilmente a la posición y velocidad.
const pos = this.innerSphere.position;
const vel = this.velocity;
// Comprobamos la colisión para el eje X (paredes izquierda y derecha).
if (Math.abs(pos.x) > maxDistance) {
pos.x = Math.sign(pos.x) * maxDistance; // Corregimos la posición para que no atraviese la pared.
vel.x *= -0.75; // Invertimos la velocidad en X y aplicamos amortiguación (rebote).
}
// Comprobamos la colisión para el eje Y (suelo y techo).
if (Math.abs(pos.y) > maxDistance) {
pos.y = Math.sign(pos.y) * maxDistance; // Corregimos posición.
vel.y *= -0.75; // Invertimos velocidad en Y.
}
// Comprobamos la colisión para el eje Z (paredes frontal y trasera).
if (Math.abs(pos.z) > maxDistance) {
pos.z = Math.sign(pos.z) * maxDistance; // Corregimos posición.
vel.z *= -0.75; // Invertimos velocidad en Z.
}
}
});
</script>
</head>
<body>
<!-- Este es el mensaje de ayuda que aparece en la parte inferior de la pantalla. -->
<div class="info-box">
Apunta la cámara a tu marcador para ver la escena.
</div>
<!-- Aquí comienza la escena de A-Frame, configurada para usar MindAR. -->
<a-scene mindar-image="imageTargetSrc: ./targets.mind;" color-space="sRGB" renderer="colorManagement: true, physicallyCorrectLights" vr-mode-ui="enabled: false" device-orientation-permission-ui="enabled: false">
<!-- La cámara por defecto de A-Frame. MindAR la controlará automáticamente. -->
<a-camera position="0 0 0" look-controls="enabled: false"></a-camera>
<!-- Esta es la entidad que se activa cuando MindAR detecta el marcador. -->
<!-- Le adjuntamos nuestro componente 'gravity-box' para que toda nuestra lógica se ejecute. -->
<a-entity mindar-image-target="targetIndex: 0" gravity-box>
</a-entity>
</a-scene>
</body>
</html>El Secreto está en la Rotación Inversa
La línea más importante de todo el script es esta:
- const localGravity = worldGravity.clone().applyQuaternion(markerWorldQuaternion.invert());
Desglosemos:
- worldGravity: Es un vector que siempre apunta hacia abajo (0, -9.8, 0), simulando la gravedad real.
- markerWorldQuaternion: Es la rotación actual del marcador en el espacio.
- .invert(): Calculamos la rotación inversa. Si el marcador está inclinado 30 grados a la derecha, la inversa es una rotación de 30 grados a la izquierda.
- applyQuaternion(…): Aplicamos esta rotación inversa al vector de gravedad.
El resultado es que transformamos la gravedad «real» al sistema de coordenadas «local» del marcador. Así, la esfera siempre sabe hacia dónde es «abajo» en el mundo real, ¡y la simulación se siente increíblemente natural!
Ahora es tu Turno
Este ejemplo es solo el comienzo. Imagina las posibilidades: juegos de laberintos, puzles de físicas, simulaciones interactivas… todo cobrando vida en tu propia mesa. La clave es experimentar. Cambia los valores, prueba con diferentes formas, añade más objetos.
La Realidad Aumentada es un lienzo en blanco, y añadirle las leyes de la física que damos por sentadas en nuestro día a día es uno de los pinceles más poderosos que tenemos para crear experiencias que dejen a la gente con la boca abierta.
¡Feliz creación!