.. _sec-webgl: Desenhando com o WebGL ====================== Em aulas passadas vimos alguns recursos básicos de interação e animação da API 2D do canvas do HTML5. A partir dessa aula, vamos usar a API do WebGL 2.0 para gerar desenhos. OpenGL, WebGL e GLSL -------------------- Na década de 1990, os computadores da Silicon Graphics (SGI) dominavam o mercado de computadores usados para a criação de gráficos em 3D, como ilustrado na cena do filme Jurassic Park, de 1993, abaixo. O modelo IRIS da SGI utilizava a API (*Application Programming Interface* ou interface de programação da aplicação) gráfica IRIS GL que veio a se tornar a primeira versão do `OpenGL (Open Graphics Library) `__ em 1992. Esse padrão evoluiu bastante desde então, introduzindo cada vez mais recursos, até a sua versão mais recente, a 4.6, lançada em 2017. .. raw:: html

Cena "It's a Unix System" do filme Jurassic Park, de 1993. Preste atenção na marca do monitor.

O OpenGL é hoje a API padrão usada para o desenvolvimento de aplicativos gráficos tanto 2D quanto para 3D. Ela é portável para múltiplas plataformas e foi agregada a várias linguagens como C, Java e Python. O WebGL surgiu da agregação do Javascript com o OpenGL e se tornou um novo padrão para geração de gráficos interativos na Web. A primeira versão do WebGL foi derivada inicialmente do padrão OpenGL ES 2.0 mantido pelo grupo `Khronos `__. O OpenGL ES (*Embeded System*) é uma versão do OpenGL voltada para sistemas gráficos embarcados como telefones celulares e consoles de jogos. A versão mais recente, WebGL 2.0 lançada em 2017, é derivada do padrão OpenGL ES 3.0 e já é compatível com os navegadores mais populares como Google Chrome, Mozilla Firefox e Opera. Com a evolução do hardware, a API também evoluiu. A partir do padrão OpenGL 2.0, é possível programar o pipeline gráfico por meio de funções de sombreamento chamadas de **shaders**. Essas funções permitem a implementação de efeitos visuais sofisticados usando uma **linguagem de sombreamento** (*shading language*) baseada no OpenGL Shading Language (GLSL). Algumas vantages de usar WebGL são: - **Programação JavaScript** - os aplicativos WebGL são escritos em JavaScript. Usando estes aplicativos, é possível interagir diretamente com outros elementos do documento HTML, inclusive outras bibliotecas JavaScript (como JQuery) e tecnologias HTML. - **Suporte para navegadores em dispositivos móveis** - o WebGL também oferece suporte a navegadores móveis, como iOS e Android. - **Código aberto** - o WebGL é um código aberto (Open Source). Você pode acessar o código-fonte da biblioteca e entender como funciona e como foi desenvolvido. - **Não há necessidade de compilação** - para executar JavaScript não há necessidade de compilar o arquivo, que pode ser aberto diretamente em qualquer navegador compatível com HTML5. - **Gerenciamento automático de memória** - o JavaScript oferece suporte ao gerenciamento automático de memória. Não há necessidade de alocação manual de memória. O WebGL herda esse recurso de JavaScript. - **Fácil de configurar** - como o WebGL é integrado ao HTML5, não há necessidade de configuração, basta um editor de texto e um navegador web. Programação com shaders ----------------------- A API do WebGL permite desenvolver **shaders**, os programas que vão ser executados dentro da GPU. Você deve se lembrar do pipeline gráfico, correto? Essa arquitetura permite a renderização de elementos geométricos simples, como pontos, linhas e polígonos, de uma forma muito eficiente. E é basicamente isso que o WebGL nos permite fazer, modelar nossas cenas usando esses elementos. Um programa que usa o WebGL deve definir o código executado pela GPU na forma de duas funções costumeiramente chamadas de: * **Vertex shader**: que calcula as posições dos vértices como vistos pela câmera; e * **Fragment shader**: que calcula a cor final de cada pixel desenhado no frame buffer. Assim como fizemos para desenhar no canvas, primeiramente definindo o estado da pena (cor, largura, forma da linha etc.) antes de mandar a pena desenhar uma linha, no WebGL devemos configurar também uma série de estados antes de desenhar. Um programa usando WebGL deve compilar os shaders que são enviados a GPU. Para enviar os dados à GPU, podemos utilizar uma das seguintes quatro formas básicas: * **Atributos e Buffers**: buffers são arrays de dados que são carregados na GPU que podem conter posições, normais, cores etc. Os atributos definem como os dados devem ser extraídos de um buffer e passado ao shader, por exemplo, como 3 floats de 32 bits. * **Uniforms**: são variáveis globais configuradas antes de executar o shader. * **Texturas**: são imagens que podem ser usadas pelos shaders. * **Varyings**: são variáveis usadas para passar dados do vertex para o fragment shader. Esqueleto de um programa usando WebGL -------------------------------------- O seguinte exemplo que desenha um triângulos ilustra os principais blocos de um programa usando WebGL. O código fonte desse exemplo está disponível no `JSitor `__. .. raw:: html Clique na aba ``HTML`` para se certificar que a página só contém um elemento canvas de ``id="glcanvas"``. Agora clique na aba ``JavaScript`` para ver o conteúdo da função ``main``, como abaixo: .. code:: JavaScript function main() { gCanvas = document.getElementById("glcanvas"); gl = gCanvas.getContext('webgl2'); if (!gl) alert( "WebGL 2.0 isn't available" ); crieShaders(); desenhe(); } Observe que, ao invés de usar ``getContext("2d")``, dessa vez usando ``getContext("webgl2")`` para utilizar a API do WebGL 2.0. Vamos acessar essa API pela variável global ``gl``. Antes de descrever as funções ``crieShaders`` e ``desenhe()``, vamos descrever um pouco mais o conteúdo dos shaders. .. admonition:: Nota sobre nossa notação Nos exemplos a seguir utilizaremos a notação chamada de "camel case" para os nomes das variáveis globais e funções. As variáveis globais começaram pela letra "g" (minúscula) e, para facilitar também o entendimento de tipos simples, uma segunda letra minúscula pode indicar um tipo primitivo, como "s" para string e "a" para array. Dessa forma, um nome ``gaTriangulo`` indica uma variável global do tipo array. Código do Vertex e Fragment Shaders ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Nesse (e em futuros) exemplos, o código fonte do vertex e fragment shaders estão em duas Strings: ``gsVertexShaderSrc`` e ``gsFragmentShaderSrc``: .. code:: Javascript var gsVertexShaderSrc = `#version 300 es // aPosition é um buffer de entrada in vec2 aPosition; void main() { // gl_Position é uma variável reservada que deve ser especificada gl_Position = vec4(aPosition, 0, 1); } `; var gsFragmentShaderSrc = `#version 300 es // Vc deve definir a precisão do FS. // Use highp ("high precision") para desktops e mediump para mobiles. precision highp float; // out define a saída out vec4 fColor; void main() { // nesse caso é uma constante fColor = vec4(1.0, 0.0, 0.0, 1.0); } `; O código de cada shader deve **sempre** conter, na primeira linha, a diretiva ``#version 300 es`` para indicar que o código está em WebGL 2.0. Nesse exemplo, o ``vertex shader`` deve receber da CPU um buffer ``aPosition`` do tipo ``vec2``, indicando que cada dado desse buffer é constituído por um vetor de 2 elementos do tipo float com 32 bits. O shader deve possuir uma função ``main()`` que, nesse caso, transforma elementos do buffer para o tipo ``vec4`` usados para desenhar em 3D. Um ponto em 3D no WebGL é representado pelas coordenadas ``(x, y, z, w)``, onde a coordenada ``w`` é chamada de coordenada homogênea. Veremos nas próximas aulas por que o WebGL utiliza 4 coordenadas para desenhar em 3D. Por hora, vamos considerar ``w=1.0`` e, como estamos desenhando um triângulo em 2D, podemos assumir que os pontos estão no plano ``z=0.0`` e o buffer ``aPosition`` recebe as coordenadas dos pontos no plano ``(x, y)``. O código do ``fragment shader`` apenas define que cada pixel de saída é da cor vermelha com RGBA = (1.0, 0.0, 0.0, 1.0). Definindo um triângulo em um sistema de coordenadas normalizado ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ É conveniente definir um triângulo em JavaScript por meio de seus vértices como em: .. code:: JavaScript var gaTriangulo = [ [-0.5, -0.5], [0.0, 0.5], [0.5, -0.5], ]; Observe que a variável global ``gaTriangulo`` é um Array em JS contendo 3 Arrays: [-0.5, -0.5], [0.0, 0.5] e [0.5, -0.5], que representando os vértices do triângulo. O WebGL foi desenvolvido para renderizar objetos 3D mas também pode ser usado para 2D. Como o WebGL precisa ser independente do sistema gráfico, ele utiliza um **sistema de coordenadas 3D normalizado** representado por um cubo de cantos (-1.0, -1.0, -1.0) e (1.0, 1.0, 1.0). O volume interno desse cubo corresponde ao volume de recorte (*clipping volume*), ou seja, qualquer objeto fora desse volume não é renderizado. O que fazer então para renderizar objetos em 2D? Como já mencionamos, no caso 2D, vamos considerar o quadrado definido pelos cantos (-1.0, -1.0) e (1.0, 1.0), no plano ``z=0.0``. Observe que, como um ``buffer`` em GLSL **não tem** a mesma estrutura que um Array do JS, precisamos converter o Array em uma estrutura linear uniforme (como um vetor na linguagem C). Isso é feito pela função ``flatten()`` disponível no código do exemplo, que devolve um array 1D (vetor) com elementos do tipo float 32 bits. Como fazer para desenhar um triângulo? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Assim como programas na linguagem C precisam ser compilados antes de serem executados, o programa gráfico também precisa ser compilado para ser executado pela GPU. A função ``makeProgram()`` faz a compilação do código fonte do vertex e fragment shaders, usando a própria API do WebGL. Não iremos entrar em detalhes sobre o funcionamento dessa função e, nos programas futuros, vamos mover a função ``makeProgram()`` e as demais funções auxiliares para uma biblioteca. Assim fica mais fácil para a gente se concentrar nos desenhos e nos fundamentos da CG. O programa compilado deve ainda ser habilitado pela chamada ``gl.useProgram();``. O programa está pronto mas e os dados? Precisamos agora indicar onde estão os dados para que a GPU possa executar o programa. O trecho de código abaixo cria um buffer na GPU que é associado aos dados vetorizados das posições (vértices), convertidos por ``flatten(gaTriangulo)``. .. code:: JavaScript var bufPosicoes = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, bufPosicoes); gl.bufferData(gl.ARRAY_BUFFER, flatten(gaTriangulo), gl.STATIC_DRAW); .. falar de array em JavaScript? var p = [x, y]; var p = new Float32Array( [x, y] ); Ainda não acabamos. A GPU agora tem um lugar para os dados (um buffer) que vem da CPU (nosso triângulo), mas ainda é necessário definir sua relação com o programa, ou seja, precisamos dizer que esse é o buffer associado à variável ``aPosition``, que é do vertex shader, do tipo ``vec2`` etc. (como definido no código do vertex shader). O trecho de código abaixo configura e habilita esse atributo. .. code:: JavaScript var aPositionLoc = gl.getAttribLocation(program, "aPosition"); // Configuração do atributo para ler do buffer // atual ARRAY_BUFFER let size = 2; // 2 elementos de cada vez let type = gl.FLOAT; // tipo de 1 elemento = float 32 bits let normalize = false; // não normalize os dados let stride = 0; // passo, quanto avançar a cada iteração depois de size*sizeof(type) let offset = 0; // começo do buffer gl.vertexAttribPointer(aPositionLoc, size, type, normalize, stride, offset); gl.enableVertexAttribArray(aPositionLoc); Com isso temos o programa executado na GPU compilado e configurado. O ``gaTriangulo`` é renderizado pela função ``desenhe()``, mostrada abaixo: .. code:: Javascript function desenhe() { // define como mapear coordenadas normalidas para o canvas gl.viewport(0, 0, gCanvas.width, gCanvas.height); // limpa o contexto gl.clearColor(0.0, 1.0, 1.0, 1.0); gl.clear(gl.COLOR_BUFFER_BIT); // desenhe! let tipo = gl.LINE_LOOP; // experimente gl.TRIANGLES e outros let offset = 0; let count = triangulo.length; gl.drawArrays(tipo, offset, count); } A chamada ``gl.viewport(x, y, width, height)`` define a área do canvas onde é projetada a região normalizada de observação definida pelos cantos (-1, -1) a (1, 1), como mostra a Figura :numref:`fig-viewport`. O viewport define uma transformação que é parte do estado da API e assim pode ser modificada para exibir desenhos diferentes em um mesmo canvas, cada um em um viewport distinto. O desenho é "limpo" pela chamada ``gl.clear(gl.COLOR_BUFFER_BIT)`` com a cor definida por ``gl.clearColor()``. Observe que limpar o desenho significa resetar informação no buffer de desenho. Como o buffer pode armazenar várias informações distintas (como profundidade), a chamada de ``gl.clear()`` permite limpar apenas certas informações, muito embora, em geral, desejamos limpar tudo. Nesse caso, como só estamos usando cor, apenas o bit de cor foi setado. .. figure:: ./figuras/a07/viewport.png :alt: Relação entre o canvas, viewport e a região normalizada de desenho. :name: fig-viewport :width: 90.0% :align: center Relação entre o canvas, viewport e a região normalizada de desenho. .. admonition:: Dica: mude a cor de fundo durante a depuração do seu programa Muitas vezes, usamos a cor branca para limpar o fundo. Durante a depuração, pode ser vantajoso alterar essa cor para algo bem visível como vermelho (1.0,0.0,0.0,1.0) ou algo incomum para o seu desenho. Essa cor também pode ser definida uma única vez durante a inicialização do programa. Toda essa preparação é necessária para que a GPU possa renderizar o modelo (triângulo). Lembre-se que a GPU só é capaz de renderizar objetos primitivos. A chamada ``gl.drawArrays()`` recebe um tipo para desenhar esse objeto, a partir de um array de vértices. Para o WebGL os tipos primitivos são mostrados na Figura :numref:`fig-primitivos` e descritos a seguir. * **POINTS**: cada vértice é desenhado como um ponto. * **LINES**: cada par de vértices é desenhado como uma linha. * **LINE_STRIP**: a sequência de vértices define um caminho (linha) aberta * **LINE_LOOP**: a sequência de vértices define um caminho fechado, o último vértice é conectado ao primeiro. * **TRIANGLES**: cada trio de vértices é desenhado como um triângulo (região interna). * **TRIANGLE_STRIP**: cada 3 vértices consecutivos da sequência definem um triângulo. * **TRIANGLE_FAN**: o primeiro vértice é considerado a referência. Cada par de vértices consecutivos é usado para definir um triângulo usando a referência. Assim, todos os triângulos tem o 1o vértice em comum. .. figure:: ./figuras/a07/tipos.png :alt: Resultado da renderização de um array de vértices usando os tipos primitivos do WebGL. :name: fig-primitivos :width: 80.0% :align: center Resultado da renderização de um array de vértices usando os tipos primitivos do WebGL. fonte `Cg Tutorial `__. .. admonition:: Exercício Modifique o código para desenhar uma lista de vértices usando cada um dos tipos primitivos. Um exemplo mais complexo ------------------------- O código fonte desse exemplo está disponível em `JSitor `__. .. raw:: html Clique na barra ``HTML`` e note que há dois scripts JS sendo carregados: .. code:: HTML WebGL - exemplo 2 A biblioteca ``macWebglUtils.js`` contém, basicamente, as funções para a compilação dos shaders. A segunda biblioteca, ``MVnew.js``, é uma biblioteca utilizada pelos exemplos do livro `Interactive Computer Graphics A Top-Down Approach with WebGL `__ , de Edward Angel e Dave Shreiner. A função ``flatten()`` e outras para tratamento de arrays do JavaScript como vec2, vec3 ou vec4 serão utilizadas em futuros exemplos. Você pode baixar essas bibliotecas da pasta `https://www.ime.usp.br/~hitoshi/mac0420/libs/ `__. Por fim, o arquivo ``webgl2.js``, contém o programa exibido nesse exemplo. Vimos que é vantajoso para o WebGL desenhar em uma região normalizada quadrada. No entanto, em geral, o canvas não é quadrado. Por isso, muitas vezes é mais conveniente desenhar usando coordenadas no canvas, em pixels, ao invés de coordenadas normalizadas no intervalo [-1, 1]. Nesse exemplo, observe que o array ``gaPositions`` define dois triângulos usando coordenadas em um canvas :math:`200\times200`. .. code:: Javascript var gaPositions = [ // triangulo 1 [ 50, 50], [150, 50], [100, 150], // triangulo 2 [ 100, 150], [ 200, 150], [ 150, 50], ]; O vertex shader vai receber essas coordenadas e deve normalizá-las para o intervalo [-1, 1]. Vamos dar uma olhada no código fonte do nosso novo vertex shader: .. code:: Javascript gsVertexShaderSrc = `#version 300 es // aPosition é um buffer de entrada in vec2 aPosition; uniform vec2 uResolution; void main() { vec2 escala1 = aPosition / uResolution; vec2 escala2 = escala1 * 2.0; vec2 clipSpace = escala2 - 1.0; gl_Position = vec4(clipSpace, 0, 1); } `; A sintaxe do GLSL se parece muito com C e permite a manipulação de dados 2D, 3D e 4D, definidos por ``vec2``, ``vec3`` e ``vec4``, respectivamente. Nesse exemplo, tanto ``aPosition`` quanto ``uResolution`` são elementos do tipo ``vec2``. Um ``uniform`` permite a passagem de informação da CPU para a GPU. Nesse caso, ``uResolution`` vai indicar a resolução do ``gCanvas``. Vamos adotar a notação que nomes de atributos (associados a um buffer) começam com letra `a` e os uniforms começam com a letra `u`. O GLSL permite operações vetoriais componente a componente. Assim, a operação ``aPosition / uResolution`` normaliza cada coordenada para o intervalo [0.0, 1.0]. Por exemplo, para um canvas com ``uResolution`` =(200,400), uma ``aPosition`` =(50, 300), a variável local ``escala1`` definida na ``main()`` do vertex shader seria (50/200, 300/400) -- divisão elemento a elemento -- resultando no vec2 =(0.25, 0.75). Em seguida, essas coordenadas são multiplicadas por 2.0 e subtraídas de 1.0, novamente, elemento a elemento. Por exemplo, o resultado anterior (0.25, 0.75)*2.0 resulta em (0.5, 1.5) que, quando subtraído de 1.0, resulta em (-0.5, 0.5). Assim as coordenadas no buffer ``aPosition`` são mapeadas para o intervalo [-1.0, 1.0]. Finalmente o ``vec2`` é convertido para um ``vec4`` composto pelas coordenadas ``(x, y)`` normalizadas de ``aPosition`` com ``z=0`` e ``w=1.0``. Observe também no código fonte do fragment shader que podemos usar um uniform para alterar a cor de um triângulo, como ilustrado no trecho a seguir pelo uniform ``uColor`` do tipo ``vec4``: .. code:: JavaScript gsFragmentShaderSrc = `#version 300 es // Vc deve definir a precisão do FS. // Use highp ("high precision") para desktops e mediump para mobiles. precision highp float; // out define a saída out vec4 outColor; uniform vec4 uColor; void main() { outColor = uColor; } `; A criação dos shaders é muito semelhante ao exemplo anterior, mas inclui chamadas extras de ``gl.getUniformLocation`` para registrar esses uniforms na estrutura global ``gShader`` que mantem as informações do programa na GPU. A função de desenho pode então utilizar esses uniforms da seguinte forma: .. code:: JavaScript function desenhe() { // define como mapear coordenadas normalidas para o canvas gl.viewport(0, 0, gCanvas.width, gCanvas.height); // limpa o contexto gl.clearColor(0.0, 1.0, 1.0, 1.0); gl.clear( gl.COLOR_BUFFER_BIT ); gl.uniform2f(gShader.uResolution, gCanvas.width, gCanvas.height); // desenhe 2 triangulos, cada um com uma cor aleatória for (let ii=0; ii<2; ii++) { // Set a random color. gl.uniform4f(gShader.uColor, Math.random(), Math.random(), Math.random(), 1); gl.drawArrays(gl.TRIANGLES, 3*ii, 3); } } Primeiro, observe as chamadas para ``gl.uniformXf``, onde ``X`` é o tamanho do dado a ser passado, ou seja, 2 floats para ``uResolution`` e 4 para ``uColor``. Por meio dessas chamadas, a GPU recebe os valores a serem aplicados pelo programa gráfico. Finalmente, cada triângulo é desenhado usando o tipo ``TRIANGLES`` com os vértices apropriados. Onde estamos e para onde vamos? ------------------------------- A partir de agora vamos usar o WebGL para ilustrar nossos exemplos e desenvolver nossos aplicativos gráficos. No entanto, continuaremos a usar recursos básicos do WebGL para demonstrar fundamentos da computação gráfica e treinar sua habilidade de programação geométrica, tópico a ser coberto nas próximas aulas. A programação de um shader pode é complexa, principalmente nesse início, devido ao grande número de elementos que precisam ser definidos como escrever o código fonte de cada shader, compilar, montar, e depois ainda configurar atributos, buffers, uniforms, etc. Na próxima aula vamos considerar mais alguns detalhes importantes para desenvolver programas gráficos com shaders, integrando-os com os elementos de interação e animação que vimos em aulas passadas. Exercícios ----------- 1. Escreva um programa que desenha um quadrado de lado aleatório em posição aleatória no canvas usando WebGL (e shaders). * Modifique esse programa para desenhar `N=50` quadrados em posições aleatórias, com tamanho e cores também aleatórias. 2. Escreva um programa que aproxima um disco de raio r centrado na origem usando incialmente 4 vértices. Em seguida, escreva um função que duplica o número de vértices usado para desenhar o disco, calculando a posição de vértices intermediários. Em seguida, inclua um slider HTML que permita controlar o número de vertices de 4 a 64. A cor do disco pode ser aleatória. Para saber mais --------------- Recomendamos a seguinte leitura: * `Fundamentos da WebGL2 `__ * `OpenGL na Wikipedia `__ * Capítulos 2 e 3 do livro "Interactive Computer Graphics", 8a edição, de Edward Angel e Dave Shreiner.