paint-brush
Migrando de WebGL a WebGPUpor@dmitrii
5,312 lecturas
5,312 lecturas

Migrando de WebGL a WebGPU

por Dmitrii Ivashchenko13m2023/12/20
Read on Terminal Reader

Demasiado Largo; Para Leer

Esta guía aclara la transición de WebGL a WebGPU y cubre diferencias clave, conceptos de alto nivel y consejos prácticos. A medida que WebGPU emerge como el futuro de los gráficos web, este artículo ofrece información invaluable tanto para ingenieros de software como para gerentes de proyectos.
featured image - Migrando de WebGL a WebGPU
Dmitrii Ivashchenko HackerNoon profile picture

Pasar a la próxima WebGPU significa más que simplemente cambiar las API de gráficos. También es un paso hacia el futuro de los gráficos web. Pero esta migración resultará mejor con preparación y comprensión, y este artículo lo preparará.


Hola a todos, mi nombre es Dmitrii Ivashchenko y soy ingeniero de software en MY.GAMES. En este artículo, analizaremos las diferencias entre WebGL y la próxima WebGPU, y explicaremos cómo preparar su proyecto para la migración.


Descripción general del contenido

  1. Cronología de WebGL y WebGPU

  2. El estado actual de WebGPU y lo que está por venir

  3. Diferencias conceptuales de alto nivel

  4. Inicialización

    • WebGL: el modelo de contexto

    • WebGPU: el modelo de dispositivo

  5. Programas y canalizaciones

    • WebGL: Programa

    • WebGPU: canalización

  6. Uniformes

    • Uniformes en WebGL 1

    • Uniformes en WebGL 2

    • Uniformes en WebGPU

  7. Sombreadores

    • Lenguaje de sombreado: GLSL frente a WGSL

    • Comparación de tipos de datos

    • Estructuras

    • Declaraciones de funciones

    • Funciones integradas

    • Conversión de sombreador

  8. Diferencias de convenciones

  9. Texturas

    • Espacio de ventana gráfica

    • Recortar espacios

  10. Consejos y trucos de WebGPU

    • Minimizar el número de tuberías que utiliza.

    • Cree canales por adelantado

    • Utilice paquetes de renderizado

  11. Resumen


Cronología de WebGL y WebGPU

WebGL , como muchas otras tecnologías web, tiene raíces que se remontan a un pasado bastante lejano. Para comprender la dinámica y la motivación detrás del cambio hacia WebGPU, es útil echar primero un vistazo rápido a la historia del desarrollo de WebGL:


  • Escritorio OpenGL (1993) Se estrena la versión de escritorio de OpenGL.
  • WebGL 1.0 (2011) : esta fue la primera versión estable de WebGL, basada en OpenGL ES 2.0, que a su vez se introdujo en 2007. Brindó a los desarrolladores web la capacidad de usar gráficos 3D directamente en los navegadores, sin la necesidad de complementos adicionales.
  • WebGL 2.0 (2017) : Introducido seis años después de la primera versión, WebGL 2.0 se basó en OpenGL ES 3.0 (2012). Esta versión trajo consigo una serie de mejoras y nuevas capacidades, haciendo que los gráficos 3D en la web sean aún más potentes.


En los últimos años, ha habido un gran interés en nuevas API de gráficos que brinden a los desarrolladores más control y flexibilidad:


  • Vulkan (2016) : Creada por el grupo Khronos, esta API multiplataforma es la "sucesora" de OpenGL. Vulkan proporciona acceso de nivel inferior a recursos de hardware de gráficos, lo que permite aplicaciones de alto rendimiento con un mejor control sobre el hardware de gráficos.
  • D3D12 (2015) : esta API fue creada por Microsoft y es exclusiva para Windows y Xbox. D3D12 es el sucesor de D3D10/11 y proporciona a los desarrolladores un control más profundo sobre los recursos gráficos.
  • Metal (2014) : Creada por Apple, Metal es una API exclusiva para dispositivos Apple. Fue diseñado teniendo en mente el máximo rendimiento en hardware de Apple.


El estado actual de WebGPU y lo que está por venir

Hoy en día, WebGPU está disponible en múltiples plataformas como Windows, Mac y ChromeOS a través de los navegadores Google Chrome y Microsoft Edge, a partir de la versión 113. Se espera compatibilidad con Linux y Android en un futuro próximo.


Estos son algunos de los motores que ya admiten (u ofrecen soporte experimental) para WebGPU:


  • Babylon JS : soporte completo para WebGPU.
  • ThreeJS : Soporte experimental por el momento.
  • PlayCanvas : En desarrollo, pero con perspectivas muy prometedoras.
  • Unity : se anunció compatibilidad muy temprana y experimental con WebGPU en la versión 2023.2 alfa.
  • Cocos Creator 3.6.2 : es compatible oficialmente con WebGPU, lo que lo convierte en uno de los pioneros en esta área.
  • Construir : actualmente compatible con v113+ solo para Windows, macOS y ChromeOS.



Teniendo esto en cuenta, la transición a WebGPU o al menos preparar proyectos para dicha transición parece ser un paso oportuno en el futuro cercano.


Diferencias conceptuales de alto nivel

Alejémonos y echemos un vistazo a algunas de las diferencias conceptuales de alto nivel entre WebGL y WebGPU, comenzando con la inicialización.

Inicialización

Al comenzar a trabajar con API de gráficos, uno de los primeros pasos es inicializar el objeto principal para la interacción. Este proceso difiere entre WebGL y WebGPU, con algunas peculiaridades para ambos sistemas.

WebGL: el modelo de contexto

En WebGL, este objeto se conoce como "contexto", que esencialmente representa una interfaz para dibujar en un elemento de lienzo HTML5. Obtener este contexto es bastante simple:

 const gl = canvas.getContext('webgl');


El contexto de WebGL en realidad está vinculado a un lienzo específico. Esto significa que si necesita renderizar en varios lienzos, necesitará varios contextos.

WebGPU: el modelo de dispositivo

WebGPU introduce un nuevo concepto llamado "dispositivo". Este dispositivo representa una abstracción de GPU con la que interactuarás. El proceso de inicialización es un poco más complejo que en WebGL, pero proporciona más flexibilidad:

 const adapter = await navigator.gpu.requestAdapter(); const device = await adapter.requestDevice(); const context = canvas.getContext('webgpu'); context.configure({ device, format: 'bgra8unorm', });


Una de las ventajas de este modelo es que un dispositivo puede renderizar en varios lienzos o incluso en ninguno. Esto proporciona flexibilidad adicional; por ejemplo, un dispositivo puede controlar la representación en múltiples ventanas o contextos.



Programas y canalizaciones

WebGL y WebGPU representan diferentes enfoques para gestionar y organizar el proceso de gráficos.

WebGL: Programa

En WebGL, el foco principal está en el programa de sombreado. El programa combina sombreadores de vértices y fragmentos, definiendo cómo se deben transformar los vértices y cómo se debe colorear cada píxel.

 const program = gl.createProgram(); gl.attachShader(program, vertShader); gl.attachShader(program, fragShader); gl.bindAttribLocation(program, 'position', 0); gl.linkProgram(program);


Pasos para crear un programa en WebGL:


  1. Creación de sombreadores : el código fuente de los sombreadores se escribe y compila.
  2. Creación de programa : los sombreadores compilados se adjuntan al programa y luego se vinculan.
  3. Uso del programa : el programa se activa antes de renderizar.
  4. Transmisión de datos : los datos se transmiten al programa activado.


Este proceso permite un control de gráficos flexible, pero también puede ser complejo y propenso a errores, especialmente para proyectos grandes y complejos.

WebGPU: tubería

WebGPU introduce el concepto de "canalización" en lugar de un programa independiente. Este canal combina no solo sombreadores sino también otra información, que en WebGL se establece como estados. Entonces, crear una canalización en WebGPU parece más complejo:

 const pipeline = device.createRenderPipeline({ layout: 'auto', vertex: { module: shaderModule, entryPoint: 'vertexMain', buffers: [{ arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }] }], }, fragment: { module: shaderModule, entryPoint: 'fragmentMain', targets: [{ format, }], }, });


Pasos para crear una canalización en WebGPU:


  1. Definición del sombreador : el código fuente del sombreador se escribe y compila de forma similar a como se hace en WebGL.
  2. Creación de canalización : los sombreadores y otros parámetros de renderizado se combinan en una canalización.
  3. Uso de la canalización : la canalización se activa antes de renderizar.


Mientras WebGL separa cada aspecto del renderizado, WebGPU intenta encapsular más aspectos en un solo objeto, haciendo que el sistema sea más modular y flexible. En lugar de administrar sombreadores y estados de renderizado por separado, como se hace en WebGL, WebGPU combina todo en un solo objeto de canalización. Esto hace que el proceso sea más predecible y menos propenso a errores:



Uniformes

Las variables uniformes proporcionan datos constantes que están disponibles para todas las instancias de sombreado.

Uniformes en WebGL 1

En WebGL básico, tenemos la capacidad de establecer variables uniform directamente mediante llamadas API.


GLSL :

 uniform vec3 u_LightPos; uniform vec3 u_LightDir; uniform vec3 u_LightColor;


JavaScript :

 const location = gl.getUniformLocation(p, "u_LightPos"); gl.uniform3fv(location, [100, 300, 500]);


Este método es simple, pero requiere múltiples llamadas API para cada variable uniform .

Uniformes en WebGL 2

Con la llegada de WebGL 2, ahora tenemos la capacidad de agrupar variables uniform en buffers. Aunque todavía puedes usar sombreadores de uniformes separados, una mejor opción es agrupar diferentes uniformes en una estructura más grande usando buffers uniformes. Luego, envía todos estos datos uniformes a la GPU a la vez, de manera similar a cómo puede cargar un búfer de vértices en WebGL 1. Esto tiene varias ventajas de rendimiento, como reducir las llamadas API y estar más cerca de cómo funcionan las GPU modernas.


GLSL :

 layout(std140) uniform ub_Params { vec4 u_LightPos; vec4 u_LightDir; vec4 u_LightColor; };


JavaScript :

 gl.bindBufferBase(gl.UNIFORM_BUFFER, 1, gl.createBuffer());


Para vincular subconjuntos de un búfer uniforme grande en WebGL 2, puede utilizar una llamada API especial conocida como bindBufferRange . En WebGPU, hay algo similar llamado compensaciones de búfer uniformes dinámicas donde puede pasar una lista de compensaciones al llamar a la API setBindGroup .



Uniformes en WebGPU

WebGPU nos ofrece un método aún mejor. En este contexto ya no se soportan variables uniform individuales y se trabaja exclusivamente a través de buffers uniform .


WGSL :

 [[block]] struct Params { u_LightPos : vec4<f32>; u_LightColor : vec4<f32>; u_LightDirection : vec4<f32>; }; [[group(0), binding(0)]] var<uniform> ub_Params : Params;


JavaScript :

 const buffer = device.createBuffer({ usage: GPUBufferUsage.UNIFORM, size: 8 });


Las GPU modernas prefieren que los datos se carguen en un bloque grande, en lugar de muchos bloques pequeños. En lugar de recrear y volver a vincular búferes pequeños cada vez, considere crear un búfer grande y usar diferentes partes del mismo para diferentes llamadas de sorteo. Este enfoque puede aumentar significativamente el rendimiento.


WebGL es más imperativo, restablece el estado global con cada llamada y se esfuerza por ser lo más simple posible. WebGPU, por otro lado, apunta a estar más orientado a objetos y centrado en la reutilización de recursos, lo que conduce a la eficiencia.


La transición de WebGL a WebGPU puede parecer difícil debido a las diferencias en los métodos. Sin embargo, comenzar con una transición a WebGL 2 como paso intermedio puede simplificarle la vida.



Sombreadores

Migrar de WebGL a WebGPU requiere cambios no solo en la API, sino también en los sombreadores. La especificación WGSL está diseñada para hacer que esta transición sea fluida e intuitiva, manteniendo al mismo tiempo la eficiencia y el rendimiento de las GPU modernas.

Lenguaje de sombreado: GLSL vs WGSL

WGSL está diseñado para ser un puente entre WebGPU y las API de gráficos nativos. En comparación con GLSL, WGSL parece un poco más detallado, pero la estructura sigue siendo familiar.


Aquí hay un ejemplo de sombreador de textura:


GLSL :

 sampler2D myTexture; varying vec2 vTexCoord; void main() { return texture(myTexture, vTexCoord); }


WGSL :

 [[group(0), binding(0)]] var mySampler: sampler; [[group(0), binding(1)]] var myTexture: texture_2d<f32>; [[stage(fragment)]] fn main([[location(0)]] vTexCoord: vec2<f32>) -> [[location(0)]] vec4<f32> { return textureSample(myTexture, mySampler, vTexCoord); } 



Comparación de tipos de datos

La siguiente tabla muestra una comparación de los tipos de datos básicos y matriciales en GLSL y WGSL:



La transición de GLSL a WGSL demuestra el deseo de una escritura más estricta y una definición explícita de los tamaños de datos, lo que puede mejorar la legibilidad del código y reducir la probabilidad de errores.



Estructuras

La sintaxis para declarar estructuras también ha cambiado:


GLSL:

 struct Light { vec3 position; vec4 color; float attenuation; vec3 direction; float innerAngle; float angle; float range; };


WGSL:

 struct Light { position: vec3<f32>, color: vec4<f32>, attenuation: f32, direction: vec3<f32>, innerAngle: f32, angle: f32, range: f32, };


La introducción de una sintaxis explícita para declarar campos en estructuras WGSL enfatiza el deseo de una mayor claridad y simplifica la comprensión de las estructuras de datos en los sombreadores.



Declaraciones de funciones

GLSL :

 float saturate(float x) { return clamp(x, 0.0, 1.0); }


WGSL :

 fn saturate(x: f32) -> f32 { return clamp(x, 0.0, 1.0); }


Cambiar la sintaxis de las funciones en WGSL refleja la unificación del enfoque de las declaraciones y los valores de retorno, lo que hace que el código sea más consistente y predecible.



Funciones integradas

En WGSL, se ha cambiado el nombre o se han reemplazado muchas funciones GLSL integradas. Por ejemplo:



Cambiar el nombre de las funciones integradas en WGSL no solo simplifica sus nombres, sino que también las hace más intuitivas, lo que puede facilitar el proceso de transición para los desarrolladores familiarizados con otras API de gráficos.



Conversión de sombreador

Para aquellos que planean convertir sus proyectos de WebGL a WebGPU, es importante saber que existen herramientas para convertir automáticamente GLSL a WGSL, como **[Naga](https://github.com/gfx-rs/naga /)**, que es una biblioteca de Rust para convertir GLSL a WGSL. Incluso puede funcionar directamente en su navegador con la ayuda de WebAssembly.


Estos son los puntos finales compatibles con Naga:



Diferencias de convenciones

Texturas

Después de la migración, es posible que te encuentres con una sorpresa en forma de imágenes invertidas. Aquellos que alguna vez han portado aplicaciones de OpenGL a Direct3D (o viceversa) ya se han enfrentado a este problema clásico.


En el contexto de OpenGL y WebGL, las texturas generalmente se cargan de tal manera que el píxel inicial corresponde a la esquina inferior izquierda. Sin embargo, en la práctica, muchos desarrolladores cargan imágenes comenzando desde la esquina superior izquierda, lo que provoca el error de imagen invertida. Sin embargo, este error puede compensarse con otros factores y, en última instancia, solucionar el problema.



A diferencia de OpenGL, sistemas como Direct3D y Metal tradicionalmente utilizan la esquina superior izquierda como punto de partida para las texturas. Considerando que este enfoque parece ser el más intuitivo para muchos desarrolladores, los creadores de WebGPU decidieron seguir esta práctica.

Espacio de ventana gráfica

Si su código WebGL selecciona píxeles del búfer de fotogramas, prepárese para el hecho de que WebGPU utiliza un sistema de coordenadas diferente. Es posible que deba aplicar una operación simple "y = 1,0 - y" para corregir las coordenadas.



Recortar espacios

Cuando un desarrollador se enfrenta a un problema en el que los objetos se recortan o desaparecen antes de lo esperado, esto suele estar relacionado con diferencias en el dominio de profundidad. Existe una diferencia entre WebGL y WebGPU en cómo definen el rango de profundidad del espacio del clip. Mientras que WebGL usa un rango de -1 a 1, WebGPU usa un rango de 0 a 1, similar a otras API de gráficos como Direct3D, Metal y Vulkan. Esta decisión se tomó debido a varias ventajas de utilizar un rango de 0 a 1 que se identificaron al trabajar con otras API de gráficos.



La principal responsabilidad de transformar las posiciones de su modelo en espacio de clip recae en la matriz de proyección. La forma más sencilla de adaptar su código es asegurarse de que su matriz de proyección genere resultados en el rango de 0 a 1. Para aquellos que usan bibliotecas como gl-matrix, existe una solución simple: en lugar de usar la función perspective , puede usar perspectiveZO ; Hay funciones similares disponibles para otras operaciones matriciales.

 if (webGPU) { // Creates a matrix for a symetric perspective-view frustum // using left-handed coordinates mat4.perspectiveZO(out, Math.PI / 4, ...); } else { // Creates a matrix for a symetric perspective-view frustum // based on the default handedness and default near // and far clip planes definition. mat4.perspective(out, Math.PI / 4, …); }


Sin embargo, a veces es posible que tenga una matriz de proyección existente y no pueda cambiar su fuente. En este caso, para transformarlo en un rango de 0 a 1, puedes multiplicar previamente tu matriz de proyección por otra matriz que corrija el rango de profundidad.



Consejos y trucos de WebGPU

Ahora, analicemos algunos consejos y trucos para trabajar con WebGPU.

Minimiza la cantidad de tuberías que utilizas.

Cuantas más canalizaciones utilice, más cambios de estado tendrá y menor rendimiento; Esto puede no ser trivial, dependiendo de dónde provengan sus activos.

Cree tuberías con anticipación

Crear una canalización y usarla inmediatamente puede funcionar, pero no se recomienda. En su lugar, cree funciones que regresen inmediatamente y comiencen a trabajar en un hilo diferente. Cuando utiliza la canalización, la cola de ejecución debe esperar a que finalicen las creaciones de canalizaciones pendientes. Esto puede provocar importantes problemas de rendimiento. Para evitar esto, asegúrese de dejar algo de tiempo entre la creación de la canalización y su uso por primera vez.


O, mejor aún, ¡use las variantes create*PipelineAsync ! La promesa se resuelve cuando el oleoducto esté listo para su uso, sin ningún tipo de estancamiento.

 device.createComputePipelineAsync({ compute: { module: shaderModule, entryPoint: 'computeMain' } }).then((pipeline) => { const commandEncoder = device.createCommandEncoder(); const passEncoder = commandEncoder.beginComputePass(); passEncoder.setPipeline(pipeline); passEncoder.setBindGroup(0, bindGroup); passEncoder.dispatchWorkgroups(128); passEncoder.end(); device.queue.submit([commandEncoder.finish()]); });

Usar paquetes de renderizado

Los paquetes de renderizado son pases de renderizado reutilizables, parciales y pregrabados. Pueden contener la mayoría de los comandos de renderizado (excepto cosas como configurar la ventana gráfica) y pueden "reproducirse" como parte de una pasada de renderizado real más adelante.


 const renderPass = encoder.beginRenderPass(descriptor); renderPass.setPipeline(renderPipeline); renderPass.draw(3); renderPass.executeBundles([renderBundle]); renderPass.setPipeline(renderPipeline); renderPass.draw(3); renderPass.end();


Los paquetes de renderizado se pueden ejecutar junto con los comandos de paso de renderizado habituales. El estado de paso de renderizado se restablece a los valores predeterminados antes y después de cada ejecución del paquete. Esto se hace principalmente para reducir la sobrecarga de JavaScript del dibujo. El rendimiento de la GPU sigue siendo el mismo independientemente del enfoque.

Resumen

La transición a WebGPU significa más que simplemente cambiar las API de gráficos. También es un paso hacia el futuro de los gráficos web, ya que combina características y prácticas exitosas de varias API de gráficos. Esta migración requiere una comprensión profunda de los cambios técnicos y filosóficos, pero los beneficios son significativos.

Recursos y enlaces útiles: