O processamento de imagens é fácil na WebGL. Quão fácil? Leia abaixo.
Esta é uma continuação de Fundamentos da WebGL2. Se você não leu, eu sugiro ir lá primeiro.
Para desenhar imagens na WebGL precisamos usar texturas. Da mesma forma que a WebGL espera coordenadas de clipspace ao renderizar em vez de pixels, a WebGL geralmente espera coordenadas de textura ao ler uma textura. As coordenadas da textura variam de 0,0 a 1,0 independentemente das dimensões da textura.
A WebGL2 adiciona a capacidade de ler uma textura usando coordenadas de pixels também. Qual caminho é melhor depende de você. Sinto que é mais comum usar as coordenadas de textura do que as coordenadas de pixels.
Uma vez que estamos apenas desenhando um único retângulo (bem, 2 triângulos), precisamos dizer a WebGL qual lugar na textura em que cada ponto do retângulo corresponde. Vamos passar esta informação do vertex para o fragmento shader usando um tipo especial de variável chamado de ‘varying’. É chamado de varying porque isso varia. WebGL irá interpolar os valores que fornecemos no vertex shader pois desenha cada pixel usando o fragmento shader.
Usando o vertex shader do final da publicação anterior precisamos adicionar um atributo para passar em coordenadas de textura e depois passar para o fragmento shader.
...
+in vec2 a_texCoord;
...
+out vec2 v_texCoord;
void main() {
...
+ // passe o texCoord para o fragmento shader
+ // O GPU irá interpolar esse valor entre pontos
+ v_texCoord = a_texCoord;
}
Então, fornecemos um fragmento shader para procurar cores da textura.
#version 300 es
precision highp float;
// nossa textura
uniform sampler2D u_image;
// O texCoords passou do vertex shader.
in vec2 v_texCoord;
// precisamos declarar uma saída para o fragmento shader
out vec4 outColor;
void main() {
// Procure uma cor da textura.
outColor = texture(u_image, v_texCoord);
}
Finalmente, precisamos carregar uma imagem, criar uma textura e copiar a imagem para a textura. Como estamos em imagens de um navegador, carregamos de forma assíncrona, então devemos reorganizar nosso código um pouco para aguardar o carregamento da textura. Uma vez que carregada, vamos desenhá-la.
+function main() {
+ var image = new Image();
+ image.src = "https://someimage/on/our/server"; // DEVE SER MESMO DOMÍNIO!!!
+ image.onload = function() {
+ render(image);
+ }
+}
function render(image) {
...
// procure onde os dados do vértice precisam ir.
var positionAttributeLocation = gl.getAttribLocation(program, "a_position");
+ var texCoordAttributeLocation = gl.getAttribLocation(program, "a_texCoord");
// uniforms de pesquisa
var resolutionLocation = gl.getUniformLocation(program, "u_resolution");
+ var imageLocation = gl.getUniformLocation(program, "u_image");
...
+ // fornecer coordenadas de textura para o retângulo.
+ var texCoordBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
+ 0.0, 0.0,
+ 1.0, 0.0,
+ 0.0, 1.0,
+ 0.0, 1.0,
+ 1.0, 0.0,
+ 1.0, 1.0]), gl.STATIC_DRAW);
+ gl.enableVertexAttribArray(texCoordAttributeLocation);
+ var size = 2; // 2 componentes por iteração
+ var type = gl.FLOAT; // os dados são floats de 32bit
+ var normalize = false; // não normalize os dados
+ var stride = 0; // 0 = mover para o tamanho * sizeof (tipo) cada iteração para obter a próxima posição
+ var offset = 0; // comece no início do buffer
+ gl.vertexAttribPointer(
+ texCoordAttributeLocation, size, type, normalize, stride, offset)
+
+ // Create a texture.
+ var texture = gl.createTexture();
+
+ // faça da unidade 0 a unidade de textura ativa
+ // (ie, the unit all other texture commands will affect
+ gl.activeTexture(gl.TEXTURE0 + 0);
+
+ // Vincule a unidade de textura 0' ponto de ligação 2D
+ gl.bindTexture(gl.TEXTURE_2D, texture);
+
+ // Defina os parâmetros para que não precisemos de mips e por isso não estamos filtrando
+ // e não repetindo
+ 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);
+
+ // Carregue a imagem para a textura.
+ var mipLevel = 0; // the largest mip
+ var internalFormat = gl.RGBA; // format we want in the texture
+ var srcFormat = gl.RGBA; // format of data we are supplying
+ var srcType = gl.UNSIGNED_BYTE // type of data we are supplying
+ gl.texImage2D(gl.TEXTURE_2D,
+ mipLevel,
+ internalFormat,
+ srcFormat,
+ srcType,
+ image);
...
// Diga para usar nosso programa (par de shaders)
gl.useProgram(program);
// Passe na resolução da tela para que possamos converter
// pixels para clipspace no shader
gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height);
+ // Diga ao shader para obter a textura da unidade de textura 0
+ gl.uniform1i(imageLocation, 0);
+ // Vincule o buffer de posição para que gl.bufferData seja chamado
+ // em setRectangle para colocar dados no buffer de posição
+ gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
+
+ // Defina um retângulo do mesmo tamanho que a imagem.
+ setRectangle(gl, 0, 0, image.width, image.height);
}
E aqui está a imagem renderizada na WebGL.
Não muito emocionante, então vamos manipular essa imagem. Que tal simplesmente trocar vermelho e azul?
...
outColor = texture(u_image, v_texCoord).bgra;
...
E agora vermelho e azul foram trocados.
E se quisermos processar imagens que realmente olhem para outros
pixels? Uma vez que a WebGL faz referência a texturas em coordenadas de textura que
variam de 0,0 a 1,0, podemos calcular o quanto move 1 pixel
com o simples math onePixel = 1.0 / textureSize
.
Aqui está um fragmento que mede os pixels esquerdos e direitos de cada pixel na textura.
#version 300 es
// fragmentos shaders não têm uma precisão padrão, então precisamos
// para escolher um. O médio é um bom padrão. Significa "precisão média"
precision highp float;
// nossa textura
uniform sampler2D u_image;
// o texCoords passou do vertex shader.
in vec2 v_texCoord;
// precisamos declarar uma saída para o fragmento shader
out vec4 outColor;
void main() {
+ vec2 onePixel = vec2(1) / vec2(textureSize(u_image, 0));
+
+ // média dos pixels esquerdo, médio e direito.
+ outColor = (
+ texture(u_image, v_texCoord) +
+ texture(u_image, v_texCoord + vec2( onePixel.x, 0.0)) +
+ texture(u_image, v_texCoord + vec2(-onePixel.x, 0.0))) / 3.0;
}
Compare com a imagem não borrada acima.
Agora que sabemos como fazer referência a outros pixels, vamos usar um convolution kernel para fazer varios processamentos de imagem comum. Neste caso, usaremos um kernel 3x3. Um convolution kernel é apenas uma matriz de 3x3, onde cada entrada na matriz representa o quanto para multiplicar os 8 pixels ao redor do pixel que estamos renderizando. Em seguida, dividimos o resultado pelo peso do kernel (a soma de todos os valores no kernel) ou 1,0, o que for maior. Aqui está um artigo muito bom sobre isso. E aqui está outro artigo que mostra algum código real se você escrevesse isso manualmente em C ++.
No nosso caso, vamos fazer isso funcionar no shader, então aqui está o novo fragment shader.
#version 300 es
// fragmentos shaders não têm uma precisão padrão, então precisamos
// para escolher um. O médio é um bom padrão. Significa "precisão média"
precision highp float;
// nossa textura
uniform sampler2D u_image;
// os dados do convolution kernal
uniform float u_kernel[9];
uniform float u_kernelWeight;
// o texCoords passou do vertex shader.
in vec2 v_texCoord;
// precisamos declarar uma saída para o fragmento shader
out vec4 outColor;
void main() {
vec2 onePixel = vec2(1) / vec2(textureSize(u_image, 0));
vec4 colorSum =
texture(u_image, v_texCoord + onePixel * vec2(-1, -1)) * u_kernel[0] +
texture(u_image, v_texCoord + onePixel * vec2( 0, -1)) * u_kernel[1] +
texture(u_image, v_texCoord + onePixel * vec2( 1, -1)) * u_kernel[2] +
texture(u_image, v_texCoord + onePixel * vec2(-1, 0)) * u_kernel[3] +
texture(u_image, v_texCoord + onePixel * vec2( 0, 0)) * u_kernel[4] +
texture(u_image, v_texCoord + onePixel * vec2( 1, 0)) * u_kernel[5] +
texture(u_image, v_texCoord + onePixel * vec2(-1, 1)) * u_kernel[6] +
texture(u_image, v_texCoord + onePixel * vec2( 0, 1)) * u_kernel[7] +
texture(u_image, v_texCoord + onePixel * vec2( 1, 1)) * u_kernel[8] ;
outColor = vec4((colorSum / u_kernelWeight).rgb, 1);
}
Em JavaScript, precisamos fornecer um convolution kernel e seu peso
function computeKernelWeight(kernel) {
var weight = kernel.reduce(function(prev, curr) {
return prev + curr;
});
return weight <= 0 ? 1 : weight;
}
...
var kernelLocation = gl.getUniformLocation(program, "u_kernel[0]");
var kernelWeightLocation = gl.getUniformLocation(program, "u_kernelWeight");
...
var edgeDetectKernel = [
-1, -1, -1,
-1, 8, -1,
-1, -1, -1
];
// definir o kernel e seu peso
gl.uniform1fv(kernelLocation, edgeDetectKernel);
gl.uniform1f(kernelWeightLocation, computeKernelWeight(edgeDetectKernel));
...
Aí está… Use a lista suspensa para selecionar diferentes kernels.
Espero que este artigo tenha o convencido de que o processamento de imagens na WebGL é bastante simples. Em seguida, falarei como aplicar mais de um efeito à imagem.