Prática 05 CG: Agora é em 3D

<< T04: Movimentando Formas T06: Texturas >>

Bem vindo ao tutorial número 5 nessa série de tutoriais WebGL. Ao invés de um triângulo e um quadrado, vamos agora criar uma pirâmide e um cubo. O tutorial é baseado na lição 5 do LearningWebGL.

Veja o resultado obtido:

Veja o resultado.

Um aviso (de novo): 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 2, o tutorial 3 e o tutorial 4, 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.

As diferenças entre o código desta lição e da anterior se concentram exclusivamente nas funções animar, iniciarBuffers, e desenharCena. Antes disso, uma pequena mudança: ao invés de rTri e rQuad, vamos renomeá-las para rPiramide e rCubo (na declaração e nos usos pelas funções).

Tarefa: Renomeie as variáveis (a localização está indicada nos comentários).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  /*---Edite isso na função animar---*/
  rPiramide  += ((90*diferenca)/1000.0) % 360.0;
  rCubo += ((75*diferenca)/1000.0) % 360.0;
  
  /*---Edite isso na declaração das variáveis---*/
  var rPiramide = 0;
  var rCubo = 0;
  
  /*---Edite isso na função desenharCena---*/
  mat4.rotate(mMatrix, mMatrix, degToRad(rPiramide), [0, 1, 0]);
  
  /*---Edite isso na função desenharCena---*/
  /*---Note o [1,1,1] ao invés de [1,0,0]---*/
  mat4.rotate(mMatrix, mMatrix, degToRad(rCubo), [1, 1, 1]);

Uma pirâmide e um cubo contém mais vértices do que um triângulo e um quadrado. O código para enviar o triângulo ou a pirâmide é o mesmo (só adicionando mais vértices, posições e cores).

Vamos renomear tudo que tenha a palavra triangle para colocar piramide (para fazer mais sentido ao tutorial).

Tarefa: Mude na função desenharCena os nomes triangleVertexPositionBuffer e triangleVertexColorBuffer para piramideVertexPositionBuffer e piramideVertexColorBuffer.

318
319
320
321
322
323
324
325
  gl.bindBuffer(gl.ARRAY_BUFFER, piramideVertexPositionBuffer);
  gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, piramideVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

  gl.bindBuffer(gl.ARRAY_BUFFER, piramideVertexColorBuffer);
  gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, piramideVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

  setMatrixUniforms();
  gl.drawArrays(gl.TRIANGLES, 0, piramideVertexPositionBuffer.numItems);

Tarefa: Mude também para o cubo

334
335
336
337
338
gl.bindBuffer(gl.ARRAY_BUFFER, cuboVertexPositionBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, cuboVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

gl.bindBuffer(gl.ARRAY_BUFFER, cuboVertexColorBuffer);
gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, cuboVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

Tarefa: Renomeie também na declaração das variáveis

39
40
41
42
var piramideVertexPositionBuffer;
var piramideVertexColorBuffer;
var cuboVertexPositionBuffer;
var cuboVertexColorBuffer;

Agora vamos mostrar algo diferente para desenhar o cubo. Há 3 estratégias para desenhá-lo:

  • Usar um "fita" de triângulos (TRIANGLE_STRIP), no qual após os três pontos de um triângulo, você define apenas mais um ponto e ele forma um outro triângulo aproveitando os dois últimos pontos do triângulo anterior, fazendo esse processo repetidamente. O problema é que queremos faces de cores diferentes do cubo, e três faces compartilham um mesmo ponto no cubo, mas não é possível determinar 3 cores para o mesmo vértice.
  • Desenhar as 6 faces separadamente (4 vértices cada, totalizando 24 posições e 24 cores), mas aí precisaríamos chamar o drawArrays 6 vezes. Imagina todo um ambiente virtual sendo desenhado dessa forma, multiplicando as chamadas drawArrays. É bastante custoso computacionalmente. Preferimos ter o mínimo de chamadas de desenho.
  • Enviar os 8 vértices do cubo + 4 cores, e referenciá-los usando índices nos triângulos. Então os 12 triângulos do cubo (2 para cada face) fariam referência a 3 vértices e uma cor. Separando os dados dos vértices com suas referências (triângulos) faz com que você possa reusar os vértices para outros triângulos, economizando dados enviados à GPU. Obviamente você precisa adicionar as referências, mas elas são números inteiros, de poucos bytes. Vou apresentar esse modo para esclarecer um pouco (talvez seja necessário mais um ou dois tutoriais para compreender a real vantagem disso, não se preocupe).

Não vamos mais usar o drawArrays, e sim drawElements. É que o primeiro trabalha diretamente com os vértices, e o segundo trabalha com índices, referências aos vértices. Então precisamos:

  • Criar o buffer para os índices desses vértices;
  • Usá-los no momento do desenho.

Para usá-los, simplesmente execute o bindBuffer nesse buffer dos índices (para o cubo, vamos chamar de cuboVertexIndexBuffer).

Tarefa: Renomeie também na declaração das variáveis

338
339
340
341
/*---Troque também gl.ARRAY_BUFFER por gl.ELEMENT_ARRAY_BUFFER---*/
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cuboVertexIndexBuffer);
setMatrixUniforms();
gl.drawElements(gl.TRIANGLES, cuboVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);

O tipo do buffer para os índices é o ELEMENT_ARRAY_BUFFER ao invés de ARRAY_BUFFER.

A função drawElements precisa do tipo de polígono a ser desenhado, o número de polígonos (você não é obrigado a desenhar todos os triângulos naquele momento), o tipo de dado (os índices geralmente são guardados como inteiros não-negativos de 1 byte - UNSIGNED_BYTE - ou 2 bytes - UNSIGNED_SHORT) e a referência para o buffer (com o bindBuffer, não precisamos dessa opção).

Tarefa: Adicione a declaração da variável para o buffer de índices do cubo.

39
40
41
42
43
44
var piramideVertexPositionBuffer;
var piramideVertexColorBuffer;
var cuboVertexPositionBuffer;
var cuboVertexColorBuffer;
/*--Adicione esta linha--*/
var cuboVertexIndexBuffer;

Vamos atualizar os buffers lá na função iniciarBuffers.

Tarefa: Renomeie as variáveis e edite os vértices e índices dos buffers na função iniciarBuffers (buffer de posição)

172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
piramideVertexPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, piramideVertexPositionBuffer);
var vertices = [
    // Frente
      0.0,  1.0,  0.0,
    -1.0, -1.0,  1.0,
      1.0, -1.0,  1.0,
    // Direita
      0.0,  1.0,  0.0,
      1.0, -1.0,  1.0,
      1.0, -1.0, -1.0,
    // Trás
      0.0,  1.0,  0.0,
      1.0, -1.0, -1.0,
    -1.0, -1.0, -1.0,
    // Esquerda
      0.0,  1.0,  0.0,
    -1.0, -1.0, -1.0,
    -1.0, -1.0,  1.0
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
piramideVertexPositionBuffer.itemSize = 3;
piramideVertexPositionBuffer.numItems = 12; // Mudar

Tarefa: Renomeie as variáveis e edite os vértices e índices dos buffers na função iniciarBuffers (buffer de cores)

196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
piramideVertexColorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, piramideVertexColorBuffer);
var cores = [
    // Frente
    1.0, 0.0, 0.0, 1.0,
    0.0, 1.0, 0.0, 1.0,
    0.0, 0.0, 1.0, 1.0,
    // Direita
    1.0, 0.0, 0.0, 1.0,
    0.0, 0.0, 1.0, 1.0,
    0.0, 1.0, 0.0, 1.0,
    // Trás
    1.0, 0.0, 0.0, 1.0,
    0.0, 1.0, 0.0, 1.0,
    0.0, 0.0, 1.0, 1.0,
    // Esquerda
    1.0, 0.0, 0.0, 1.0,
    0.0, 0.0, 1.0, 1.0,
    0.0, 1.0, 0.0, 1.0
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(cores), gl.STATIC_DRAW);
piramideVertexColorBuffer.itemSize = 4;
piramideVertexColorBuffer.numItems = 12; // Mudar

Tarefa: Agora para o cubo => Renomeie as variáveis e edite os vértices e índices dos buffers na função iniciarBuffers (buffer de posições)

220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
cuboVertexPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, cuboVertexPositionBuffer);
vertices = [
  // Frente
  -1.0, -1.0,  1.0,
   1.0, -1.0,  1.0,
   1.0,  1.0,  1.0,
  -1.0,  1.0,  1.0,

  // Trás
  -1.0, -1.0, -1.0,
  -1.0,  1.0, -1.0,
   1.0,  1.0, -1.0,
   1.0, -1.0, -1.0,

  // Topo
  -1.0,  1.0, -1.0,
  -1.0,  1.0,  1.0,
   1.0,  1.0,  1.0,
   1.0,  1.0, -1.0,

  // Base
  -1.0, -1.0, -1.0,
   1.0, -1.0, -1.0,
   1.0, -1.0,  1.0,
  -1.0, -1.0,  1.0,

  // Direita
   1.0, -1.0, -1.0,
   1.0,  1.0, -1.0,
   1.0,  1.0,  1.0,
   1.0, -1.0,  1.0,

  // Esquerda
  -1.0, -1.0, -1.0,
  -1.0, -1.0,  1.0,
  -1.0,  1.0,  1.0,
  -1.0,  1.0, -1.0,
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
cuboVertexPositionBuffer.itemSize = 3;
cuboVertexPositionBuffer.numItems = 24; // Mudar

No OpenGL (e WebGL) se dois vértices tiverem qualquer um dos atributos diferentes entre si (posição ou cor ou coordenada de textura ou ...), mesmo que todos os outros sejam idênticos, então eles são diferentes vértices. Nesse exemplo, os dados que se repetem são as cores (4 vezes). Então vamos criar as cores distintas e replicá-los.

Tarefa: Renomeie as variáveis e edite os vértices e índices dos buffers na função iniciarBuffers (buffer de cores)

263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
cuboVertexColorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, cuboVertexColorBuffer);
cores = [
  [1.0, 0.0, 0.0, 1.0],     // Frente
  [1.0, 1.0, 0.0, 1.0],     // Trás
  [0.0, 1.0, 0.0, 1.0],     // Topo
  [1.0, 0.5, 0.5, 1.0],     // Base
  [1.0, 0.0, 1.0, 1.0],     // Direita
  [0.0, 0.0, 1.0, 1.0],     // Esquerda
];
var coresReplicadas = [];
for (var i in cores) {
  var cor = cores[i];
  for (var j=0; j < 4; j++) {
    coresReplicadas = coresReplicadas.concat(cor);
  }
}
/*---Veja que coresReplicadas está sendo usado, e não cores---*/
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(coresReplicadas), gl.STATIC_DRAW);
cuboVertexColorBuffer.itemSize = 4;
cuboVertexColorBuffer.numItems = 24;

E agora vamos criar os nossos índices para desenhar nosso cubo. Se um triângulo precisar dos vértices que estão na segunda, quarta e quinta posições do buffer (de posição ou de cor), então precisamos inserir os valores 1, 3 e 4 (o primeiro está no índice 0).

Tarefa: Adicione esse código abaixo do último código supracitado na função iniciarBuffers.

284
285
286
287
288
289
290
291
292
293
294
295
296
cuboVertexIndexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cuboVertexIndexBuffer);
var indices = [
  0, 1, 2,      0, 2, 3,    // Frente
  4, 5, 6,      4, 6, 7,    // Trás
  8, 9, 10,     8, 10, 11,  // Topo
  12, 13, 14,   12, 14, 15, // Base
  16, 17, 18,   16, 18, 19, // Direita
  20, 21, 22,   20, 22, 23  // Esquerda
]
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
cuboVertexIndexBuffer.itemSize = 1;
cuboVertexIndexBuffer.numItems = 36;

Lembra do UNSIGNED_SHORT? Ele usa 2 bytes (16 bits), por isso estamos usando Uint16Array.

Exercícios:

  • Cor do Cubo
    • Se o cubo for de apenas uma cor, a variável vertices só precisa de 8 vértices e cores de 1 cor (o número de índices é o mesmo usando gl.TRIANGLES). Tente fazer isso.
    • Tipo de Primitivas de Desenhos
  • Crie Novas Formas Geométricas
    • Deve-se Alterar o Modelo para que o Cubo Orbite o Triangulo, sendo que os dois não devem sair da Tela.
    • Baseado no último dígito do número de matrícula, desenha uma das figuras abaixo e posicione rotacionando sobre o Eixo X,Y com Shier Aplicado de 20Graus no Plano X.
    • Os .JS serão usados as mesmas versões do Tutorial
<< T03: Movimentando Formas T06: Texturas >>