Prática 04 CG: Movimentando objetos

<< T03: Adicionando Cores T05: Agora é em 3D >>

Bem vindo ao tutorial número 3 nessa série de tutoriais WebGL. Neste momento vamos deixar o mundo do estático e irmos para o mundo dos movimentos. Por enquanto estaremos o mesmo triângulo e quadrado, planos, mas não se preocupe. No próximo tutorial iremos trabalhar com formas tridimensionais. O tutorial é baseado na lição 3 do LearningWebGL.

Veja o resultado obtido:

Veja o resultado.

Um aviso: estas lições estão baseadas no conteúdo dado na disciplina de Introdução à Computação Gráfica do Instituto de Matemática e Estatística da USP. Mesmo assim, outras pessoas que não sejam alunos dessa disciplina podem aproveitar e compreender o conteúdo destes tutoriais. Se você não fez o tutorial 02 e o tutorial 03, recomendo fazê-lo antes de avançar para este tutorial. Se você se sente seguro em compreender o que se passa aqui, pode continuar. Se houver falhas ou achar que falta alguma coisa para melhorar o tutorial, não hesite em me avisar.

Para o OpenGL, existem diversas bibliotecas gráficas (como GLUT, GLFW, QT, SDL e WxWidgets) que promovem um gerenciamento de janelas e modos de desenhar a imagem nessas janelas. No WebGL, precisamos utilizar o JavaScript. Para fazer animação, precisamos a cada instante atualizar nossa tela de desenho. Então precisamos chamar a função desenharCena constantemente.

Com que frequência? Você escolhe. Você pode esperar a cada segundo, para desenhar uma cena, pode desenhar 30 imagens dentro desse segundo, ou então desenhar logo que a imagem ficar pronta e disponível. O JavaScript contém funções como requestAnimFrame(func) que recebe uma função que será chamada tão logo quanto possível. E se dentro da função func estiver justamente o requestAnimFrame(func)? Então a função func será regularmente chamada, e podemos utilizar esse recurso para fazer nossa animação. Alguns navegadores preferem criar suas próprias funções. Por exemplo, a Mozilla (do Firefox) disponibiliza a função mozRequestAnimationFrame. Para evitar que nosso ambiente seja incompatível com algum navegador, existe um código feito para resolver a incompatibilidade. Ele está disponível no WebGLSamples, um repositório de exemplos no Google Code. O arquivo é o webgl-utils.js.

31
32
33
34
<script type="text/javascript" src="js/glMatrix-2.4.0.min.js"></script>
<script type="text/javascript" src="js/jquery-3.2.1.min.js"></script>
<!--Adicione esta linha-->
<script type="text/javascript" src="js/webgl-utils.js"></script>

Tarefa: Modifique a função iniciaWebGL da forma como abaixo.

62
63
64
65
66
67
68
69
70
71
function iniciaWebGL()
{
  var canvas = $('#licao01-canvas')[0];
  iniciarGL(canvas); // Definir como um canvas 3D
  iniciarShaders();  // Obter e processar os Shaders
  iniciarBuffers();  // Enviar o triângulo e quadrado na GPU
  iniciarAmbiente(); // Definir background e cor do objeto
  /*---Remova desenharCena e adicione esta linha---*/
  tick();            // Desenhar a cena repetidamente
}

Removemos a função desenharCena (ela vai ser chamada dentro do tick()) e inserimos a função tick(). A função tick() programará a sua próxima chamada atraveś do requestAnimFrame.

E a função setInterval do Javascript? A função setInterval programa a chamada de uma função depois de um determinado tempo, e isso repetidamente. Ele também pode ser usado para animações. Porém ele tem um problema: mesmo que você não esteja vendo a animação (está em outra aba por exemplo acessando outras coisas), a função é executada, piorando o desempenho de sua navegação, especialmente quando você tem mais de uma aba com animações WebGL (a execução de uma animação pioraria a execução da outra). A função requestAnimFrame só é chamada quando a aba estiver ativada.

Tarefa: Adicione a função tick() no script em JavaScript:

72
73
74
75
76
77
function tick()
{
  requestAnimFrame(tick);
  desenharCena();
  animar();
}

Ele programa sua próxima execução, desenha a cena e atualiza informações para animação (por exemplo, incrementando o ângulo de rotação). Você pode, se desejar, trocar a ordem das funções (atualizar os dados antes de desenhar). Ah, e já que vamos rotacionar, vamos então guardar o ângulo de rotação em uma variável.

Tarefa: Adicione estas variáveis globais (fora de qualquer função):

50
51
var rTri = 0;
var rQuad= 0;

Eles serão usados para rotacionar os objetos. A cada tick, iremos incrementar o ângulo. Variáveis globais não são uma boa prática quando for trabalhar com projetos mais complexos. Teremos tutoriais em que organizaremos melhor estas variáveis.

A próxima mudança se dá em desenharCena. Logo após a translação, faremos a rotação dos objetos. Vamos fazer rotações diferentes para cada objeto. Então precisamos guardar a matriz antes da rotação do triângulo para recuperar a matriz original e rotacioná-lo para o quadrado. Se não houver essa operação guardar/recuperar, as rotações serão combinadas. Então o quadrado sofrerá a rotação do triângulo e dele (nessa ordem). Não queremos isso.

Tarefa: Modifique a função desenharCena (a parte do triângulo):

223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
function desenharCena()
{
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  mat4.perspective(pMatrix, 45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0);
  mat4.identity(mMatrix);
  mat4.identity(vMatrix);
  
  // Desenhando Triângulo
  mat4.translate(mMatrix, mMatrix,[-1.5, 0.0, -7.0]);
  
  /*---Adicione estas duas linhas---*/
  mPushMatrix();
  mat4.rotate(mMatrix, mMatrix, degToRad(rTri), [0, 1, 0]);
  
  gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
  gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
  gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexColorBuffer);
  gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, triangleVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);
  setMatrixUniforms();
  gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);
  
  /*---Adicione esta linha---*/
  mPopMatrix();  

Digamos que nós temos a matriz \(T_t\) que representa a translação do triângulo, e \(R_t\) a sua rotação. A matriz \(R_tT_t\) representa a translação e rotação do triângulo (a ordem é da direita para a esquerda, como uma composição de funções matemáticas). Para o quadrado, poderíamos aplicar a rotação inversa do triângulo para neutralizar sua rotação (\(R_t^{-1}R_tT_t\)) e depois a traslação do quadrado e sua rotação (\(R_qT_qR_t^{-1}R_tT_t\)). Mas veja que \(R_t^{-1}R_t = I\). E por isso a transformação do quadrado é \(R_qT_qT_t\). Ou seja, antes da translação do quadrado, só basta a translação do triângulo. Então guardamos a transformação \(T_t\), e depois de rotacionar o triângulo, substituímos a transformação atual \(R_tT_t\) pela matriz salva \(T_t\), utilizando uma estrutura de pilha. Dessa forma, não precisamos aplicar a rotação inversa.

Permita-me explicar essa função de rotação: os ângulos precisam ser convertidos para radianos (mostrarei a função degToRad depois, ele é bem simples). Para o triângulo, utilizamos a nossa variável rTri. Vamos rotacionar a partir do eixo \(y = [0,1,0]^T\). Para saber como ele será rotacionado, vou te dar uma dica: usando a mão direita (estamos utilizando a regra da mão direita como sistema de coordenadas), coloque o seu polegar oapontado para o eixo (nesse caso o y) e os outros dedos curvados darão o sentido da rotação. Olha a figura abaixo:

Tarefa: Modifique a função desenharCena (agora para o quadrado):

246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
  // Desenhando o Quadrado
  mat4.translate(mMatrix, mMatrix, [3.0, 0.0, 0.0]);
  /*---Adicione estas duas linhas---*/
  mPushMatrix();
  mat4.rotate(mMatrix, mMatrix, degToRad(rQuad), [1, 0, 0]);
  
  gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
  gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
  gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer);
  gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, squareVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);
  setMatrixUniforms();
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems);
  /*---Adicione esta linha---*/
  mPopMatrix();
}

Isso é tudo para a função desenharCena. Vamos agora atualizar os ângulos para o próximo desenho.

Tarefa: Adicione a função animar().

268
269
270
271
272
273
274
275
276
277
278
279
  var ultimo = 0;
  function animar()
  {
    var agora = new Date().getTime();
    if(ultimo != 0)
    {
      var diferenca = agora - ultimo;
      rTri  += ((90*diferenca)/1000.0) % 360.0;
      rQuad += ((75*diferenca)/1000.0) % 360.0;
    }
    ultimo = agora;
  }

Poderíamos atualizar o ângulo simplesmente com rTri += valor_a_incrementar e rQuad += valor_a_incrementar. Mas há um problema: máquinas mais rápidas rotacionarão o triângulo mais rápido do que máquinas mais lentas (pois elas incrementarão o ângulo mais rapidamente). E como sincronizá-los para ter a mesma animação independente da velocidade da máquina? Usamos a duração entre os frames para servir de peso para a rotação: se a máquina é lenta, então a duração entre os frames é maior, então rotacionaremos o proporcional a essa duração; se a máquina for rápida, a duração entre os frames é menor, então faremos pequenas rotações. No final, em um segundo, tanto a máquina rápida quanto a máquina lenta terão a mesma rotação.

Ah, adicionamos novas funções mPushMatrix e mPopMatrix. Eles simplesmente trabalham com uma pilha, retirando e colocando matrizes.

Tarefa: Adicione a pilha (variável global) e as funções mPushMatrix e mPopMatrix.

281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
  var mMatrix = mat4.create();
  /*---Adicione esta linha---*/
  var mMatrixPilha = [];
  var vMatrix = mat4.create();
  var pMatrix = mat4.create();

  /*---Adicione esta função---*/
  function mPushMatrix() {
    var copy = mat4.clone(mMatrix);
    mMatrixPilha.push(copy);
  }

  /*---Adicione esta função---*/
  function mPopMatrix() {
    if (mMatrixPilha.length == 0) {
      throw "inválido popMatrix!";
    }
    mMatrix = mMatrixPilha.pop();
  }

A função mPushMatrix copia a matriz atual mMatrix e guarda na pilha. A função mPopMatrix devolve a matriz guardada no topo da pilha para a mMatrix.

Lembra da função degToRad? Ela é apenas uma linha de código, em que \(rad = \frac{PI}{180}\times graus\).

Tarefa: Adicione a função degToRad:

1
2
3
function degToRad(graus) {
  return graus * Math.PI / 180;
}

Pronto. A função tick chamará o desenharCena que rotacionará os objetos, e o animar que atualizará os ângulos. Fim das modificações. Agora vamos dar um corpo tridimensional nesses polígonos no tutorial 05

<< T03: Adicionando Cores T05: Agora é em 3D >>