Saltar al contenido principal
febrero 20, 2020

Wave Engine’s on-line glTF viewer

TL;DR: Anunciamos nuestro visor experimental online glTF creado con Wave Engine 3.0, impulsado por WebAssembly. ¡Prueba la demo! http://gltf.waveengine.net 

Y si quieres conocer más sobre la última versión de nuestro motor gráfico, ¡no te pierdas Evergine!

Durante la DotNet 2019 presentamos nuestro soporte inicial para WebAssembly (Wasm), mostrando nuestra librería WebGL.NET, que nos permite dibujar en una capa de bajo nivel. En los meses siguientes trabajamos en la refactorización de la pieza OpenGL en la plataforma agnóstica WaveEngine.OpenGL.Common, de la que nació WaveEngine.WebGL. A finales de año nuestros test visuales empezaron a aceptarse, por lo que ya estábamos listos para probarlo en un escenario más real.

Nuestro backend actual de WebGL se basa en su versión 2.0, que es aceptada por la mayoría de los navegadores: el nuevo Edge (basado en Chromium), Firefox y Chrome. Aunque Safari permite habilitar WebGL 2.0 a través de su menú de Características Experimentales, no está completado al 100% y se colapsa al ejecutar nuestra app. Si utilizas macOS, prueba con Chrome o Firefox; en iOS, actualmente no es posible porque todos los navegadores dependen de WebKit, la base de Safari.

La experiencia de la incorporación

glTF viewer es una SPA (Single Page Application, una web que funciona con una sola página) que funciona enteramente en el lado del cliente, potenciada por el soporte Mono paraWasm, que se  incluirá en .NET 5. glTF es, actualmente, el estándar para los modelos 3D, y puede ser visto online, simplemente arrastrando y soltando dentro (hemos incluido una demostración para aquellos que no tienen un archivo a mano).

El paquete con las características principales:

  • Soporte para glTF 2.0 (*) en diferentes modos: plain glTF, glTF-Binary (.glb) y glTF-Embedded
    • Aquí puedes encontrar modelos de ejemplo
    • (*) puede ocurrir que los modelos no se carguen: estamos trabajando en hacer el proceso de importación más fuerte, y nos ayudaría si nos reportas algún problema que encuentres (¡gracias de antemano!)
  • Los archivos load .glb de links externos: puedes mostrar modelos a otros simplemente compartiendo el link (ejemplo)
  • Manipular con el ratón o los dedos, pensando en los dispositivos móviles para este ultimo.

Este articulo mostrará algunas advertencias que hemos encontrado durante el desarrollo, y cómo las hemos superado finalmente. Esperamos que disfrutes leyendo y, con suerte, aprendas algo entre tanto.

No existe un File System (FS) en la Web

Esto no es del todo cierto. De hecho, este es el punto donde nos encontramos cuando empezamos a trabajar en este proyecto: Wave Engine se basa en el concepto del contenido, un camino en el FS donde los activos se colocan. Estos activos son, en la mayoría de los casos, preprocesados en tiempo de compilación. Cuando una app empieza, los activos están listos para ser consumidos por el motor.

Pensando en lanzar un set de archivos glTF dentro de la pagina web, primero necesitábamos conducir a través del pipeline para procesar el modelo con nuestro importador de glTF. Junto con Kenneth Pouncey (thank you!), de Mono, ya habíamos reescrito en C# la herramienta de Emscripten para construir un Virtual FS (VFS), Mono.WebAssembly.FilePackager, especificando una ruta local; sin embargo, ¿cómo escribimos allí los archivos que vienen del exterior? Emscripten ya ha resuelto esto, ofreciendo una API de FS en JavaScript, con un sabor similar a las operaciones comunes de E/S:

1
2
3
4
5
6
7
8
9
10
11
12
13
function writeToFS(filename, typedArray) {
    // TODO would there be any other way without handling exceptions?
    try {
        FS.stat(DropAbsoluteDirectory);
    } catch (exception) {
        FS.mkdir(DropAbsoluteDirectory);
    }
    let destinationFullPath = DropAbsoluteDirectory + '/' + filename;
    let stream = FS.open(destinationFullPath, 'w+');
    FS.write(stream, typedArray, 0, typedArray.byteLength, 0);
    FS.close(stream);
}

Esta función redacta bytes en typedArray en el VFS de Emscripten

Resuelto esto, el siguiente problema que nos encontramos fue cómo superar los archivos glTF que no fueron procesados de ninguna manera, y Wave Engine “no soporta” la lectura sobre la marcha. Las comillas son intencionadas, ya que si soportamos eso: al soltarlo en el Editor se renderiza el modelo inmediatamente, pero sigue habiendo alfo de magia debajo.

Empezamos a explorar para consumir el espacio de nombres WaveEngine.Assets dentro de una aplicación, en lugar de sólo desde el Editor, que era su entorno natural. Y voilá, ¡funcionó! Cuando un archivo .glb (el formato binario único para glTF) se suelta:

  • Se importa «descomprimiendo» su contenido (texturas, materiales, etc.) en el FS y
  • se exporta generando archivos .we* listos para ser leidos por Wave Engine

Uno de esos archivos es el actual modelo, el cual más adelante es usado para instanciar toda la jerarquía de entidades añadida a las escena. No es un proceso largo cuando se ejecuta en el escritorio pero, cuando se usa desde Wasm, tarda algunos segundos. Mono ha añadido recientemente soporte para multi-threading pero, hasta que no sea aceptado ampliamente por la mayoría de los navegadores comunes, nosotros todavía no podremos confiar en él, aunque definitivamente, nos ayudará a reducir ese tiempo, ya que actualmente procesamos los elementos uno por uno.

Una de las tareas que más tiempo y memoria consume del proceso anterior es la importación de texturas, que trataremos a continuación.

Obtención de Píxeles

Del gran número de modelos que hemos testado esos días, las texturas más comunes son de 2048 x 2048 píxeles. Cuando leemos tales, el formato de los píxeles se expresa como RGBA, lo que significa 4 bytes por píxel. Por lo tanto, si queremos dejar espacio para leer la imagen en la memoria necesitamos matrices de 2048 * 2048 * 4 bytes, que es mucho. Hemos encontrado en algunos casos menores texturas de 8K, que hacen esto aún peor.

La asignación de memoria en sí no es un problema, Wave depende internamente de ArrayPool para importar texturas, lo que al menos lo hace más suave. Las texturas son manejadas por nuestro ImageSharpImporter, el ImageSharp de SixLabors, que elegimos principalmente por su característica de multiplataforma pero, bajo Wasm hay un gran espacio abierto de mejora. Con números grandes, decodificar una imagen de 2048 píxeles puede llevar más de 10 s, lo que rompe la experiencia sin duda. (Tambien encontramos bloqueos con AOT, pero hemos solucionado esto deshabilitando el stripper en sus montajes).

¿Cómo, entonces, podemos leer las imágenes más rápido? Para nuestra galería de muestras de WebGL.NET, nuestro amigo Juan Antonio Cano consumió Skia a través de algunos bindings del .NET iniciales, y ya solucionó tal cosa tardando unos cientos de ms. Sin embargo, el estado actual de tales bindings, creados por el equipo Uno, no eran compatibles con vanilla Mono Wasm, por lo que buscamos una alternativa pensando en el mantenimiento en un futuro. También es importante que sólo necesitábamos la pequeña pieza para decodificar una imagen, y nada más.

Resulta que CanvasKit («Skia en Wasm», rápidamente), exponía dicha pieza, y tiene una interfaz JavaScript. Hicimos algunas pruebas en el playground de CanvasKit y parecía prometedor. Entonces, nuestro CanvasKitImporter nació -reemplazando el de ImageSharp.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private JSObject DecodeImage(Stream stream)
{
    if (!stream.CanSeek)
    {
        throw new ArgumentException("The stream cannot be seeked", nameof(stream));
    }
    stream.Seek(0, SeekOrigin.Begin);
    JSObject image;
    using (var memoryStream = new MemoryStream())
    {
        stream.CopyTo(memoryStream);
        using (var array = Uint8Array.From(memoryStream.GetBuffer()))
        {
            image = (JSObject)canvasKit.Invoke("MakeImageFromEncoded", array);
        }
    }
    return image;
}

CanvasKitImporter.DecodeImage() pasa la matriz de bytes subyacente a CanvasKit, que descodifica la imagen

Pasamos de cargar modelos como FlightHelmet en minutos (15-30) con la pestaña congelada, a menos de 20 segundos. Igualmente, es mucho tiempo para nosotros, pero debemos recordar que el proceso de importacion y exportación de archivos no se toca desde el código del escritorio. Al principio pensamos que la llamada de ArrayPool.Rent(length) estaba forzando el Garbage Collector (GC) para pasar e incurrir algunos segundos, pero, después de aislar esas llamadas, vimos que no era el culpable. Lo que significa que necesitamos investigar más a fondo.

A la Web y más allá

No todo esta resuelto: el tiempo de carga de los modelos grandes se ha de reducir, la asignación de memoria también debe disminuir, nuestra abstracción WebGL tambien podría ser más rápida. Sin embargo, esta visualización glTF es nuestro primer proyecto público creado con Wave Engine 3.0 para la Web.

Hemos perseguido esto durante un tiempo, pero el escenario todavia no estaba listo para lanzarse. A día de hoy, vemos muchisimas posibilidades que ayudarán a nuestros clientes a tener una experiencia visual dentro de los navegadores, añadiendo la Web a la lista oficial de plataformas que lo toleran.

Si crees que te podemos ayudar a conseguir la Web, estamos aquí para escucharte. Ah, y si tienes algún problema, por favor, comunícanoslo. Gracias por tu atención.

Autor
Marcos Cobeña Morián
Plain Concepts Mobile