.. _sec-transformacoes2: Desenhando em 3D com o WebGL ============================ Agora vamos aplicar os fundamentos vistos na aula anterior para gerar vistas tridimensionais de uma cena. Vamos começar desenhando formas 3D sem perspectiva e inserir a matriz de projeção mais tarde. Desenho de um cubo, ainda sem perspectiva ----------------------------------------- Considere os vértices de um cubo de lado unitário centrado no volume de visualização canônico usado no WebGL, definido pelo trecho de código abaixo. .. code:: JavaScript // posições dos 8 vértices de um cubo de lado 1 // centrado na origem var gaPosicoes = [ (-0.5, -0.5, 0.5), (-0.5, 0.5, 0.5), ( 0.5, 0.5, 0.5), ( 0.5, -0.5, 0.5), (-0.5, -0.5, -0.5), (-0.5, 0.5, -0.5), ( 0.5, 0.5, -0.5), ( 0.5, -0.5, -0.5) ]; // cores associadas a cada vértice var gaCores = [ vec4(0.0, 0.0, 0.0, 1.0), // black vec4(1.0, 0.0, 0.0, 1.0), // red vec4(1.0, 1.0, 0.0, 1.0), // yellow vec4(0.0, 1.0, 0.0, 1.0), // green vec4(0.0, 0.0, 1.0, 1.0), // blue vec4(1.0, 0.0, 1.0, 1.0), // magenta vec4(1.0, 1.0, 1.0, 1.0), // white vec4(0.0, 1.0, 1.0, 1.0) // cyan ]; Os vértices no array ``gaPosicoes`` poderiam ser do tipo ``vec4`` mas, como sabemos que a coordenada homogênea é sempre igual a 1, essa última coordenada é inserida diretamente pelo vertex shader. Nos exercícios anteriores, o array de posições dos vértices era preenchido com triângulos, ou seja, cada 3 indices consecutivos desse array correspondia a um triângulo. Nesse exemplo vamos mostrar um jeito diferente, vamos utilizar um **array de índices** que definem os triângulos usando os vértices únicos, sem repetições, de ``gaPosicoes``, como no trecho: .. code:: JavaScript // indices para cada um dos 12 triângulos, 2 por face, que definem o cubo. var gaIndices = [ 1, 0, 3, 3, 2, 1, 2, 3, 7, 7, 6, 2, 3, 0, 4, 4, 7, 3, 6, 5, 1, 1, 2, 6, 4, 5, 6, 6, 7, 4, 5, 4, 0, 0, 1, 5 ]; Observe que cada linha do array ``gaIndices`` contém três inteiros onde cada inteiro corresponde a um índice do array ``gaPosicoes`` (e também de ``gaCores``). Assim, cada linha do array define portanto um triângulo, no total de 12 triângulos (2 para cada face do cubo). Vamos usar a função ``gl.drawElements()`` do WebGL para desenhar usando uma lista de índices de vértices (elementos do array ``gaPosicoes``), ao invés de desenhar diretamente a partir da lista de vértices. A função para desenho ``render()`` usando ``gl.drawElements()`` pode ser simplesmente: .. code:: JavaScript function render() { gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); gl.drawElements(gl.TRIANGLES, gCtx.numV, gl.UNSIGNED_BYTE, 0); } A vantagem desse método é que podemos reutilizar os vértices, resultando portanto em uma notação mais compacta e menos redundante. A função recebe os parâmetros ``gl.drawElements(mode, count, type, offsest)`` onde: * *mode*: define o modo de desenho, como ``gL.POINTS``, ``gl.LINES``, ``gl.TRIANGLES``, etc., semelhante aos modos usados pelo ``gl.drawArrays()``. * *count*: número de índices a serem usados. No caso do cubo, temos 6 faces, com 2 triângulos por face e 3 índices por triângulo, ou seja, *count* = 6 * 2 * 3 = 36, que no caso corresponde ao comprimento do array ``gaIndices``. * *type*: descreve o tipo usado no array ``gaIndices``. No exemplo usamos ``gl.UNSIGNED_BYTE``, que usa apenas 8 bits. Outros tipos possíveis poderiam ser ``gl.UNSIGNED_SHORT`` (16 bits) e ``gl.UNSIGNED_INT`` (32 bits). * *offset*: Deslocamento para desconsiderar elementos no início do array ``gaIndices``. .. admonition:: Limpando o buffer de profundidade Em 2D cada pixel tinha, basicamente, uma cor que precisava ser "limpa" usando o comando ``gl.clear(gl.COLOR_BUFFER_BIT)``. Agora que vamos trabalhar em 3D, além de limpar o buffer de cor, precisamos limpar também o **buffer de profundidade**. Lembre-se que, na última aula, vimos como criar uma matriz de transformação perspectiva com profundidade. Essa profundidade fica armazenada no buffer de profundidade do WebGL e é usada para resolver oclusões, ou seja, apenas o pixel mais perto da câmera é pintado. Cada buffer é representado por um bit no contexto do WebGL e, para limpar esses dois buffers basta combinar esse bits usando um operador ``|`` (ou lógico) pelo comando ``gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);`` Desenho de um cubo em 3D: 1a tentativa -------------------------------------- Como exercício, antes de continuar sua leitura, tente criar um programa gráfico (com shaders etc) que utilize os dados fornecidos acima: * ``gaPosicoes``, * ``gaCores``, * ``gaIndices``, e * ``gl.drawElements()`` para ver o resultado da função ``render()`` que também foi fornecida. Você deve usar o seguinte vertex shader: .. code:: JavaScript gVertexShaderSrc = `#version 300 es // aPosition é um buffer de entrada in vec3 aPosition; in vec4 aColor; // buffer com a cor de cada vértice out vec4 vColor; // varying -> passado ao fShader void main() { gl_Position = vec4(aPosition, 1); vColor = aColor; } `; O código desse exemplo pode ser adaptado do exemplo completo de animação do cubo fornecido mais adiante. Se você conseguiu completar as lacunas desse programa, deve obter algo parecido com o ilustrado na Figura :numref:`fig:a15-primeira`. Observe nessa figura que, como o vertex shader usa diretamente as posições dos vértices, sem alterar a posição default da câmera, nossa vista só permite ver uma das faces do cubo, como se nosso desenho fosse em 2D. A face sendo exibida é a mais "perto" da câmera, ou seja, no plano :math:`z=-0.5`, com as cores *blue*, *magenta*, *white* e *cyan*. .. figure:: figuras/a15/cubo0-3d.png :alt: Cubo em 3D -- primeira tentativa. :name: fig:a15-primeira :width: 55.0% :align: center Renderização de um cubo em 3D - primeira tentativa. 2a tentativa: usando a função *lookAt()* ---------------------------------------- Se o problema está em mudar a câmera de lugar, vamos aplicar a função ``lookAt()`` (do módulo ``MVnew.js``) para colocar a câmera em :math:`eye=(0.75, 0.75, 0.75)`, olhando para :math:`at=(0, 0, 0)` e :math:`up=(0, 1, 0)`. Para usar essa matriz vamos modificar o vertex shader para incluir um uniforme de nome ``uModelView``: .. code:: JavaScript gVertexShaderSrc = `#version 300 es // aPosition é um buffer de entrada in vec3 aPosition; uniform mat4 uModelView; in vec4 aColor; // buffer com a cor de cada vértice out vec4 vColor; // varying -> passado ao fShader void main() { gl_Position = uModelView * vec4(aPosition, 1); vColor = aColor; } `; A nossa nova função de desenho deve calcular e passar essa matriz ao shader: .. code:: JavaScript function render() { gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // calcula a matriz de transformação da camera let eye = (0.75, 0.75, 0.75); let at = (0, 0, 0); let up = (0, 1, 0); gCtx.vista = lookAt( eye, at, up); gl.uniformMatrix4fv(gShader.uModelView, false, flatten(gCtx.vista)); // desenha gl.drawElements(gl.TRIANGLES, gCtx.numV, gl.UNSIGNED_BYTE, 0); } O resultado de mover a câmera para :math:`eye=(0.75, 0.75, 0.75)` é ilustrado na Figura :numref:`fig:a15-segunda` a. Mas por que a figura não mostra todo o cubo? Lembre-se que ``lookAt()`` apenas move a câmera, como se estivesse movendo o volume canônico de visualização. Nessa nova posição, apenas um "bico" do cubo fica contido dentro do novo volume de visualização (o resto fica para fora do volume e portanto é recortado). Vamos mover então a câmera para um pouco mais perto, digamos, para :math:`eye=(0.5, 0.5, 0.5)`. O resultado é ilustrado na Figura :numref:`fig:a15-segunda` b. Esse resultado lhe parece estranho? Você consegue ver o que está acontecendo? .. figure:: figuras/a15/a15-lookAt.png :alt: Cubo em 3D -- segunda tentativa. :name: fig:a15-segunda :width: 95.0% :align: center Segunda tentativa para renderizar um cubo usando a função ``lookAt(eye, at, up)``. a) mostra o resultado com :math:`eye=(0.75, 0.75. 0.75)`; b) mostra o resultado com :math:`eye=(0.5, 0.5, 0.5)` e c) mostra o resultado também em :math:`eye=(0.5, 0.5, 0.5)` mas usando ``gl.cullBack(gl.BACK)`` para eliminar as faces que não estão de frente para a câmera. Apesar do desenho mostrar uma área maior do cubo, ele mostra partes do cubo que deveriam estar escondidas devido à oclusão das faces do cubo, pois o WebGL está desenhando os dois lados de cada triângulo (frente e verso). Para casos assim, podemos fazer o WebGL "esconder" as faces internas (vamos considerar como verso) e só desenhar as faces externas (vamos considerar como faces da frente) usando a função ``gl.cullFace(gl.BACK)`` que desconsidera (não pinta) o verso das faces. Mas como eu defino a frente e o verso de uma face, ou triângulo? Para variar, vamos adotar a regra da mão direita, como ilustrado na Figura :numref:`fig:a15-normal`, onde o triângulo definido pelos vértices na ordem 1-2-3 tem normal para fora do cubo ("frente"). Ao desenhar, quando o ``gl.cullFace(gl.BACK)`` estiver habilitado, o WebGL só vai desenhar o lado da "frente", testando se a normal dos triângulos apontam para a câmera. Observe que esse pode ser um jeito eficiente de eliminar "metade" das faces a serem desenhadas. Como exercício, verifique a ordem dos índices de cada triângulo em ``gaIndices`` e verifique que eles seguem essa regra. .. figure:: figuras/a15/normal.png :alt: Normal externa :name: fig:a15-normal :width: 35.0% :align: center O triangulo formado pelos vertices 1-2-3 tem sua normal apontando para "fora" do cubo, que vamos definir como lado da "frente". Para definir a "frente" triângulo como o lado oposto, utilize a ordem 3-2-1. A seguinte função ``render()`` mostra como usar o ``gl.cullFace()``. Observe que é possível também esconder objetos fazendo ``gl.cullFace(gl.FRONT_AND_BACK)`` ou ainda, desenhar apenas a parte interna usando ``gl.cullFace(gl.FRONT)``. O resultado com ``gl.cullFace`` é exibido na Figura :numref:`fig:a15-segunda` c. .. code:: JavaScript function render() { gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); gl.enable(gl.CULL_FACE); gl.cullFace(gl.BACK); // calcula a matriz de transformação da camera let eye = (0.75, 0.75, 0.75); let at = (0, 0, 0); let up = (0, 1, 0); gCtx.vista = lookAt( eye, at, up); gl.uniformMatrix4fv(gShader.uModelView, false, flatten(gCtx.vista)); // desenha gl.drawElements(gl.TRIANGLES, gCtx.numV, gl.UNSIGNED_BYTE, 0); } Além da limitação do volume de visualização e da necessidade de usar ``cullFace()`` pois não estamos usando informação de profundidade, nosso desenho também não apresenta distorções perspectiva. Para isso, vamos fazer uma nova tentativa de renderização, agora usando também uma matriz de projeção. 3a tentativa: usando a função *perspective()* --------------------------------------------- Vamos dar um pouco mais de trabalho para a GPU, modificando novamente o vertex shader para que receba, além da ``uModelView``, a matriz ``uPerspective``, como a seguir: .. code:: JavaScript gVertexShaderSrc = `#version 300 es // aPosition é um buffer de entrada in vec3 aPosition; uniform mat4 uModelView; uniform mat4 uPerspective; in vec4 aColor; // buffer com a cor de cada vértice out vec4 vColor; // varying -> passado ao fShader void main() { gl_Position = uPerspective * uModelView * vec4(aPosition, 1); vColor = aColor; } `; A nova função ``render()`` deve agora calcular a perspectiva com profundidade. Vamos aproveitar e afastar um pouco mais a câmera, para :math:`eye=(1.75, 1.75, 1.75)`. Nesse caso, a nova ``render()`` poderia ser: .. code:: JavaScript function render() { gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // gl.enable(gl.CULL_FACE); // gl.cullFace(gl.BACK); // calcula a matriz de transformação perpectiva (fovy, aspect, near, far) gCtx.perspectiva = perspective( 60, 1, 0.1, 5); gl.uniformMatrix4fv(gShader.uPerspective, false, flatten(gCtx.perspectiva)); // calcula a matriz de transformação da camera let eye = (1.75, 1.75, 1.75); let at = (0, 0, 0); let up = (0, 1, 0); gCtx.vista = lookAt( eye, at, up); gl.uniformMatrix4fv(gShader.uModelView, false, flatten(gCtx.vista)); // desenha gl.drawElements(gl.TRIANGLES, gCtx.numV, gl.UNSIGNED_BYTE, 0); } O código completo desse exemplo está disponível no `JSitor `__ e também pode ser visto logo abaixo. Modifique os valores para o campo de visão vertical ``fovy`` e a razão de aspecto para ver os seus efeitos sobre o resultado. Tanto o plano ``near`` quanto o ``far`` podem ser modificados desde que o intervalo contenha os objetos de interesse e depende também da posição da câmera (``eye``). .. raw:: html Animação do cubo em perspectiva ------------------------------- Os exemplos anteriores serviram para ilustrar, passo-a-passo, o efeito da função ``lookAt()``, que define a posição e orientação da câmera e da função ``perspective()``, que desenha as distorções devido à projeção perspectiva. Vamos agora animar o cubo, fazendo-o rodar ao longo de um dos eixos. Vamos fazer isso por meio de transformações no espaço afim, aplicando transformações de rotação sobre o cubo centrado na origem. O código desse exemplo está disponível no `JSitor `__ e também pode ser visto mais abaixo. Para gerar a animação, a seguinte função ``render()`` atualiza a transformação de rotação aplicada a apenas um dos eixos. Observe que apenas a matriz ``model`` é atualizada nessa função. Nessa nova versão do programa, as demais matrizes, ``view`` e ``perspective``, são calculadas e passadas para a GPU apenas uma única vez, quando os shaders são criados. .. code:: JavaScript function render() { gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // modelo muda a cada frame da animação if(!gCtx.pause) gCtx.theta[gCtx.axis] += 2.0; let rx = rotateX(gCtx.theta[EIXO_X]); let ry = rotateY(gCtx.theta[EIXO_Y]); let rz = rotateZ(gCtx.theta[EIXO_Z]); let model = mult(rz, mult(ry, rx)); gl.uniformMatrix4fv(gShader.uModelView, false, flatten(mult(gCtx.vista, model))); gl.drawElements(gl.TRIANGLES, gCtx.numV, gl.UNSIGNED_BYTE, 0); window.requestAnimationFrame(render); } A escolha do eixo é feita clicando em um dos botões da interface. Estude esse exemplo para entender como funciona a ``Pausa`` desse exemplo. .. raw:: html Desenho de uma esfera --------------------- Assim como no caso 2D, o desenho de objetos mais complexos, como por exemplo objetos compostos por superfícies não planares como uma esfera, é um pouco mais trabalhoso também. Lembre-se que, para desenhar um disco em 2D, ao invés de calcular cada vértice, usamos uma forma recursiva que refina o contorno do disco a cada iteração. Assim, podemos começar com apenas 4 vértices, que formam um quadrado, e refinar nosso modelo de disco incluindo um novo vértice no meio de cada lado, até atingir a qualidade desejada. .. No caso 2D, você pode ter imaginado soluções mais simples, como abaixo, que gera todos os vértices usando funções trigonométricas: .. code:: JavaScript // gera uma lista com n vértices de um disco de raio unitário // centrado na origem. function geraDisco( n ) { let vertices = []; let deltaAngulo = 2*Math.PI/n; let a = 0.0; for (let i=0; i 0) { let ab = mix( a, b, 0.5); let bc = mix( b, c, 0.5); let ca = mix( c, a, 0.5); ab = normalize(ab); bc = normalize(bc); ca = normalize(ca); dividaTriangulo( a, ab, ca, ndivs - 1); dividaTriangulo( b, bc, ab, ndivs - 1); dividaTriangulo( c, ca, bc, ndivs - 1); dividaTriangulo(ab, bc, ca, ndivs - 1); } else { insiraTriangulo(a, b, c); } }; Nesse caso, como a esfera tem raio unitário, quando um "ponto" médio é normalizado, na verdade estamos normalizando o vetor que conecta o ponto a origem. O vetor normalizado, somado à origem, resulta em um novo ponto sobre a superfície da esfera de raio unitário. Observer que o processo continua recursivamente até atingir o nível desejado e, ao terminar, o triângulo é inserido na lista de vértices. Dessa vez, estamos colocando triângulos no array de vértices para usar a função ``gl.drawArrays()``. O código completo desse exemplo está disponível no `JSitor `__ e, basicamente, utiliza a mesma estrutura que usamos para desenhar o cubo em perspectiva. .. raw:: html Onde estamos e para onde vamos? ------------------------------- Nessa aula aplicamos os fundamentos teóricos apresentados nas aulas anteriores para posicionar a câmera na cena usando a função ``lookAt()`` e aplicar uma projeção perspectiva com profundidade usando a função ``perspective()``. Aproveitamos a extensão de 2D para 3D para mostrar também outra forma de desenhar no WebGL, usando a função `g.drawElements()` e apresentar o recurso ``gl.cullFace`` que pode ser usado para desenhar apenas os triângulos voltados para a câmera. Por fim, damos alguns exemplos de animação e desenho de objetos com superfícies mais complexas, como uma esfera. Na próxima aula, vamos continuar nossa jornada para aumentar o realismo de nossos desenhos introduzindo efeitos de iluminação e sombreamento. Para saber mais --------------- * Capítulo 5 do livro "Interactive Computer Graphics, 8th edition" de Angel e Shreiner. * `WebGL2 3D Perspective `__. * `WebGL2 3D Cameras `__.