WebGL Tutorial 4 – Algunos objetos de auténtico 3D

Escrito por el 27 Octubre, 2011

Bienvenido al cuarto tutorial de la serie Aprende webGL. Esta vez vamos a mostrar auténticos objetos en 3D.  Es una traducción no literal de su respectivo tutorial en Learning webGL, que a su vez está basada en el capítulo 5 del tutorial sobre openGL de NeHe.

Si tu navegador soporta webGL, este video muestra lo que deberías ver:

Abre este enlace para ver el ejemplo en vivo de webGL. Para poder verlo en acción, tendrás que usar un navegador que soporte webGL, como Mozilla Firefox o Google Chrome.

Más información acerca de cómo funciona todo, abajo…

Pero antes, la advertencia habitual: Estas lecciones están pensadas para personas con unos razonables conocimientos de programación previa, pero sin experiencia en gráficos 3D; el objetivo que buscamos es que podáis poner en marcha en poco tiempo vuestras propias páginas web con 3D, con una buena comprensión de lo que está ocurriendo en el código en cada momento. Si no has leído el primer, el segundo, y el tercer tutorial, deberías hacerlo, pues aquí pasaremos a comentar los cambios que tenemos que hacer al código de la última lección, para sustituir los objetos 2D por otros en 3D.

Como antes, puede que hayan conceptos erróneos en este tutorial. Si ves algo que sea mejorable, no dudes en comentármelo para que lo corrija.

Para ver el código de la lección de hoy al completo, abrid la página con el ejemplo y mirad el código fuente. Una vez que te hayas copiado el código, abre tu editor de textos favoritos y échale un vistazo.

Las diferencias entre el código de esta lección con el de la anterior lección, se encuentran en las funciones animate, initBuffers y drawScene. Si empezamos mirando la función animate, veremos que sólo hay un pequeño cambio: las variables donde guardábamos el estado de la rotación de cada objeto, rTri y rSquare, han sido renombradas, sólo por tener un nombre más acorde a la nueva realidad. Antes teníamos un triángulo y un cuadrado, y ahora vamos a dibujar una pirámide y un cubo. Además, el sentido de rotación del cubo lo hemos cambiado, por puros motivos estéticos. Ahora tenemos:

    rPyramid += (90 * elapsed) / 1000.0;
    rCube -= (75 * elapsed) / 1000.0;

Eso es todo para ésta función. Ahora pasemos a modificar drawScene. Justo encima de la declaración de la función, sustituímos la definición de las nuevas variables:

var rPyramid = 0;
var rCube = 0;

Luego viene la cabecera de la función, seguido por el código de configuración y el que nos mueve a la posición donde se va a dibujar la pirámide. Ahora viene donde rotamos la figura. Y como antes, simplemente cambiamos la antigua variable de rotación por la nueva:

    mat4.rotate(mvMatrix, degToRad(rPyramid), [0, 1, 0]);

…Y luego se dibuja la pirámide. La única diferencia con el código de la lección anterior es que igual que antes, renombraremos los nombres de los búferes de colores y posiciones para tener nombres autoexplicativos, lo cual es siempre una muy buena práctica de programación, aunque ahora sea un poco pesado.

    gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexPositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, pyramidVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

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

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

De acuerdo, de momento todo es sencillísimo. Ahora pasamos al cubo. El primer paso, es hacerlo girar, pero esta vez, en lugar de hacerlo rotar en el eje X, vamos a hacerlo girar de un eje en diagonal cruzada, concretamente, si miráramos el cubo de frente, lo vamos a hacer rotar sobre la diagonal que iría desde el vértice superior derecho delantero, hasta el vértice inferior izquierdo trasero. Así podremos apreciar todas sus caras, algo que con la rotación que le hemos puesto a la pirámide, no vamos a conseguir (nunca veremos su cara base). Y como verás en el código, explicarlo es más difícil que hacerlo:

    mat4.rotate(mvMatrix, degToRad(rCube), [1, 1, 1]);

A continuación, dibujamos el cubo. Es un poco más complicado. Ahora tenemos 3 formas de dibujar un cubo:

  1. Usando triangle strip (Espero que recuerdes lo que era, si no, acude a la lección 1). Si el cubo fuera de un sólo color, sería bastante fácil hacerlo, se podrían utilizar las posiciones de los vértices que hemos usado hasta ahora para dibujar la cara delantera, a continuación, añadir dos puntos más para añadir otra cara, y otros dos puntos más para otra, etc. Esto sería muy eficiente. Por desgracia, queremos que cada cara tenga un color diferente. Debido a que cada vértice se especifica una esquina del cubo, y que cada esquina se reparte entre tres caras, tendríamos que especificar cada vértice tres veces, y hacerlo sería tan complicado que ni siquiera voy a tratar de explicarlo.
  2. Podríamos hacer trampa, y dibujar nuestro cubo con seis cuadrados por separado, uno por cada cara, cada uno con su propio buffer de posiciones y otro de color, como ya hemos visto. La primera versión de este tutorial lo hice así, y funcionaba muy bien. Sin embargo, no es una buena práctica, ya que webGL tarda cierta cantidad de tiempo al gestionar cada objeto de la escena, con lo que estaríamos perdiendo un poco de rendimiento. Además existe otra forma de dibujar un cubo con menos llamadas a la función drawArrays, es decir, también se tardaría menos en dibujarlo.
  3. La última opción es definir un cubo con seis cuadrados, cada uno compuesto de dos triángulos (método triangle), bastante parecido al punto 2, pero esta vez, enviándoselos todos juntos a webGL para que lo dibuje de una sola vez, y no en seis llamadas. Esta forma es similar a como lo haríamos usando triangle strip, a excepción de que estamos especificando los triángulos en su totalidad cada vez en vez de ir dibujando trozo a otro añadiendo nuevos puntos para ir formando las caras. Así es más fácil elegir un color para cada cara, el código es más elegante, y además me da la oportunidad de presentar una nueva función, drawElements, asi que sera así como lo hagamos 🙂

El primer paso es renombrar los buffer que contienen las posiciones de los vértices del cubo y sus colores tal como hicimos con la piramide.

    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, cubeVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

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

El siguiente paso es dibujar los triángulos que formarán el cubo. Hay un problema en esto. Imagina la cara frontal, tenemos cuatro posiciones de los vértices y cada uno de ellos tiene un color asociado. Y hay que dibujarla mediante dos triángulos (modo de dibujo triangles). Sin embargo, el modo triangles necesita tres vértices para cada triángulo, y como necesitamos dos para dibujar el cuadrado, necesitaríamos seis vértices, pero sólo tenemos cuatro en nuestro buffer.

Lo que queremos hacer es pedirle a webGL que dibuje un triángulo con los tres primeros vértices del buffer, a continuación dibuje el otro con el primer, tercer y cuarto vértice del buffer. La preparación del resto del cubo sería similar. Y eso será exactamente lo que haremos.

Usaremos otro buffer, pero del tipo element array buffer, y una nueva función, drawElements, para esto. Al igual que con los buffers (del tipo array buffer) que hemos estado usando hasta ahora, el buffers de lista de elementos se llenará con valores apropiados en initBuffers, que serán los índices de los elementos contenidos en los otros buffers, el de vértices y el de colores, con índice de base cero, cómo se relacionan para ir pintando los triángulos. En el código se verá un poco más claro.

Con el fin de dibujar el cubo, hacemos que webGL use como buffer actual nuestro element array buffer, con la orden que ya conocemos gl.bindBuffer, y por fin, pasamos a dibujar los triángulos que formarán el cubo con drawElements.

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);
    setMatrixUniforms();
    gl.drawElements(gl.TRIANGLES, cubeVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);

Y con ésto acabamos drawScene. El resto del código está en initBuffers, y es bastante autoexplicativo. Se definen los buffers de posiciones de vértices y colores con los nuevos nombres que les hemos puesto, y se añade el nuevo tipo de buffer necesario para dibujar el cubo de forma eficiente.

var pyramidVertexPositionBuffer;
var pyramidVertexColorBuffer;
var cubeVertexPositionBuffer;
var cubeVertexColorBuffer;
var cubeVertexIndexBuffer;

A continuación ponemos los valores de las posiciones de los vértices de la pirámide, cambiando también su numItems:

    pyramidVertexPositionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexPositionBuffer);
    var vertices = [
        // Front face
         0.0,  1.0,  0.0,
        -1.0, -1.0,  1.0,
         1.0, -1.0,  1.0,
        // Right face
         0.0,  1.0,  0.0,
         1.0, -1.0,  1.0,
         1.0, -1.0, -1.0,
        // Back face
         0.0,  1.0,  0.0,
         1.0, -1.0, -1.0,
        -1.0, -1.0, -1.0,
        // Left face
         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);
    pyramidVertexPositionBuffer.itemSize = 3;
    pyramidVertexPositionBuffer.numItems = 12;</pre>
...lo mismo para el buffer de color de pa pirámide:
<pre>    pyramidVertexColorBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexColorBuffer);
    var colors = [
        // Front face
        1.0, 0.0, 0.0, 1.0,
        0.0, 1.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0,
        // Right face
        1.0, 0.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0,
        0.0, 1.0, 0.0, 1.0,
        // Back face
        1.0, 0.0, 0.0, 1.0,
        0.0, 1.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0,
        // Left face
        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(colors), gl.STATIC_DRAW);
    pyramidVertexColorBuffer.itemSize = 4;
    pyramidVertexColorBuffer.numItems = 12;

…y para el buffer con la posición de los vértices del cubo:

    cubeVertexPositionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
    vertices = [
      // Front face
      -1.0, -1.0,  1.0,
       1.0, -1.0,  1.0,
       1.0,  1.0,  1.0,
      -1.0,  1.0,  1.0,

      // Back face
      -1.0, -1.0, -1.0,
      -1.0,  1.0, -1.0,
       1.0,  1.0, -1.0,
       1.0, -1.0, -1.0,

      // Top face
      -1.0,  1.0, -1.0,
      -1.0,  1.0,  1.0,
       1.0,  1.0,  1.0,
       1.0,  1.0, -1.0,

      // Bottom face
      -1.0, -1.0, -1.0,
       1.0, -1.0, -1.0,
       1.0, -1.0,  1.0,
      -1.0, -1.0,  1.0,

      // Right face
       1.0, -1.0, -1.0,
       1.0,  1.0, -1.0,
       1.0,  1.0,  1.0,
       1.0, -1.0,  1.0,

      // Left face
      -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);
    cubeVertexPositionBuffer.itemSize = 3;
    cubeVertexPositionBuffer.numItems = 24;

El buffer de color es ligeramente más complejo, ya que utilizaremos un bucle para crear la lista de colores de los vértices, para  no tener que escribir a mano cuatro veces cada uno de los seis colores en el buffer (una vez por cada vértice de la cara de ese color):

    cubeVertexColorBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexColorBuffer);
    colors = [
      [1.0, 0.0, 0.0, 1.0],     // Front face
      [1.0, 1.0, 0.0, 1.0],     // Back face
      [0.0, 1.0, 0.0, 1.0],     // Top face
      [1.0, 0.5, 0.5, 1.0],     // Bottom face
      [1.0, 0.0, 1.0, 1.0],     // Right face
      [0.0, 0.0, 1.0, 1.0],     // Left face
    ];
    var unpackedColors = [];
    for (var i in colors) {
      var color = colors[i];
      for (var j=0; j < 4; j++) {
        unpackedColors = unpackedColors.concat(color);
      }
    }
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(unpackedColors), gl.STATIC_DRAW);
    cubeVertexColorBuffer.itemSize = 4;
    cubeVertexColorBuffer.numItems = 24;

Finalmente, definimos el element array buffer (nótese la diferencia del primer parámetro en el gl.bindBuffer y el gl.bufferData):

    cubeVertexIndexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);
    var cubeVertexIndices = [
      0, 1, 2,      0, 2, 3,    // Front face
      4, 5, 6,      4, 6, 7,    // Back face
      8, 9, 10,     8, 10, 11,  // Top face
      12, 13, 14,   12, 14, 15, // Bottom face
      16, 17, 18,   16, 18, 19, // Right face
      20, 21, 22,   20, 22, 23  // Left face
    ]
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(cubeVertexIndices), gl.STATIC_DRAW);
    cubeVertexIndexBuffer.itemSize = 1;
    cubeVertexIndexBuffer.numItems = 36;

Recordar que cada número en este buffer corresponde al índice de los buffers de vértices y colores que debe utilizar para dibujar cada triángulo (hay que dibujar seis caras, necesitamos 12 triángulos, los cuales podemos apreciar en el buffer de elementos del código anterior). Pero recordad que ese índice no es exactamente el índice del array del buffer (es decir, el típico buffer[indice]), si no el índice al elemento que contiene, que en el caso del buffer de vértices, corresponde a 3 posiciones consecutivas en el array (para las coordenadas X, Y y Z), y a cuatro posiciones consecutivas en el array de colores (un color ocupaba 4 números). Por ejemplo, el primer triángulo usará los elementos 0, 1 y 2 contenidos tanto en el buffer de vértices como en el de colores, para pintar el primer triángulo, y los elementos 0, 2 y 3 para el segundo. Ambos juntos forman la primera cara del cubo. Repetimos lo mismo con todos los triángulos, y obtenemos un perfecto cubo.

Ahora ya conoces cómo crear escenas usando objetos 3D, y también a reusar los vértices que has especificado en los buffers (array buffers) usando el tipo element array buffer y drawElements. Si tienes alguna pregunta, o sugerencia, por favor, deja un comentario abajo, e intentaremos entre todos ayudarte.

Agradecimientos: Naturalmente agradezco a Giles Thomas, creador de learning webGL, ya que esta serie de tutoriales son traduciones casi literales de sus respectivas lecciones en inglés, que son de lo mejorcito que podemos encontrar para introducirnos en esta muy novedosa tecnología web. Él, a su vez, agradece la ayuda de terceras personas, que por cortesía, voy a reproducir a continuación.

Como siempre, estoy profundamente agradecido a NeHe por sus tutoriales de openGL que usé como base para el código de esta lección. Chris Marrin y su caja giratoria fueron la inspiración para introducir el tipo element array buffer en esta lección.

Etiquetas: , ,

Comentarios (3)

  • Excelentes tutoriales !!

    Donde explicas las 3 formas de crear el cubo…
    Creo que se podría construir también, usando 1 solo buffer con las posiciones de los vértices de todos sus lados.

    He estado siguiendo tus tutoriales paso a paso, pero, creando mis propias figuras, por lo que cree una clase para crear figuras distintas, donde lo único que hago es entregarle una matriz para vértices y otra para colores. No quiero arruinar la calidad de tu tutorial colocando código, por lo que te cuento que me ha funcionado muy bien, ya que al crear cada figura, la clase, calcula la cantidad de vértices, asumiendo que siempre vienen grupos de (x,y,z), por lo que en drawScene solo usaba gl.drawArrays(gl.TRIANGLE_STRIP, 0, forma.get(“vertexBufferNumItem”));

    Entonces nace mi duda: ¿ habría una GRAN diferencia entre usar TRIANGLE o TRIANGLE_STRIP?,
    Por que la verdad es que, al crear una clase para crear formas, me di cuenta que podía crear todas las formas 2d y 3d que imaginara, simplemente indicándole donde están los vértices.

    Discúlpame si la respuesta esta en los tutoriales que siguen, pero la verdad es que he tenido que agudizar mi capacidad lectora para leer detenidamente y entender este preciado material, y no quiero enredarme con conceptos futuros sin tener claro este que te comento.

    Muchas gracias por estos conocimientos,

    Saludos
    ion

  • No estaría mal que compartieras tu clase para crear figuras, para que otros puedan tener una base sobre la que crear sus propios experimentos. La mejor forma de aprender es ver código de otros, esté mejor o peor programado.
    ¿Habría una GRAN diferencia entre usar TRIANGLE o TRIANGLE_STRIP? Sí pero su grandeza depende de la cantidad de polígonos que tenga el objeto que estás pintando. Para los ejemplos de éstos tutoriales, prácticamente esa diferencia es insignificante.
    Y la diferencia se debe al funcionamiento de cada uno, como dije en el primer tutorial. El gl.TRIANGLES crea triángulos independientes unos de otros. Poniendo cada triángulo en la posición correcta, puedes crear cualquier figura. Cada uno de esos triángulos necesita 3 vértices. Si quiero hacer un cuadrado, por ejemplo, necesito “pegar” dos triángulos. Y necesitaría indicar 6 vértices. Sin embargo, dos de los vértices de un triángulo estarían exáctamente encima de dos de los vértices del otro triángulo.
    Hay una manera más eficiente de evitar duplicar información que realmente no es necesaria, a la hora de dibujar un polígono, y es usar tiras de triángulos gl.TRIANGLE_STRIP. Para el ejemplo del cuadrado, sólo necesitaría indicarle 4 vértices :A,B,C,D. El primer triángulo lo crearía con A,By C. Y el segundo triángulo, usaría B, C y D. Y así, me he ahorrado duplicar 2 vértices, con la consecuente ahorro de memoria y eficiencia en tiempo de ejecución.
    Cuando el ejemplo es pequeño, no se nota la mejora. Pero si tuvieras miles de polígonos, la diferencia de rendimiento de uno y otro sistema es bastante grande.

  • Estuve dándole vueltas a la Clase de la que te comentaba, para hacerla un poco más útil de lo que era en un principio, por lo que el código no se entenderá mejor en un ejemplo:

    http://www.efortia.com/pruebas/space.html

    La clase sirve para crear tanto formas planas como 3d, se le envía como parámetros los vértices y los indices para crear las formas. Posee un propio método initDraw para almacenarse en buffer y luego dibujarlas en drawScene.

    Podrás ver los comentarios de la clase en el código fuente. Cualquier comentario me sería muy útil y si le sirve a alguien, pues mejor aún.

    Desarrollando la clase, entendí más profundamente el como webGL crea las figuras en base a los vértices que le entregamos y los indices.

    Ahora ya tengo mucho más clara esta parte, continuaré con el próximo tutorial.

    Muchas Gracias por toda esta información.

    Saludos.

¿Tienes algo que decir?

Gestionado con Wordpress y Stripes Theme Entradas (RSS) | Comentarios (RSS)