Uso de sombreadores WebGL en WebAssembly

WebAssembly es increíblemente rápido para la combinación de números, motores de juegos y muchas otras cosas, pero nada puede compararse con la paralelización extrema de los sombreadores, que se ejecuta en la GPU.

Esto es especialmente así si estás buscando hacer un procesamiento de imágenes. Por lo general, en la web, esto se hace a través de WebGL, pero ¿cómo accedería a sus API al usar WebAssembly?

Configuración

Revisaremos brevemente la configuración de un proyecto de ejemplo, luego veremos cómo se puede cargar una imagen como textura. Luego, en un contexto separado, aplicaremos un sombreador GLSL de detección de bordes a la imagen.

Todo el código está en un repositorio aquí, si prefiere saltar directamente a eso. Tenga en cuenta que debe entregar sus archivos a través de un servidor para que funcione WebAssembly.

Como requisito previo, supondré que ya tiene configurado su proyecto WebAssembly. Si no, puede consultar el artículo aquí sobre cómo hacerlo, o simplemente bifurcar el repositorio vinculado anteriormente.

Para la demostración del código siguiente, estoy usando un archivo html básico que sirve solo para cargar una imagen, obtener su imageData y pasarlo al código de WebAssembly usando la función ccallArrays.

El archivo HTML con la imagen de entrada de vista previa

En cuanto al código C ++, hay un archivo emscripten.cpp que administra y enruta las llamadas a métodos a instancias de contexto creadas en el archivo Context.cpp. El archivo Context.cpp está estructurado de la siguiente manera:

Compilacion

WebGL se basa y sigue la especificación OpenGL ES (Embedded Systems), que es un subconjunto de OpenGL. Al compilar, emscripten asignará nuestro código a la API de WebGL.

Hay un par de versiones diferentes a las que podemos dirigirnos. OpenGL ES 2 se asigna a WebGL 1, mientras que OpenGL ES 3 se asigna a WebGL 2. De forma predeterminada, debe apuntar a WebGL 2, ya que viene con algunas optimizaciones y mejoras gratuitas.

Para hacer esto, debemos agregar el indicador USE_WEBGL2 = 1 a la compilación.

Si planea usar algunas características de OpenGL ES que no están presentes en la especificación de WebGL, puede usar los indicadores FULL_ES2 = 1 y / o FULL_ES3 = 1.

Para poder manejar texturas / imágenes grandes, también podemos agregar el indicador ALLLOW_MEMORY_GROWTH = 1. Esto elimina el límite de memoria del programa WebAssembly, a costa de algunas optimizaciones.

Si sabe de antemano cuánta memoria necesitará, puede usar el indicador TOTAL_MEMORY = X, donde X es el tamaño de la memoria.

Así que vamos a terminar con algo como esto:

emcc -o ./dist/appWASM.js ./dev/cpp/emscripten.cpp -O3 -s ALLOW_MEMORY_GROWTH = 1 -s USE_WEBGL2 = 1 -s FULL_ES3 = 1 -s WASM = 1 -s NO_EXIT_RUNTIME = 1 -std = c ++ 1z

Finalmente, necesitamos las siguientes importaciones, en nuestro código:

#include 
#include 
#include 
#include 
"C" externa {
   #include "html5.h" // módulo emscripten
}

Implementación

Si tiene experiencia previa con WebGL u OpenGL, este bit puede parecerle familiar.

Al escribir OpenGL, la API no funcionará hasta que cree un contexto. Esto normalmente se realiza mediante API específicas de la plataforma. Sin embargo, la web no está vinculada a la plataforma, y ​​en su lugar podemos usar una API integrada en OpenGL ES.

Sin embargo, la mayoría del trabajo preliminar puede implementarse más fácilmente utilizando las API de emscripten en el archivo html5.h. Las funciones que nos interesan son:

  • emscripten_webgl_create_context: esto creará una instancia de un contexto para el lienzo y los atributos dados
  • emscripten_webgl_destroy_context: esto es necesario para limpiar la memoria al destruir instancias de contexto
  • emscripten_webgl_make_context_current - Esto asignará y cambiará a qué contexto representará WebGL

Crea el contexto

Para comenzar a implementar, primero debe crear los elementos del lienzo en su código JavaScript. Luego, cuando utiliza la función emscripten_webgl_create_context, pasa el id del lienzo como primer parámetro, con cualquier configuración como el segundo. La función emscripten_webgl_make_context_current se usa para establecer el nuevo contexto como el que está actualmente en uso.

A continuación, el sombreador de vértices (para especificar coordenadas) y el sombreador de fragmentos (para calcular el color en cada píxel) se compilan y se crea el programa.

Finalmente, los sombreadores se adjuntan al programa, que luego se vincula y valida.

Aunque eso suena mucho, el código para esto es el siguiente:

La compilación del sombreador se realiza dentro de la función auxiliar CompileShader que realiza la compilación, imprimiendo cualquier error:

Crea el sombreador

El código de sombreador para este ejemplo es mínimo, y solo asigna cada píxel a sí mismo, para mostrar la imagen como una textura:

Puede acceder al contexto del lienzo en JavaScript además del contexto en el código C ++, pero debe ser del mismo tipo, "webgl2". Si bien la definición de varios tipos de contexto no hace nada al usar JavaScript, si lo hace antes de crear el contexto webgl2 en WebAssembly, arrojará un error cuando llegue la ejecución del código.

Cargando la textura

Lo primero que debe hacer al aplicar el sombreador es llamar a la función emscripten_webgl_make_context_current para asegurarse de que todavía estamos usando el contexto correcto, y glUseProgram para asegurarse de que estamos usando el programa correcto.

A continuación, obtenemos los índices de las variables GLSL (similar a obtener un puntero) a través de las funciones glGetAttribLocation y glGetUniformLocation, por lo que podemos asignar nuestros propios valores a esas ubicaciones. La función que solía hacer eso depende del tipo de valor.

Por ejemplo, un número entero, como la ubicación de la textura, necesita glUniform1i, mientras que un flotador necesitaría glUniform1f. Este es un buen recurso para ver qué función necesita usar.

A continuación, obtenemos el objeto de textura a través de glGenTextures, lo asignamos como la textura activa y cargamos el búfer imageData. Los búferes de vértices e índices se unen, para establecer los límites de la textura para llenar el lienzo.

Finalmente, borramos el contenido existente, definimos nuestras variables restantes con datos y dibujamos en el lienzo.

La textura que se carga

Detectar bordes con un sombreador

Para agregar otro contexto, donde se realiza la detección de bordes, cargamos un sombreador de fragmentos diferente (que aplica el filtro Sobel), y vinculamos el ancho y la altura como variables adicionales, en el código.

Para elegir entre diferentes sombreadores de fragmentos, para los diferentes contextos, simplemente agregamos una instrucción if-else en el constructor, así:

Y para cargar las variables ancho y alto, agregamos lo siguiente a la función de ejecución:

Si se encuentra con un error similar a ERROR: GL_INVALID_OPERATION: glUniform1i: función uniforme incorrecta para el tipo, entonces hay una función de asignación no coincidente para la variable dada.

Una cosa a tener en cuenta al enviar el imageData es utilizar el montón correcto, entero sin signo (la matriz con tipo Uint8Array). Puede obtener más información sobre estos aquí, pero si está utilizando la función ccallArray, configure la configuración "heapIn" en "HEAPU8", como se ve arriba.

Si el tipo no es correcto, la textura aún se cargará, pero verá representaciones extrañas, como estas:

Conclusión

Hemos pasado por un mini proyecto estilo "¡Hola Mundo!" Para mostrar cómo cargar texturas y aplicarles sombreadores GLSL en WebAssembly. El código completo está alojado en GitHub aquí, para mayor referencia.

Para un proyecto real, es posible que desee agregar un tratamiento adicional de errores. Lo omití aquí, para mayor claridad.

También puede ser más eficiente (en el ejemplo anterior) compartir datos como la textura imageData entre contextos. Puedes leer más sobre esto y más aquí.

Para leer más, puede consultar este enlace para errores comunes, o puede ver algunos proyectos de demostración en la carpeta glbook de emscripten, en GitHub.

Para ver el uso de WebGL en un proyecto de WebAssembly, puede consultar la rama de desarrollo en jsNet, un marco de aprendizaje profundo basado en la web, donde trabajaré para mover cálculos más pesados ​​a sombreadores, durante las próximas semanas (soporte para cómputo WebGL los sombreadores a través de OpenGL ES 3.1 no pueden venir lo suficientemente pronto ).

Actualizar

Para ver cómo se vería el cálculo de GPU utilizando sombreadores en WebAssembly, puede consultar el repositorio de GPGPU, una pequeña biblioteca en la que estoy trabajando, con versiones de JavaScript y WebAssembly.