Mapeamento de texturas no WebGL =============================== Agora vamos aplicar o que vimos na aula passada para mapear texturas (imagens) para a superfície de objetos. Para variar, vamos começar com um exemplo bem simples, usando uma textura procedimental. Textura procedimental --------------------- Uma `textura procedimental `__ é uma textura criada por uma função matemática (ou algoritmo), ao invés de ser carregada de uma imagem. Vamos usar o trecho de código abaixo para criar um tabuleiro de xadrez com :math:`8 \times 8` casas. Esse tabuleiro é armazenado em um array do tipo ``Uint8`` com :math:`128 \times 128` pixels, sendo que cada pixel contém uma cor RGBA. .. code:: JavaScript // textura é um tabuleiro de xadrez 8x8 const TEX_LINS = 8; const TEX_COLS = 8; const TEX_LADO = 128; // lado da imagem textura const TEX_COR_SIZE = 4; // RGBA // cria array de textura. Pixels costumam ser do tipo Uint8 var gaTextura = new Uint8Array( TEX_COR_SIZE * TEX_LADO * TEX_LADO); // varre e carrega o array for (var i = 0, ind = 0; i`__. .. O outro método envolve fatiar o polígono em pedaços poligonais suficientemente pequenos, de modo que dentro de cada pedaço a quantidade de distorção devido à perspectiva seja pequena. Lembre-se de que, com o sombreamento de Gouraud, muitas vezes era necessário subdividir grandes polígonos em pequenos pedaços para um cálculo preciso da iluminação. Se você já fez isso, a distorção de perspectiva devido ao mapeamento de textura pode não ser um problema significativo para você. Um segundo problema tem a ver com algo chamado *aliasing*. Lembre-se que dissemos que depois de determinar o fragmento do espaço de textura no qual o pixel se projeta, devemos calcular a média das cores dos texels neste fragmento. O procedimento acima considera apenas um único ponto no espaço de textura e não tem média. Em situações em que o pixel corresponde a um ponto muito distante e, portanto, cobre uma grande região no espaço de textura, isso pode produzir resultados com uma aparência estranha porque a cor de todo o pixel é determinada inteiramente por um único ponto no espaço de textura que correspondem (digamos) às coordenadas do centro do pixel. Lidar com *aliasing* em geral é um problema que é tipicamente estudado na área de processamento de sinais. O WebGL aplica um método simples para lidar com esse problema, chamado de **mipmapping**. A sigla “mip” vem da frase latina *multum in parvo*, que significa “muito em pouco”. A ideia por trás do mipmapping é gerar uma série de imagens de textura em níveis de resolução decrescentes. Por exemplo, se você começou originalmente com uma imagem de :math:`128 \times 128`, um mipmap consistiria nessa imagem junto com uma imagem de :math:`64 \times 64`, uma imagem de :math:`32 \times 32` etc.. Cada pixel da imagem de :math:`64 \times 64` representa a média de um bloco de :math:`2 \times 2` do original. Cada pixel da imagem :math:`32 \times 32` representa a média de um bloco :math:`4 \times 4`` do original e assim por diante. .. (veja a Fig. 51). Quando fazemos o WebGL aplicar o mapeamento de textura a um pixel que se sobrepõe a muitos texels, ele determina o mipmap na hierarquia que está no nível mais próximo de resolução e usa o correspondente valor médio de pixel dessa imagem do mipmap, resultando em uma imagem mais suave. A seguir, veremos como configurar o mipmap e outros elementos para realizar o mapeamento de texturas usando WebGL. .. (veja a Fig. 50(b)). Como configurar um mapa de textura no WebGL? ---------------------------------------------- O WebGL suporta um mecanismo bastante geral para mapeamento de textura mas o processo envolve um grande número de opções diferentes. Você deve consultar a documentação do WebGL para obter informações mais detalhadas. Vamos nos limitar a descrever alguns parâmetros mais comuns para implementar programas gráficos usando texturas, como ilustrado no trecho de código abaixo com a função ``configureTextura()``. .. code:: JavaScript function configureTextura(img) { var texture = gl.createTexture(); // cria uma textura gl.activeTexture(gl.TEXTURE0); // ativa a unidade TEXTURE0 gl.bindTexture(gl.TEXTURE_2D, texture); // conecta a textura a unidade ativa gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, TEX_LADO, TEX_LADO, 0, gl.RGBA, gl.UNSIGNED_BYTE, img); // carrega a textura gl.generateMipmap(gl.TEXTURE_2D); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); } Essa função recebe uma imagem ``img``, no caso desse exemplo, a imagem do tabuleiro de xadrez. Um buffer de textura é criado por ``gl.createTexture()`` e associado a unidade ``gl.TEXTURE0``. O número de unidades de textura disponíveis depende da sua plataforma, mas é `ao menos 8 `__. O número máximo disponível pode ser conhecido pela constante ``gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS``. De forma semelhante ao buffer de vértices, a textura se torna ativa pelo comando ``gl.bindTexture()``, usando para isso a textura que acabamos de criar. Nesse exemplo estamos criando uma textura 2D, mas ela poderia ainda ser 1D ou 3D. Os comandos seguintes ao ``bindTexture`` são aplicados à textura ativa. O comando seguinte, ``gl.texImage2D()`` passa a textura para a GPU e recebe vários parâmetros para que possa ser convertido para seu formato interno. Vamos discutir alguns desses parâmetros. A sintaxe usada para esse comando é: glTexImage2d(gl.TEXTURE_2D, nível, formato interno, largura, altura, borda, formato, tipo, imagem); Em nosso exemplo, a *imagem* ``img`` é um array composto por elementos do *tipo* ``gl.UNSIGNED_BYTE`` (um byte sem sinal) e cada pixel está no *formato* interno ``gl.RGBA``. Essa imagem possui *largura* e *altura* ``TEX_LADO`` e não tem *borda* (borda = 0). Estamos armazenando a resolução de nível mais alto. Outros níveis de resolução são usados para implementar o processo de mipmap. Normalmente, o parâmetro de *nível* será 0 (nível = 0). O *formato* dos texels também é ``gl.RGBA``. .. A chamada ``gl.generateMipmap(gl.TEXTURE_2D)`` cria o mipmap com todos os níveis menores de resolução. A definição de parâmetros de textura é realizada usando o comando `gl.texParamenteri `__ (para inteiros, mas pode ser também ``gl.texParamenterf`` para reais). UM parâmetro útil que determina como o arredondamento é realizado durante a ampliação (``gl.TEXTURE_MAG_FILTER`` quando um pixel de tela é menor que o pixel de textura correspondente) e redução (``gl.TEXTURE_MIN_FILTER`` quando um pixel de tela é maior que o pixel de textura correspondente). A opção mais simples, mas não a mais bonita, em cada caso, é usar apenas o pixel mais próximo na textura usando ``gl.NEAREST``: gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); No caso de redução, quando muitos texels precisam ser convertidos para 1 pixel, podemos aplicar os seguintes 6 modos de filtragem para ``gl.TEXTURE_MIN_FILTER``: * NEAREST : escolhe 1 pixel do maior nível do mipmap; * LINEAR : escolhe 4 pixels do maior nível do mipmap e os mistura linearmente; * NEAREST_MIPMAP_NEAREST : escolhe o melhor nível (MIPMAP_NEAREST) e escolhe 1 pixel desse nível; * LINEAR_MIPMAP_NEAREST : escolhe o melhor nível e mistura 4 pixels desse nível; * NEAREST_MIPMAP_LINEAR : escolhe os 2 melhores níveis (MIPMAP_LINEAR), escolhe 1 pixel de cada e os mistura (valor default); * LINEAR_MIPMAP_LINEAR : escolhe os 2 melhores níveis, escolhe 4 de cada e os mistura. Na magnificação, o valor default de ``gl.TEXTURE_MAG_FILTER`` no WebGL é ``gl.LINEAR``. Outro parâmetro comum a ser definido serve para especificar se uma textura deve ser repetida ou não. As seguintes opções podem ser usadas. gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); // default gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); Essas opções determinam o que acontece se o parâmetro s da coordenada de textura for menor que 0 ou maior que 1. Se isso nunca acontecer, você não precisa se preocupar com esta opção. A opção ``gl.REPEAT`` é usada por default pelo WebGL e faz com que a textura seja enrolada repetidamente usando o valor fracionário de s. Assim, s = 1.234 e s = 99.234 são ambos equivalentes a s = 0.234. Isso pode ser definido independentemente para o parâmetro t da coordenada de textura usando ``gl.TEXTURE_WRAP_T`` ao invés de ``gl.TEXTURE_WRAP_S``. Usando a opção ``gl.CLAMP_TO_EDGE`` os valores de s que são negativos são tratados como se fossem 0, e os valores de s que excedem 1 são tratados como se fossem 1. Já a opção ``g.MIRRORED_REPEAT`` alterna valores da textura com valores espelhados. O código completo desse exemplo está disponível no `JSitor `__ e também pode ser visto logo abaixo. Modifique os valores da textura, como número de linhas e colunas da textura, para ver seu resultado. .. raw:: html .. 1o exemplo jsitor Combinando textura com cores e iluminação ----------------------------------------- Como as cores das texturas são combinadas com as cores dos objetos? Nosso exemplo anterior simplesmente torna a cor do pixel igual à cor da textura. Essa forma pode ser adotada para pintar texturas que já estão pré-iluminadas, o que significa que a iluminação já foi aplicada. Exemplos incluem skyboxes (usadas para pintar o céu por exemplo) e iluminação pré-computada para o teto e as paredes de uma sala. Uma outra forma comum é modular a iluminação usando a textura. Dessa forma, a cor resultante de cada pixel se torna o produto da cor do pixel (que pode ou não usar um modelo de iluminação como a de Phong) pela cor da textura. O programa a seguir mostra como o nosso exercício anterior pode ser estendido para combinar a textura com cores distintas em cada vértice. O trecho de código a seguir ilustra os shaders para receber um buffer de cores. .. code:: JavaScript var gVertexShaderSrc = `#version 300 es // buffers de entrada in vec3 aPosition; in vec2 aTexCoord; in vec4 aColor; uniform mat4 uModelView; uniform mat4 uPerspective; out vec2 vTexCoord; out vec4 vColor; void main() { gl_Position = uPerspective * uModelView * vec4(aPosition, 1); vTexCoord = aTexCoord; vColor = aColor; } `; var gFragmentShaderSrc = `#version 300 es precision highp float; in vec4 vColor; in vec2 vTexCoord; uniform sampler2D uTextureMap; out vec4 outColor; void main() { outColor = vColor * texture(uTextureMap, vTexCoord); } `; O código completo desse exemplo está disponível no `JSitor `__ e pode ser visto logo a seguir. .. raw:: html .. admonition:: Exercício Estenda esse código para incluir também efeitos de iluminação segundo o modelo de Phong. .. 2o exemplo jsitor Usando imagens de arquivos como textura ---------------------------------------- Um problema de utilizar arquivos de imagens que estão em alguma 'nuvem' é que seu carregamento ocorre de forma assíncrona. Assim, seu programa gráfico deve esperar até que o navegador faça o download da imagem. Há duas soluções comuns para resolver esse problema. A primeira é fazer o programa ficar esperando até que tudo tenha sido trazido da nuvem para iniciar a renderização. Uma solução alternativa é usar uma textura 'falsa' até que a imagem seja descarregada da nuvem. Assim a renderização pode começar imediatamente e, quando a imagem estiver disponível, basta passar a imagem verdadeira para ser renderizada. Isso pode ser feito pelo trecho de código abaixo: .. code:: JavaScript function configureTexturaDaURL( url ) { // cria a textura var texture = gl.createTexture(); // seleciona a unidade TEXTURE0 gl.activeTexture(gl.TEXTURE0); // ativa a textura gl.bindTexture(gl.TEXTURE_2D, texture); // Carrega uma textura de um pixel 1x1 vermelho, temporariamente gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255])); // Carraga a imagem da URL: // veja https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/Image var img = new Image(); // cria um bitmap img.src = url; img.crossOrigin = "anonymous"; // espera carregar. img.addEventListener('load', function() { console.log("Carregou imagem", img.width, img.height); // depois de carregar, copiar para a textura gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, img.width, img.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, img); gl.generateMipmap(gl.TEXTURE_2D); // experimente usar outros filtros removendo o comentário da linha abaixo. //gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); } ); return img; }; Para esse exemplo vamos considerar a seguinte figura: .. figure:: https://upload.wikimedia.org/wikipedia/commons/thumb/a/a5/Flower_poster_2.jpg/1200px-Flower_poster_2.jpg :width: 400 :align: center :alt: Imagem contendo várias flores que vamos usar como textura. Imagem contendo várias flores que vamos usar como textura. Para usar essa imagem como textura vamos escolher os cantos para selecionar apenas as primeiras 9 flores, como ilustrado no seguinte trecho de código: .. code:: JavaScript const URL = "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a5/Flower_poster_2.jpg/1200px-Flower_poster_2.jpg" var gaTexCoords = []; var vTextura = [ // cantos escolhidos para recortar a parte desejada vec2(0.05, 0.05), vec2(0.05, 0.75), vec2(0.95, 0.75), vec2(0.95, 0.05) ]; O código completo desse exemplo está disponível no `JSitor `__ e pode ser visto logo a seguir. .. raw:: html .. Uma vez que as inicializações estejam completas, você está pronto para começar a desenhar. Primeiro, vincule a textura desejada (ou seja, torne-a a textura ativa), defina os parâmetros de textura e habilite a texturização. Em seguida, comece a desenhar seus objetos texturizados. Para cada vértice desenhado, certifique-se de especificar as coordenadas de textura associadas a este vértice, antes de emitir o comando glVertex. Se a iluminação estiver habilitada, você também deve fornecer a superfície normal. Um exemplo genérico é mostrado no bloco de código abaixo. .. admonition:: Texturas devem ser carregadas de domínios seguros. O WebGL exige que as texturas sejam carregadas de contextos seguros e não permite usar texturas carregadas de ``file://``. Isso significa que você precisa usar um servidor web seguro para testar e implantar seu código usando imagens em arquivos locais. Seu servidor precisa ser configurado para fornecer aprovação do CORS (`*cross-origin resource sharing* `__). Onde estamos e para onde vamos? ------------------------------- Nessa aula aplicamos os conceitos vistos em aulas passadas para implementar programas gráficos usando superfícies texturizadas. .. Nas próximas aulas vamos apresentar outros efeitos de textura como *Bump Mapping* e *Environment Mapping*. Na próxima aula vamos discutir como renderizar sombras, um outro efeito de iluminação capaz de melhorar ainda mais o realismo das imagens geradas por computador. Para saber mais --------------- * Capítulo 7 do livro "Interactive Computer Graphics, 8th edition" de Angel e Shreiner. * `Notas de aula do Prof. Dave Mount `__. * `WebGL2 Textures `__.