paint-brush
Migrando de WebGL para WebGPUby@dmitrii
6,916
6,916

Migrando de WebGL para WebGPU

Este guia elucida a transição do WebGL para o WebGPU, abordando as principais diferenças, conceitos de alto nível e dicas práticas. À medida que o WebGPU surge como o futuro dos gráficos da web, este artigo oferece insights inestimáveis para engenheiros de software e gerentes de projeto.
featured image - Migrando de WebGL para WebGPU
Dmitrii Ivashchenko HackerNoon profile picture

Mudar para a próxima WebGPU significa mais do que apenas trocar APIs gráficas. É também um passo em direção ao futuro dos gráficos da web. Mas essa migração será melhor com preparação e compreensão – e este artigo irá prepará-lo.


Olá a todos, meu nome é Dmitrii Ivashchenko e sou engenheiro de software na MY.GAMES. Neste artigo, discutiremos as diferenças entre o WebGL e o futuro WebGPU e explicaremos como preparar seu projeto para a migração.


Visão geral do conteúdo

  1. Linha do tempo de WebGL e WebGPU

  2. O estado atual da WebGPU e o que está por vir

  3. Diferenças conceituais de alto nível

  4. Inicialização

    • WebGL: o modelo de contexto

    • WebGPU: o modelo do dispositivo

  5. Programas e pipelines

    • WebGL: Programa

    • WebGPU: pipeline

  6. Uniformes

    • Uniformes em WebGL 1

    • Uniformes em WebGL 2

    • Uniformes em WebGPU

  7. Sombreadores

    • Linguagem de Shader: GLSL vs WGSL

    • Comparação de tipos de dados

    • Estruturas

    • Declarações de Função

    • Funções integradas

    • Conversão de Shader

  8. Diferenças na Convenção

  9. Texturas

    • Espaço da janela de visualização

    • Espaços de clipe

  10. Dicas e truques da WebGPU

    • Minimize o número de pipelines usados.

    • Crie pipelines com antecedência

    • Usar RenderBundles

  11. Resumo


Linha do tempo de WebGL e WebGPU

WebGL , como muitas outras tecnologias da web, tem raízes que remontam a um passado bastante distante. Para entender a dinâmica e a motivação por trás da mudança em direção ao WebGPU, é útil primeiro dar uma rápida olhada na história do desenvolvimento do WebGL:


  • Desktop OpenGL (1993) A versão desktop do OpenGL é lançada.
  • WebGL 1.0 (2011) : Esta foi a primeira versão estável do WebGL, baseada no OpenGL ES 2.0, que foi introduzida em 2007. Forneceu aos desenvolvedores web a capacidade de usar gráficos 3D diretamente em navegadores, sem a necessidade de plug-ins adicionais.
  • WebGL 2.0 (2017) : Introduzido seis anos após a primeira versão, o WebGL 2.0 foi baseado no OpenGL ES 3.0 (2012). Esta versão trouxe consigo uma série de melhorias e novos recursos, tornando os gráficos 3D na web ainda mais poderosos.


Nos últimos anos, tem havido um aumento de interesse em novas APIs gráficas que fornecem aos desenvolvedores mais controle e flexibilidade:


  • Vulkan (2016) : Criada pelo grupo Khronos, esta API multiplataforma é a “sucessora” do OpenGL. Vulkan fornece acesso de nível inferior aos recursos de hardware gráfico, permitindo aplicativos de alto desempenho com melhor controle sobre o hardware gráfico.
  • D3D12 (2015) : Esta API foi criada pela Microsoft e é exclusiva para Windows e Xbox. D3D12 é o sucessor do D3D10/11 e fornece aos desenvolvedores um controle mais profundo sobre os recursos gráficos.
  • Metal (2014) : Criado pela Apple, Metal é uma API exclusiva para dispositivos Apple. Ele foi projetado tendo em mente o desempenho máximo em hardware Apple.


O estado atual da WebGPU e o que está por vir

Hoje, o WebGPU está disponível em diversas plataformas, como Windows, Mac e ChromeOS, por meio dos navegadores Google Chrome e Microsoft Edge, começando com a versão 113. O suporte para Linux e Android é esperado em um futuro próximo.


Aqui estão alguns dos motores que já suportam (ou oferecem suporte experimental) para WebGPU:


  • Babylon JS : suporte completo para WebGPU.
  • ThreeJS : Suporte experimental no momento.
  • PlayCanvas : Em desenvolvimento, mas com perspectivas muito promissoras.
  • Unity : O suporte inicial e experimental para WebGPU foi anunciado na versão 2023.2 alfa.
  • Cocos Creator 3.6.2 : Suporta oficialmente WebGPU, tornando-o um dos pioneiros nesta área.
  • Construct : atualmente compatível com v113+ apenas para Windows, macOS e ChromeOS.



Considerando isto, a transição para WebGPU ou pelo menos a preparação de projetos para tal transição parece ser um passo oportuno no futuro próximo.


Diferenças conceituais de alto nível

Vamos diminuir o zoom e dar uma olhada em algumas das diferenças conceituais de alto nível entre WebGL e WebGPU, começando com a inicialização.

Inicialização

Ao começar a trabalhar com APIs gráficas, um dos primeiros passos é inicializar o objeto principal para interação. Este processo difere entre WebGL e WebGPU, com algumas peculiaridades para ambos os sistemas.

WebGL: o modelo de contexto

No WebGL, esse objeto é conhecido como “contexto”, que representa essencialmente uma interface para desenhar em um elemento de tela HTML5. Obter este contexto é bastante simples:

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


O contexto do WebGL está, na verdade, vinculado a uma tela específica. Isso significa que se você precisar renderizar em diversas telas, precisará de diversos contextos.

WebGPU: o modelo do dispositivo

WebGPU introduz um novo conceito chamado “dispositivo”. Este dispositivo representa uma abstração de GPU com a qual você interagirá. O processo de inicialização é um pouco mais complexo que no WebGL, mas oferece mais flexibilidade:

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


Uma das vantagens desse modelo é que um dispositivo pode renderizar em várias telas ou até mesmo em nenhuma. Isto proporciona flexibilidade adicional; por exemplo, um dispositivo pode controlar a renderização em múltiplas janelas ou contextos.



Programas e pipelines

WebGL e WebGPU representam abordagens diferentes para gerenciar e organizar o pipeline gráfico.

WebGL: Programa

No WebGL, o foco principal está no programa shader. O programa combina shaders de vértices e fragmentos, definindo como os vértices devem ser transformados e como cada pixel deve ser colorido.

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


Passos para criar um programa em WebGL:


  1. Criando Shaders : O código fonte dos shaders é escrito e compilado.
  2. Criando Programa : Shaders compilados são anexados ao programa e depois vinculados.
  3. Usando o programa : O programa é ativado antes da renderização.
  4. Transmissão de dados : Os dados são transmitidos para o programa ativado.


Este processo permite um controle gráfico flexível, mas também pode ser complexo e propenso a erros, especialmente para projetos grandes e complexos.

WebGPU: pipeline

WebGPU introduz o conceito de "pipeline" em vez de um programa separado. Este pipeline combina não apenas shaders, mas também outras informações, que no WebGL são estabelecidas como estados. Portanto, criar um pipeline no WebGPU parece mais complexo:

 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, }], }, });


Etapas para criar um pipeline no WebGPU:


  1. Definição do shader : o código-fonte do shader é escrito e compilado, de forma semelhante à forma como é feito no WebGL.
  2. Criação de pipeline : Shaders e outros parâmetros de renderização são combinados em um pipeline.
  3. Uso do pipeline : o pipeline é ativado antes da renderização.


Enquanto o WebGL separa cada aspecto da renderização, o WebGPU tenta encapsular mais aspectos em um único objeto, tornando o sistema mais modular e flexível. Em vez de gerenciar shaders e estados de renderização separadamente, como é feito no WebGL, o WebGPU combina tudo em um objeto de pipeline. Isso torna o processo mais previsível e menos sujeito a erros:



Uniformes

Variáveis uniformes fornecem dados constantes que estão disponíveis para todas as instâncias de sombreador.

Uniformes em WebGL 1

No WebGL básico, temos a capacidade de definir variáveis uniform diretamente por meio de chamadas de 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 é simples, mas requer múltiplas chamadas de API para cada variável uniform .

Uniformes em WebGL 2

Com a chegada do WebGL 2, agora temos a capacidade de agrupar variáveis uniform em buffers. Embora você ainda possa usar sombreadores uniformes separados, uma opção melhor é agrupar uniformes diferentes em uma estrutura maior usando buffers uniformes. Em seguida, você envia todos esses dados uniformes para a GPU de uma vez, semelhante a como você pode carregar um buffer de vértice no WebGL 1. Isso tem várias vantagens de desempenho, como reduzir chamadas de API e estar mais próximo de como as GPUs modernas funcionam.


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 um buffer grande e uniforme no WebGL 2, você pode usar uma chamada de API especial conhecida como bindBufferRange . No WebGPU, existe algo semelhante chamado deslocamentos de buffer uniformes dinâmicos, onde você pode passar uma lista de deslocamentos ao chamar a API setBindGroup .



Uniformes em WebGPU

WebGPU nos oferece um método ainda melhor. Neste contexto, as variáveis uniform individuais não são mais suportadas e o trabalho é feito exclusivamente atravé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 });


As GPUs modernas preferem que os dados sejam carregados em um bloco grande, em vez de muitos blocos pequenos. Em vez de recriar e religar buffers pequenos a cada vez, considere criar um buffer grande e usar diferentes partes dele para diferentes chamadas de desenho. Essa abordagem pode aumentar significativamente o desempenho.


WebGL é mais imperativo, redefinindo o estado global a cada chamada e se esforçando para ser o mais simples possível. Já o WebGPU pretende ser mais orientado a objetos e focado na reutilização de recursos, o que leva à eficiência.


A transição do WebGL para o WebGPU pode parecer difícil devido às diferenças nos métodos. No entanto, começar com uma transição para WebGL 2 como uma etapa intermediária pode simplificar sua vida.



Sombreadores

A migração do WebGL para o WebGPU requer mudanças não apenas na API, mas também nos shaders. A especificação WGSL foi projetada para tornar essa transição suave e intuitiva, ao mesmo tempo que mantém a eficiência e o desempenho das GPUs modernas.

Linguagem de Shader: GLSL vs WGSL

WGSL foi projetado para ser uma ponte entre WebGPU e APIs gráficas nativas. Comparado ao GLSL, o WGSL parece um pouco mais detalhado, mas a estrutura permanece familiar.


Aqui está um exemplo de shader para 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); } 



Comparação de tipos de dados

A tabela abaixo mostra uma comparação dos tipos de dados básicos e matriciais em GLSL e WGSL:



A transição de GLSL para WGSL demonstra o desejo de uma digitação mais rigorosa e definição explícita de tamanhos de dados, o que pode melhorar a legibilidade do código e reduzir a probabilidade de erros.



Estruturas

A sintaxe para declarar estruturas também mudou:


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, };


A introdução de sintaxe explícita para declaração de campos em estruturas WGSL enfatiza o desejo de maior clareza e simplifica a compreensão das estruturas de dados em shaders.



Declarações de Função

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); }


A alteração da sintaxe das funções no WGSL reflete a unificação da abordagem às declarações e valores de retorno, tornando o código mais consistente e previsível.



Funções integradas

No WGSL, muitas funções GLSL integradas foram renomeadas ou substituídas. Por exemplo:



Renomear funções integradas no WGSL não apenas simplifica seus nomes, mas também as torna mais intuitivas, o que pode facilitar o processo de transição para desenvolvedores familiarizados com outras APIs gráficas.



Conversão de sombreador

Para quem está planejando converter seus projetos de WebGL para WebGPU, é importante saber que existem ferramentas para conversão automática de GLSL para WGSL, como **[Naga](https://github.com/gfx-rs/naga /)**, que é uma biblioteca Rust para converter GLSL em WGSL. Pode até funcionar diretamente no seu navegador com a ajuda do WebAssembly.


Aqui estão os endpoints suportados pelo Naga:



Diferenças na Convenção

Texturas

Após a migração, você poderá encontrar uma surpresa na forma de imagens invertidas. Aqueles que já portaram aplicativos de OpenGL para Direct3D (ou vice-versa) já enfrentaram esse problema clássico.


No contexto do OpenGL e WebGL, as texturas geralmente são carregadas de forma que o pixel inicial corresponda ao canto inferior esquerdo. No entanto, na prática, muitos desenvolvedores carregam imagens começando no canto superior esquerdo, o que leva ao erro de imagem invertida. No entanto, este erro pode ser compensado por outros fatores, acabando por nivelar o problema.



Ao contrário do OpenGL, sistemas como Direct3D e Metal tradicionalmente usam o canto superior esquerdo como ponto de partida para texturas. Considerando que esta abordagem parece ser a mais intuitiva para muitos desenvolvedores, os criadores do WebGPU decidiram seguir esta prática.

Espaço da janela de visualização

Se o seu código WebGL selecionar pixels do buffer de quadros, esteja preparado para o fato de que o WebGPU usa um sistema de coordenadas diferente. Pode ser necessário aplicar uma operação simples "y = 1,0 - y" para corrigir as coordenadas.



Espaços de clipe

Quando um desenvolvedor enfrenta um problema em que os objetos são cortados ou desaparecem antes do esperado, isso geralmente está relacionado a diferenças no domínio de profundidade. Há uma diferença entre WebGL e WebGPU na forma como eles definem a faixa de profundidade do clip space. Enquanto o WebGL usa um intervalo de -1 a 1, o WebGPU usa um intervalo de 0 a 1, semelhante a outras APIs gráficas, como Direct3D, Metal e Vulkan. Esta decisão foi tomada devido a diversas vantagens de usar um intervalo de 0 a 1 que foram identificadas ao trabalhar com outras APIs gráficas.



A principal responsabilidade por transformar as posições do seu modelo em clip space é da matriz de projeção. A maneira mais simples de adaptar seu código é garantir que a saída da matriz de projeção resulte na faixa de 0 a 1. Para quem usa bibliotecas como gl-matrix, existe uma solução simples: em vez de usar a função perspective , você pode usar perspectiveZO ; funções semelhantes estão disponíveis para outras operações matriciais.

 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, …); }


No entanto, às vezes você pode ter uma matriz de projeção existente e não pode alterar sua origem. Neste caso, para transformá-lo em um intervalo de 0 a 1, você pode pré-multiplicar sua matriz de projeção por outra matriz que corrija o intervalo de profundidade.



Dicas e truques da WebGPU

Agora, vamos discutir algumas dicas e truques para trabalhar com WebGPU.

Minimize o número de pipelines que você usa.

Quanto mais pipelines você usar, mais alternância de estado terá e menos desempenho; isso pode não ser trivial, dependendo de onde vêm seus ativos.

Crie pipelines com antecedência

Criar um pipeline e usá-lo imediatamente pode funcionar, mas não é recomendado. Em vez disso, crie funções que retornem imediatamente e comecem a trabalhar em um thread diferente. Quando você usa o pipeline, a fila de execução precisa aguardar a conclusão das criações pendentes do pipeline. Isso pode resultar em problemas significativos de desempenho. Para evitar isso, reserve algum tempo entre a criação do pipeline e a primeira utilização dele.


Ou, melhor ainda, use as variantes create*PipelineAsync ! A promessa é resolvida quando o pipeline está pronto para uso, sem qualquer paralisação.

 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 RenderBundles

Os pacotes de renderização são passagens de renderização pré-gravadas, parciais e reutilizáveis. Eles podem conter a maioria dos comandos de renderização (exceto coisas como configurar a janela de visualização) e podem ser "repetidos" como parte de uma passagem de renderização real posteriormente.


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


Os pacotes de renderização podem ser executados junto com comandos regulares de passagem de renderização. O estado de passagem de renderização é redefinido para os padrões antes e depois de cada execução do pacote configurável. Isso é feito principalmente para reduzir a sobrecarga do JavaScript no desenho. O desempenho da GPU permanece o mesmo, independentemente da abordagem.

Resumo

A transição para WebGPU significa mais do que apenas trocar APIs gráficas. É também um passo em direção ao futuro dos gráficos da web, combinando recursos e práticas bem-sucedidas de várias APIs gráficas. Esta migração requer uma compreensão profunda das mudanças técnicas e filosóficas, mas os benefícios são significativos.

Recursos e links úteis: