El Hobbit

Cómo darle vida a la Tierra Media con WebGL para dispositivos móviles

Daniel Isaksson
Daniel Isaksson

Históricamente, llevar experiencias interactivas, basadas en la Web y con muchos elementos multimedia a dispositivos móviles y tablets ha sido un desafío. Las limitaciones principales son el rendimiento, la disponibilidad de la API, las limitaciones en el audio HTML5 en los dispositivos y la falta de una reproducción fluida de videos en línea.

A principios de este año, comenzamos un proyecto con amigos de Google y Warner Bros. para crear una experiencia web centrada en los dispositivos móviles para la nueva película El hobbit, El hobbit: La desolación de Smaug. Crear un experimento de Chrome para dispositivos móviles con muchos elementos multimedia ha sido una tarea realmente inspiradora y desafiante.

La experiencia está optimizada para Chrome para Android en los nuevos dispositivos Nexus, en los que ahora tenemos acceso a WebGL y Web Audio. No obstante, gran parte de la experiencia también es accesible en dispositivos y navegadores que no son WebGL gracias a la composición acelerada por hardware y las animaciones CSS.

Toda la experiencia se basa en un mapa de la Tierra Media y las ubicaciones y los personajes de las películas de El Hobbit. El uso de WebGL nos permitió dramatizar y explorar el rico mundo de la trilogía del Hobbit y permitir que los usuarios controlen la experiencia.

Desafíos de WebGL en dispositivos móviles

En primer lugar, el término "dispositivos móviles" es muy amplio. Las especificaciones de los dispositivos varían mucho. Por lo tanto, como desarrollador, debes decidir si quieres admitir más dispositivos con una experiencia menos compleja o, como lo hicimos en este caso, limitar los dispositivos compatibles a aquellos que puedan mostrar un mundo en 3D más realista. Para "Un viaje por la Tierra Media", nos centramos en los dispositivos Nexus y en cinco smartphones Android populares.

En el experimento, usamos three.js, como lo hicimos en algunos de nuestros proyectos de WebGL anteriores. Comenzamos con la implementación con la compilación de una versión inicial del juego del Trollshaw que se ejecutaría bien en la tablet Nexus 10. Después de algunas pruebas iniciales en el dispositivo, tuvimos en mente una lista de optimizaciones que se veía muy similar a lo que normalmente usaríamos para una laptop de baja especificación:

  • Usa modelos de pocos polígonos
  • Cómo usar texturas de baja resolución
  • Combina la geometría para reducir la cantidad de llamadas de dibujo tanto como sea posible.
  • Simplifica los materiales y la iluminación
  • Cómo quitar efectos de publicación y desactivar el suavizado de contorno
  • Optimiza el rendimiento de JavaScript
  • Renderiza el lienzo de WebGL a mitad de tamaño y aumenta la escala con CSS.

Después de aplicar estas optimizaciones a nuestra primera versión preliminar del juego, obtuvimos una velocidad de fotogramas constante de 30 FPS con la que estábamos contentos. En ese momento, nuestro objetivo era mejorar las imágenes sin afectar negativamente la velocidad de fotogramas. Probamos muchos trucos: algunos tuvieron un impacto real en el rendimiento, mientras que otros no tuvieron el efecto deseado.

Usa modelos de pocos polígonos

Comencemos con los modelos. El uso de modelos de pocos polígonos ayuda, sin dudas, al tiempo de descarga, así como al tiempo que lleva inicializar la escena. Descubrimos que podíamos aumentar bastante la complejidad sin afectar mucho el rendimiento. Los modelos de troll que usamos en este juego tienen aproximadamente 5,000 caras y la escena es de alrededor de 40,000, y eso funciona bien.

Uno de los trolls del bosque del Bosque de los Trolls
Uno de los trolls del bosque de Trollshaw

En el caso de otra ubicación (aún no publicada) en la experiencia, observamos un mayor impacto en el rendimiento debido a la reducción de polígonos. En ese caso, cargamos objetos con un polígono más bajo para dispositivos móviles que los objetos que cargamos para computadoras de escritorio. Crear diferentes conjuntos de modelos 3D requiere un poco de trabajo adicional y no siempre es necesario. Realmente depende de qué tan complejos sean tus modelos al principio.

Cuando trabajamos en escenas grandes con muchos objetos, intentamos ser estratégicos con la manera de dividir la geometría. Esto nos permitió activar y desactivar con rapidez mallas menos importantes para encontrar un parámetro de configuración que funcionara para todos los dispositivos móviles. Luego, podríamos optar por combinar la geometría en JavaScript durante el tiempo de ejecución para la optimización dinámica o combinarla en preproducción para guardar solicitudes.

Cómo usar texturas de baja resolución

Para reducir el tiempo de carga en dispositivos móviles, elegimos cargar diferentes texturas que eran de la mitad del tamaño de las texturas en las computadoras de escritorio. Resulta que todos los dispositivos pueden controlar tamaños de texturas de hasta 2048 x 2048 px y la mayoría pueden admitir 4096 x 4096 px. La búsqueda de texturas individuales no parece ser un problema una vez que se cargan a la GPU. El tamaño total de las texturas debe caber en la memoria de la GPU para evitar que estas se carguen y descarguen constantemente. Sin embargo, es probable que esto no sea un gran problema para la mayoría de las experiencias web. Sin embargo, combinar texturas en la menor cantidad de hojas de objeto como sea posible es importante para reducir la cantidad de llamadas de dibujo, lo que tiene un gran impacto en el rendimiento en dispositivos móviles.

Textura de uno de los trolls del bosque de Trollshaw
Textura de uno de los trolls del bosque del Bosque de los Trolls
(tamaño original: 512 × 512 px)

Simplifica los materiales y la iluminación

La elección de los materiales también puede afectar significativamente el rendimiento y se deben administrar con prudencia en los dispositivos móviles. Para optimizar el rendimiento, usamos MeshLambertMaterial (cálculo por luz de vértice) en tres.js en lugar de MeshPhongMaterial (cálculo por luz de téxel). En general, intentamos usar sombreadores simples con la menor cantidad posible de cálculos de iluminación.

Para ver cómo los materiales que usas afectan el rendimiento de una escena, puedes anular los materiales de la escena con un elemento MeshBasicMaterial . Esto te permitirá hacer una buena comparación.

scene.overrideMaterial = new THREE.MeshBasicMaterial({color:0x333333, wireframe:true});

Optimiza el rendimiento de JavaScript

A la hora de compilar juegos para dispositivos móviles, la GPU no siempre es el obstáculo más grande. Se dedica mucho tiempo a la CPU, especialmente a la física y las animaciones básicas. Un truco que, en ocasiones, es útil, según la simulación, es solo ejecutar estos costosos cálculos en cada fotograma. También puedes usar las técnicas de optimización de JavaScript disponibles cuando se trata de agrupación de objetos, recolección de elementos no utilizados y creación de objetos.

Actualizar los objetos preasignados en bucles en lugar de crear objetos nuevos es un paso importante para evitar “trampas” de la recolección de elementos no utilizados durante el juego.

Por ejemplo, considera usar código como este:

var currentPos = new THREE.Vector3();

function gameLoop() {
  currentPos = new THREE.Vector3(0+offsetX,100,0);
}

Una versión mejorada de este bucle evita la creación de objetos nuevos que deben recolectarse como elementos no utilizados:

var originPos = new THREE.Vector3(0,100,0);
var currentPos = new THREE.Vector3();
function gameLoop() {
  currentPos.copy(originPos).x += offsetX;
  //or
  currentPos.set(originPos.x+offsetX,originPos.y,originPos.z);
}

En la medida de lo posible, los controladores de eventos solo deben actualizar las propiedades y permitir que el bucle de renderización requestAnimationFrame controle la actualización de la etapa.

Otra sugerencia es optimizar o calcular previamente las operaciones de transmisión de rayos. Por ejemplo, si necesitas adjuntar un objeto a una malla durante un movimiento de ruta estática, puedes "registrar" las posiciones durante un bucle y, luego, leer estos datos en lugar de realizar una proyección de rayos contra la malla. O, al igual que en la experiencia de Rivendell, la proyección de rayos permite detectar interacciones del mouse con una malla invisible más simple de pocos polígonos. La búsqueda de colisiones en una malla de alto polígono es muy lenta y debe evitarse en un bucle de juego en general.

Renderiza el lienzo de WebGL a mitad de tamaño y aumenta la escala con CSS.

El tamaño del lienzo de WebGL es probablemente el único parámetro más eficaz que puedes ajustar para optimizar el rendimiento. Cuanto más grande sea el lienzo que utilices para dibujar tu escena en 3D, más píxeles deberán dibujarse en cada fotograma. Esto, por supuesto, afecta el rendimiento.El Nexus 10 con su pantalla de alta densidad de 2560 x 1600 píxeles tiene que superar 4 veces el número de píxeles que una tablet de baja densidad. A fin de optimizar esto para los dispositivos móviles, usamos un truco en el que configuramos el lienzo en la mitad del tamaño (50%) y, luego, lo escalamos a su tamaño deseado (100%) con transformaciones CSS 3D aceleradas por hardware. La desventaja de esto es una imagen pixelada en la que las líneas finas pueden convertirse en un problema, pero en una pantalla de alta resolución, el efecto no es tan malo. El rendimiento adicional vale la pena.

La misma escena sin escalamiento en la Nexus 10 (16 FPS) y escalada al 50% (33 FPS)
La misma escena sin escalamiento en la Nexus 10 (16 FPS) y escalada al 50% (33 FPS)

Objetos como componentes básicos

Para poder crear el gran laberinto del castillo de Dol Guldur y el valle interminable de Rivendell, creamos un conjunto de modelos 3D de bloques de construcción que reutilizamos. La reutilización de objetos nos permite garantizar que se creen instancias de objetos y se suban al comienzo de la experiencia y no durante su transcurso.

Bloques de construcción de objetos 3D utilizados en el laberinto de Dol Guldur.
Componentes básicos de objetos 3D usados en el laberinto de Dol Guldur.

En Rivendell, tenemos varias secciones básicas que cambiamos constantemente en profundidad en Z a medida que avanza el viaje del usuario. A medida que el usuario pasa por secciones, estas se reubican en la distancia.

Para el castillo Dol Guldur, quisimos regenerar el laberinto para cada juego. Para ello, creamos un script que regenera el laberinto.

La combinación completa de la estructura en una gran malla desde el principio da como resultado una escena muy grande y un rendimiento deficiente. Para abordar esto, decidimos ocultar y mostrar los componentes básicos dependiendo de si están a la vista o no. Desde el principio, tuvimos una idea de cómo usar una secuencia de comandos de proyección de rayos en 2D, pero al final usamos la serción de frustrum integrada de tres.js. Reutilizamos el script de la proyección de rayos para acercar el "peligro" al que se enfrenta el reproductor.

El siguiente paso importante es la interacción del usuario. En las computadoras de escritorio, se utiliza la entrada del mouse y del teclado. En los dispositivos móviles, los usuarios interactúan con los controles táctiles, de deslizamiento, de pellizcar, de la orientación del dispositivo, etcétera.

Cómo usar la interacción táctil en experiencias web móviles

Agregar compatibilidad táctil no es difícil. Puedes leer artículos geniales sobre el tema. Sin embargo, hay algunas cosas menores que pueden hacerlo más complicado.

Puedes tener acceso tanto al mouse. La Chromebook Pixel y otras laptops compatibles con el tacto cuentan con compatibilidad táctil y de mouse. Un error común es verificar si el dispositivo tiene habilitado el modo táctil y, luego, solo agregar objetos de escucha de eventos táctiles y ninguno para el mouse.

No actualices la renderización en los objetos de escucha de eventos. En su lugar, guarda los eventos táctiles en variables y reacciona a ellos en el bucle de renderización de requestAnimationFrame. Esto mejora el rendimiento y también fusiona eventos conflictivos. Asegúrate de reutilizar objetos en lugar de crear objetos nuevos en los objetos de escucha de eventos.

Recuerda que es multitáctil: event.touches es un array de todos los toques. En algunos casos, es más interesante mirar event.targetTouches o event.changedTouches en cambio y simplemente reaccionar a los toques que te interesan. Para separar los toques de los deslizamientos, usamos un retraso antes de comprobar si el toque se movió (deslizamiento) o si se está quieto (toque). Para obtener un pellizco, medimos la distancia entre los dos toques iniciales y cómo cambia con el tiempo.

En un mundo 3D, tienes que decidir cómo reacciona la cámara a las acciones del mouse en comparación con las acciones de deslizamiento. Una forma común de agregar movimiento de la cámara es seguir el movimiento del mouse. Esto puede hacerse con el control directo mediante la posición del mouse o con un movimiento delta (cambio de posición). No siempre querrás tener el mismo comportamiento en un dispositivo móvil que en un navegador de escritorio. Realizamos pruebas exhaustivas para decidir cuál era la mejor opción para cada versión.

Cuando trabajes con pantallas y pantallas táctiles más pequeñas, encontrarás que los dedos del usuario y los gráficos de interacción de la IU a menudo obstaculizan lo que deseas mostrar. Esto es algo a lo que estamos acostumbrados cuando diseñamos aplicaciones nativas, pero nunca antes habíamos tenido que pensar acerca de las experiencias web. Este es un verdadero desafío para los diseñadores y diseñadores de UX.

Resumen

Nuestra experiencia general a partir de este proyecto es que WebGL en dispositivos móviles funciona muy bien, especialmente en dispositivos más nuevos y de alta gama. En lo que respecta al rendimiento, parece que el recuento de polígonos y el tamaño de la textura afectan principalmente los tiempos de descarga e inicialización, y que los materiales, los sombreadores y el tamaño del lienzo de WebGL son las partes más importantes que se deben optimizar para el rendimiento en dispositivos móviles. Sin embargo, es la suma de las partes que afectan el rendimiento, por lo que todo lo que puede hacer para optimizar las cantidades

Orientarse a dispositivos móviles también significa que debe acostumbrarse a pensar en las interacciones táctiles, y que no solo se trata del tamaño en píxeles, sino también del tamaño físico de la pantalla. En algunos casos, tuvimos que acercar la cámara 3D para realmente ver qué sucedía.

Ya lanzamos el experimento y ha sido un viaje fantástico. Esperamos que lo disfrutes.

¿Quieres probarla? Haz tu propio viaje a la Tierra Media.