Construye la web de realidad virtual con A-Frame

4 junio, 2016 21:29 por

Esta es una traducción del artículo original publicado en el blog de Mozilla Hacks. Traducción por Rober Villar.

El equipo de Mozilla WebVR (MozVR) estableció hace un año la pregunta, ”¿cómo se vería la realidad virtual (VR) en la Web?” Hoy hacemos click en los enlaces para saltar de una página a otra, algún día caminaremos a través de los portales para saltar de un mundo a otro. Desafortunadamente, hay solo un puñado de desarrolladores WebGL en el mundo que saben cómo crear experiencias en 3D altamente interactivas. Pero hay potencialmente millones de desarrolladores web, y artistas 3D que anhelan una herramienta para hacer la creación de contenidos VR tan sencilla como construir una página web.

Recientemente hemos publicado una plataforma de código abierto llamada A-Frame para crear fácilmente experiencias VR y 3D en la Web. A-Frame pone en nuestras manos la creación de contenido VR permitiéndonos crear escenas con HTML declarativo que funciona en escritorio, Oculus Rift, y teléfonos inteligentes. Podemos manipular escenas con JavaScript simple como si lo hiciéramos con elementos HTML , y podemos continuar utilizando nuestas librerías favoritas y plataformas de JavaScript (por ejemplo: d3, React). Una escena básica en A-Frame se vería algo así:

En esta escena:

  • Tenemos algunas geometrías básicas utilizando <a-cube>, <a-cylinder>, <a-sphere>.
  • Tenemos una foto de 360 grados utilizando <a-sky> para el fondo.
  • Podemos movernos con las teclas WASD y mirar alrededor arrastrando el puntero.

Para entrar a VR, hacemos click en el ícono de los lentes (Goggles). Esta escena puede ser vista en unas gafas Oculus Rift, en el escritorio o en un teléfono inteligente utilizando un soporte de Google Cardboard. O, puede funcionar también como una escena en 3D normal. Más información sobre introducción a la VR. La sintaxis anterior debería parecernos familiar a la mayoría de nosotros; cada elemento en <a-scene> representa un objeto 3D , y podemos modificar esos objetos utilizando atributos HTML . Sin embargo debajo de este sencillo marcado , se encuentra una estructura 3D flexible y extensible.

three.js + Entidad-Componente-Sistema

Bajo la cubierta, A-Frame es una estructura three.js que trae el patrón entidad-componente-sistema (ECS) para el DOM. A-Frame está construida como una capa abstracta en la parte superior de three.js y es lo suficientemente extensible para realizar casi cualquier cosa que three.js pueda hacer.

El patrón ECS es un patrón comúnmente usado en el desarrollo de juegos, que favorece la composición sobre la herencia. Ya que A-Frame se lanzó para traer experiencias 3D altamente interactivas a la Web, éste adoptó los patrones existentes de la industria de los juegos. En ECS, cada objeto en la escena es una entidad, la cual es un contenedor de uso general que por sí mismo no hace nada. Los componentes son módulos reutilizables que entonces son conectados a una entidad con el fin de añadir apariencia, comportamiento, y/o funcionalidad.

Para dar un sencillo ejemplo abstracto, podemos tener componentes para color, neumáticos, y motor. Podemos componer entidades al configurar, mezclar y añadir componentes reutilizables:

  • Componer una entidad coche azul mediante la utilización del componente color con valor azul, el componente neumático con el número establecido a cuatro, y añadiendo el componente motor.
  • Componer una entidad bicicleta roja mediante la utilización del componente color con valor rojo, el componente neumático con el número establecido a dos, y sin añadir el componente motor.
  • Componer una entidad barco amarillo mediante la utilización del componente color  con valor amarillo, el componente neumático con el número establecido a cero, y añadiendo el componente motor.
Ejemplo de aframe

Representación abstracta del patrón entidad-componente-sistema por Ruben Mueller en The VR Jump.

En un A-Frame:

  • Una entidad está representada por <a-entity>. Es el bloque de construcción básico que comprende todo dentro de una escena.
  • Un componente está representado por un atributo HTML ( por ejemplo <a-entity engine>).
  • Las propiedades de un componente se pasan a través de una cadena en un atributo HTML donde serán analizadas más tarde .
  • Si un componente tiene solo una propiedad que lo define, entonces se ve como un atributo HTML normal (por ejemplo <a-entity visible="false">).
  • Si un componente tiene mas de una propiedad que lo define, entonces las propiedades se pasan a través de una sintaxis similar a los estilos CSS en línea (por ejemplo, <a-entity engine="cylinders: 4; horsepower: 158; mass: 200">).

Tomemos por ejemplo <a-cube>, podemos dividirlo en geometría (forma) y componentes materiales (apariencia):

<!-- la forma real de <a-cube>. -->
<a-entity geometry="primitive: box; depth: 2; height: 10; width: 4"
material="color: #FFF; src: url(texture.png)">

Los desarrolladores pueden escribir componentes para hacer casi cualquier cosa y compartirlos con otros desarrolladores para reutilizar. Vamos a configurar y añadir más componentes para componer una estructura más compleja:

Composición de una entidad

En un patrón de ECS , casi toda la lógica y el comportamiento deben ser encapsulados dentro de los componentes para fomentar modularidad y reutilización.

Construcción de una escena interactiva

Vamos con un ejemplo de la construcción de una escena en la que el flujo de trabajo gira en torno a crear componentes. Vamos a construir una escena interactiva en la que disparamos rayos láser a los enemigos que nos rodean. Podemos utilizar los componentes estándar que suministra el A-frame, o utilizar componentes que los desarrolladores de A-Frame han publicado en el ecosistema. Mejor todavía, ¡podemos escribir nuestros propios componentes para hacer lo que queramos!

Si deseas seguir el ejemplo, hay muchos modos de codificar con un A-Frame:

Vamos a comenzar añadiendo un objetivo enemigo:

Esto crea una escena estática básica donde el enemigo te observa incluso cuando te mueves a su alrededor. Podemos utilizar componentes A-Frame procedentes del ecosistema para hacer algunas cosas interesantes.

Utilización de componentes

El genial repositorio awesome-aframe es un buen lugar para encontrar componentes que la comunidad ha creado para habilitar nuevas características. Muchos de esos componentes se inician desde la plantilla de componentes (Component Boilerplate) y deberían tener builds disponibles en las carpetas dist/ de sus repositorios. Tomemos por ejemplo el componente diseño. Podemos tomar el build, introducirlo en nuestra escena, e inmediatamente es capaz de utilizar un sistema de diseño 3D que automáticamente posicione las entidades. En lugar de tener un enemigo, tendremos diez enemigos posicionados en un círculo alrededor del jugador:

Es desordenado tener el enemigo duplicado diez veces en el código. Podemos introducir el componente plantilla para mejorarlo. También podemos utilizar el sistema de animación  de A-Frame para tener enemigos dando marchando a nuestro alrededor.

Mezclando y armonizando los componentes de diseño y de la plantilla, ahora tenemos diez enemigos rodeándonos en un círculo. Vamos a activar el juego escribiendo nuestros propios componentes.

Creando componentes

Los desarrolladores que conocen JavaScript y three.js pueden crear componentes para añadir apariencia, comportamiento, y funcionalidad a las entidades. Como hemos visto, estos componentes pueden ser reutilizados y compartidos con la comunidad. No todos los componentes tienen que ser compartidos; pueden ser hechos a medida para una función o fin determinado.

Los componentes tienen datos, los cuales están definidos por el esquema y pueden ser pasados a través de HTML, y los métodos del ciclo de vida, que definen como se utilizan los datos para modificar la entidad a la que son adjuntados. Los métodos del ciclo de vida generalmente interactúan con el three.js, el DOM, y las APIs de A-Frame. Mi publicación previa sobre Como escribir un componente A-Frame VR entra en más detalles sobre como utilizar la API de componentes para registrar un componente.

Para la escena , queremos que sea capaz de disparar láser a los enemigos para hacerlos desaparecer. Necesitaremos componentes que creen el láser en clicks, al generar clicks, se impulsan los láseres, y se verifica cuando un láser alcanza a un enemigo.

Componente generador

Comencemos con la capacidad de crear láseres. Queremos ser capaces de generar una entidad láser que se inicie en la posición actual del jugador. Crearemos un componente generador que responda a los impulsos de la entidad, y cuando ese impulso es emitido, vamos a generar una entidad con una mezcla predefinida de componentes:

AFRAME.registerComponent('spawner', {
  schema: {
    on: { default: 'click' },
    mixin: { default: '' }
  },

  /**
   * Agregar manejador de eventos a la entidad
   */
  update: function (oldData) {
    this.el.addEventListener(this.data.on, this.spawn.bind(this));
  },

  /**
   * Crear la nueva entidad con una mezcla de componentes en la posición actual de la entidad.
   */
  spawn: function () {
    var el = this.el;
    var entity = document.createElement('a-entity');
    var matrixWorld = el.object3D.matrixWorld;
    var position = new THREE.Vector3();
    var rotation = el.getAttribute('rotation');
    var entityRotation;

    position.setFromMatrixPosition(matrixWorld);
    entity.setAttribute('position', position);

    // Haver que la nueva entidad vea en la misma dirección que la original
    // Permitir a la entidad modificar la rotación heredada.
    position.setFromMatrixPosition(matrixWorld);
    entity.setAttribute('position', position);
    entity.setAttribute('mixin', this.data.mixin);
    entity.addEventListener('loaded', function () {
      entityRotation = entity.getComputedAttribute('rotation');
      entity.setAttribute('rotation', {
        x: entityRotation.x + rotation.x,
        y: entityRotation.y + rotation.y,
        z: entityRotation.z + rotation.z
      });
    });
    el.sceneEl.appendChild(entity);
  }
});

Componente manejador de click

Ahora necesitaremos una manera de generar un evento click en la entidad del jugador con objeto de generar el láser. Podemos escribir un JavaScript sencillo en un script de contenido, pero es más reutilizable al escribir un componente que pueda permitir escuchar clicks a cualquier entidad:

AFRAME.registerComponent('click-listener', {
  // Cuando se hace click en la ventana, emitir un evento click desde la entidad.
  init: function () {
    var el = this.el;
    window.addEventListener('click', function () {
      el.emit('click', null, false);
    });
  }
});

Desde el HTML, definimos el láser mezclando y añadiendo el generador y el componente de click al jugador. Cuando pulsamos, el componente generador generará un láser saliendo en frente de la cámara:

Componente proyectil

Ahora los láser se generan frente a nosotros cuando pulsamos, pero necesitamos que se disparen y avancen. En el componente generador, tenemos el punto láser en la rotación de la cámara, y giramos ésta 90 grados alrededor del eje X para alinearla correctamente. Podemos añadir un componente proyectil para tener al láser viajando directamente en la dirección contraria (el eje Y en este caso):

AFRAME.registerComponent('projectile', {
  schema: {
    speed: { default: -0.4 }
  },

  tick: function () {
    this.el.object3D.translateY(this.data.speed);
  }
});

Entonces añadimos el componente proyectil a la mezcla del láser:

<a-assets>
<!-- Agregar comportamiento de proyectil. -->
<a-mixin id="laser" geometry="primitive: cylinder; radius: 0.05; translate: 0 -2 0"
material="color: green; metalness: 0.2; opacity: 0.4; roughness: 0.3"
projectile="speed: -0.5"></a-mixin>
</a-assets>

El láser ahora disparará como un proyectil al pulsar:

Componente colisionador

El último paso es añadir el componente colisionador para poder detectar cuando el láser alcanza a un enemigo. Podemos hacerlo utilizando three.js Raycaster, dibujando un rayo (línea) desde un extremo al otro del láser, entonces continuamente se comprueba si alguno de los enemigos está en intersección con el rayo. Si un enemigo esta intersectando nuestro rayo, lo está tocando, y utilizamos un evento para decirle que ha sido alcanzado:

AFRAME.registerComponent('collider', {
  schema: {
    target: { default: '' }
  },

  /**
   * Calcular objetivos.
   */
  init: function () {
    var targetEls = this.el.sceneEl.querySelectorAll(this.data.target);
    this.targets = [];
    for (var i = 0; i < targetEls.length; i++) {
      this.targets.push(targetEls[i].object3D);
    }
    this.el.object3D.updateMatrixWorld();
  },

  /**
   * Verificar por colisiones (para el cilindro).
   */
  tick: function (t) {
    var collisionResults;
    var directionVector;
    var el = this.el;
    var sceneEl = el.sceneEl;
    var mesh = el.getObject3D('mesh');
    var object3D = el.object3D;
    var raycaster;
    var vertices = mesh.geometry.vertices;
    var bottomVertex = vertices[0].clone();
    var topVertex = vertices[vertices.length - 1].clone();

    // Calcular posiciones absolutas de inicio y fin de la entidad.
    bottomVertex.applyMatrix4(object3D.matrixWorld);
    topVertex.applyMatrix4(object3D.matrixWorld);

    // Vector de inicio a fin de la entidad.
    directionVector = topVertex.clone().sub(bottomVertex).normalize();

    // Rayo de la colisión.
    raycaster = new THREE.Raycaster(bottomVertex, directionVector, 1);
    collisionResults = raycaster.intersectObjects(this.targets, true);
    collisionResults.forEach(function (target) {
      // Informar a la entidad sobre la colisión.
      target.object.el.emit('collider-hit', {target: el});
    });
  }
});

Entonces añadimos una etiqueta a los enemigos para designarlos como objetivos, añadiendo animaciones que estallen en la colisión para hacerlos desaparecer, y finalmente añadimos el componente colisionador al láser que se dirige a los enemigos:

<a-assets>
<img id="enemy-sprite" src="img/enemy.png">

<script id="enemies" type="text/x-nunjucks-template">
<a-entity layout="type: circle; radius: 5">
<a-animation attribute="rotation" dur="8000" easing="linear" repeat="indefinite" to="0 360 0"></a-animation>

{% for x in range(num) %}
<!-- Agregar clase de enemigo. -->
<a-image class="enemy" look-at="#player" src="#enemy-sprite" transparent="true">
<!-- Agregar animaciones de colisión. -->
<a-animation attribute="opacity" begin="collider-hit" dur="400" ease="linear"
from="1" to="0"></a-animation>
<a-animation attribute="scale" begin="collider-hit" dur="400" ease="linear"
to="0 0 0"></a-animation>
</a-image>
{% endfor %}
</a-entity>
</script>

<!-- Agregar colisionador que apunta a enemigos. -->
<a-mixin id="laser" geometry="primitive: cylinder; radius: 0.05; translate: 0 -2 0"
material="color: green; metalness: 0.2; opacity: 0.4; roughness: 0.3"
projectile="speed: -0.5" collider="target: .enemy"></a-mixin>
</a-assets>

Y ahí tenemos una completa escena básica interactiva en un A-Frame que puede ser vista en VR. Empaquetamos potencia en los componentes que nos permita construir escenas declarativas sin perder el control o la flexibilidad . El resultado es un rudimentario juego FPS que soporta VR en solo 30 líneas de HTML:

Comunidad

La comunidad ha construido algunas cosas fantásticas únicamente con la versión inicial de A-Frame. Echa un vistazo a lo que se ha compartido en hecho con A-Frame e impresionante A-Frame.

Todos pasamos el rato en el Slack de A-Frame en el que actualmente hay casi 350 personas dándole uso. ¡Juega con A-Frame y dinos lo que piensas! La realidad virtual ha llegado, y no puedes perder ese tren.

The following two tabs change content below.

jorgev

Add-ons Developer Relations Lead at Mozilla
Jorge trabaja para el equipo de complementos de Mozilla, y se dedica a Mozilla Hispano y Mozilla Costa Rica en su tiempo libre. Actualmente está encargado del blog de Mozilla Hispano Labs.

Compartir artículo:

Start the discussion at foro.mozilla-hispano.org

cc-by-sa