Índice

WebGL2Fundamentals.org

Fix, Fork, Contribute

Processamento de Imagem Continuada WebGL2

Este artigo é uma continuação do Processamento de imagem WebGL2. Se você não leu, eu sugiro que você comece por lá.

A próxima pergunta mais óbvia para o processamento de imagens é, como aplicar múltiplos efeitos?

Bem, você poderia tentar gerar shaders em tempo real. Fornecer uma interface que permite ao usuário selecionar os efeitos que ele quer usar, em seguida, gerar um shader que faz todos os efeitos. Isso nem sempre é possível, embora essa técnica seja usada frequentemente para criar efeitos para gráficos em tempo real.

Uma maneira mais flexível é usar mais 2 texturas work e renderizar a cada textura por sua vez, fazendo um ping-pong para frente e para trás e aplicando o próximo efeito a cada vez.

Imagem original -> [Blur]        -> Textura 1
Textura 1      -> [Sharpen]     -> Textura 2
Textura 2      -> [Edge Detect] -> Textura 1
Textura 1      -> [Blur]        -> Textura 2
Textura 2      -> [Normal]      -> Tela

Para fazer isso precisamos criar framesbuffers. Na WebGL e OpenGL, um Framebuffer é realmente um nome ruim. Uma WebGL / OpenGL Framebuffer é realmente apenas uma lista de anexos e não realmente um buffer de qualquer tipo. Mas, ao anexar uma textura a um framebuffer, podemos renderizar essa textura.

Primeiro, vamos transformar o antigo código de criação de textura em uma função

  function createAndSetupTexture(gl) {
    var texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);

    // Configure a textura para que possamos renderizar qualquer imagem de tamanho e, portanto estamos
    // trabalhando com pixels.
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

    return texture;
  }

  // Crie uma textura e coloque a imagem nela.
  var originalImageTexture = createAndSetupTexture(gl);

  // Carregue a imagem para a textura.
  var mipLevel = 0;               // o maior mip
  var internalFormat = gl.RGBA;   // formato que queremos na textura
  var srcFormat = gl.RGBA;        // formato de dados que estamos fornecendo
  var srcType = gl.UNSIGNED_BYTE  // tipo de dados que estamos fornecendo
  gl.texImage2D(gl.TEXTURE_2D,
                mipLevel,
                internalFormat,
                srcFormat,
                srcType,
                image);

E agora vamos usar essa função para criar mais 2 texturas e anexá-las a 2 framebuffers.

  // crie 2 texturas e anexe-as a framesbuffers.
  var textures = [];
  var framebuffers = [];
  for (var ii = 0; ii < 2; ++ii) {
    var texture = createAndSetupTexture(gl);
    textures.push(texture);

    // faça a textura do mesmo tamanho que a imagem
    var mipLevel = 0;               // o maior mip
    var internalFormat = gl.RGBA;   // formato que queremos na textura
    var border = 0;                 // deve ser 0
    var srcFormat = gl.RGBA;        // formato de dados que estamos fornecendo
    var srcType = gl.UNSIGNED_BYTE  // tipo de dados que estamos fornecendo
    var data = null;                // sem dados = crie uma textura em branco
    gl.texImage2D(
        gl.TEXTURE_2D, mipLevel, internalFormat, image.width, image.height, border,
        srcFormat, srcType, data);

    // Crie um framebuffer
    var fbo = gl.createFramebuffer();
    framebuffers.push(fbo);
    gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);

    // Anexe uma textura a ele.
    var attachmentPoint = gl.COLOR_ATTACHMENT0;
    gl.framebufferTexture2D(
        gl.FRAMEBUFFER, attachmentPoint, gl.TEXTURE_2D, texture, mipLevel);
  }

Agora vamos fazer um conjunto de kernels e, em seguida, uma lista deles para se inscrever.

  // Definir vários kernels convolution
  var kernels = {
    normal: [
      0, 0, 0,
      0, 1, 0,
      0, 0, 0
    ],
    gaussianBlur: [
      0.045, 0.122, 0.045,
      0.122, 0.332, 0.122,
      0.045, 0.122, 0.045
    ],
    unsharpen: [
      -1, -1, -1,
      -1,  9, -1,
      -1, -1, -1
    ],
    emboss: [
       -2, -1,  0,
       -1,  1,  1,
        0,  1,  2
    ]
  };

  // Lista de efeitos a serem aplicados.
  var effectsToApply = [
    "gaussianBlur",
    "emboss",
    "gaussianBlur",
    "unsharpen"
  ];

E, finalmente, vamos aplicar a cada um, o ping ponging de qual texturas estamos renderizando também

  function drawEffects() {
    // Diga para usar nosso programa (par de shaders)
    gl.useProgram(program);

    // Vincule o conjunto de atributo/buffer que queremos.
    gl.bindVertexArray(vao);

    // comece com a imagem original na unidade 0
    gl.activeTexture(gl.TEXTURE0 + 0);
    gl.bindTexture(gl.TEXTURE_2D, originalImageTexture);

    // Diga ao shader para obter a textura da unidade de textura 0
    gl.uniform1i(imageLocation, 0);

    // não toque as imagens enquanto desenha com as texturas
    gl.uniform1f(flipYLocation, 1);

    // faça um loop para cada efeito que queremos aplicar.
    var count = 0;
    for (var ii = 0; ii < tbody.rows.length; ++ii) {
      var checkbox = tbody.rows[ii].firstChild.firstChild;
      if (checkbox.checked) {
        // Configuração para desenhar em um dos framebuffers.
        setFramebuffer(framebuffers[count % 2], image.width, image.height);

        drawWithKernel(checkbox.value);

        // para o próximo desenho, use a textura que acabamos de renderizar.
        gl.bindTexture(gl.TEXTURE_2D, textures[count % 2]);

        // contagem de incremento, então usamos a outra textura na próxima vez.
        ++count;
      }
    }

    // finalmente, desenhe o resultado para a tela.
    gl.uniform1f(flipYLocation, -1);  // precisa virar para tela

    setFramebuffer(null, gl.canvas.width, gl.canvas.height);

    drawWithKernel("normal");
  }

  function setFramebuffer(fbo, width, height) {
    // faça deste o framebuffer para o qual estamos renderizando.
    gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);

    // Diga ao shader a resolução do framebuffer.
    gl.uniform2f(resolutionLocation, width, height);

    // Diga WebGL como converter do clip space para pixels
    gl.viewport(0, 0, width, height);
  }

  function drawWithKernel(name) {
    // definir a kernel e seu peso
    gl.uniform1fv(kernelLocation, kernels[name]);
    gl.uniform1f(kernelWeightLocation, computeKernelWeight(kernels[name]));

    // Desenhe o retângulo.
    var primitiveType = gl.TRIANGLES;
    var offset = 0;
    var count = 6;
    gl.drawArrays(primitiveType, offset, count);
  }

Aqui está uma versão de trabalho com uma interface um pouco mais flexível. Verifique os efeitos para ativá-los. Arraste os efeitos para reordenar como eles são aplicados.

Algumas coisas que devo passar.

Chamar gl.bindFramebuffer com null diz para a WebGL que você deseja renderizar na tela em vez de em um de seus framebuffers.

Os framebuffers podem ou não funcionar, dependendo de quais anexos você colocou sobre eles. Há uma lista de quais tipos e combinações de anexos devem funcionar sempre. O utilizado aqui, um RGBA/UNSIGNED_BYTE textura atribuída ao COLOR_ATTACHMENT0 como ponto de ligação, é sempre suposto irá funcionar. Formatos de textura mais exótica ou combinações de anexos podem não funcionar. Nesse caso, você deve vincular o framebuffer e depois chamar gl.checkFramebufferStatus e veja se ele retorna gl.FRAMEBUFFER_COMPLETE. Se sim, você está pronto para ir. Caso contrário, você precisará dizer ao usuário que recorra a outra coisa. Felizmente a WebGL2 suporta muitos formatos e combinações.

A WebGL tem que converter de clipspace novamente em pixels. Ela faz isso com base nas configurações de gl.viewport. Uma vez que os framebuffers em que estamos renderizando são de tamanho diferente da tela que precisamos para configurar a viewport adequadamente, isso depende se estamos renderizando uma textura ou na tela.

Finalmente, no exemplo original nós invertemos a coordenada Y ao renderizar porque a WebGL exibe a tela com 0,0 sendo o canto inferior esquerdo em vez do mais tradicional do 2D no superior esquerda. Isso não é necessário ao renderizar um framebuffer. Como o framebuffer nunca é exibido, qual parte é superior e inferior é irrelevante. Tudo o que importa é que o pixel 0,0 no framebuffer correspondam a 0,0 em nossos cálculos. Para lidar com isso, eu tornei possível definir se deve virar ou não, adicionando mais uma entrada uniforme na chamada do shader u_flipY.

...
+uniform float u_flipY;
...

void main() {
  ...
+   gl_Position = vec4(clipSpace * vec2(1, u_flipY), 0, 1);
  ...
}

E então podemos configurá-lo quando renderizamos com

  ...
+  var flipYLocation = gl.getUniformLocation(program, "u_flipY");

  ...

+  // não virar
+  gl.uniform1f(flipYLocation, 1);

  ...

+  // virar
+  gl.uniform1f(flipYLocation, -1);

Mantive esse exemplo simples usando um único programa GLSL que pode alcançar múltiplos efeitos. Se você quisesse fazer tudo no processamento de imagem, você provavelmente precisaria de muitos programas GLSL. Um programa para ajuste de tonalidade, saturação e luminância. Outro para brilho e contraste. Um para inverter, outro para ajustar níveis, etc. Você precisaria alterar o código para trocar programas GLSL e atualizar os parâmetros para esse programa específico. Eu considerei em escrever esse exemplo, mas é melhor como um exercício para o leitor, porque vários programas GLSL, cada um com seu próprio parâmetro, precisam provavelmente de uma grande refatoração para evitar que tudo se torne uma grande bagunça.

Espero que este e os exemplos anteriores tenham tornado o WebGL um pouco mais acessível e espero que começar com o 2D ajuda a tornar o WebGL um pouco mais fácil de entender. Se eu encontrar tempo, vou tentar escrever mais alguns artigos sobre como fazer em 3D, bem como mais detalhes sobre O que o WebGL realmente está fazendo sob o capô. Para um próximo passo, aprenda como usar 2 ou mais texturas.

Problemas/Bug? Pergunte no github.
comments powered by Disqus