WebGL Tutorial 3 – Un poquito de movimiento

Escrito por el 26 octubre, 2011

¡Bienvenidos al tercer tutorial de la serie Aprende webGL! Esta vez vamos a hacer que los objetos se muevan un poco en la escena. De nuevo, este artículo es una traducción casi literal de su respectivo tutorial de la web Learning WebGL, que a su vez está basado en el número 4 de los tutoriales de 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, una advertencia: 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 o el segundo tutorial, deberías hacerlo, pues aquí pasaremos a comentar los cambios que tenemos que hacer al código de la última lección, para conseguir un poco de movimiento.

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.

Antes de empezar a describir el código, quiero aclarar una cosa. El modo de animar escenas 3D en webGL es muy simple, basta con dibujar el objeto muchas veces en un segundo, dibujándolo en una posición diferente cada vez. Ésto debería de ser obvio para la mayoría de los lectores, pero nunca está de más comentarlo por si acaso por si alguien es un completo desconocedor del increíble mundo de las animaciónes por ordenador. Resumiendo, webGL no tiene funciones para mover y rotar, éstas tienes que calcularlas tú en javascript y comunicárselas a webGL.

De todas formas, esto significa que nuestro código necesita de una nueva función que mueva un poquito los objetos por la escena, y que sea utilizada justo después de hacer la llamada a drawScene, que es la que se encarga de dibujar todo.

/*function webGLStart() {
    var canvas = document.getElementById("lesson03-canvas");
    initGL(canvas);
    initShaders();
    initTexture();

    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.enable(gl.DEPTH_TEST);*/


    tick();
/*}*/

El único cambio que hacemos es que en lugar de llamar a drawScene con un intervalo para dibujar la escena, utilizamos una nueva función llamada tick. Esta función debe ser llamada con regularidad; se encargará de actualizar el estado de animación de la escena (por ejemplo, mover la rotación del triángulo de 81º a 82º) y a continuación dibujar la escena. También se ocupará de llamarse a sí mismo en el momento oportuno. Echémosle un vistazo, por partes:

function tick() {
    requestAnimFrame(tick);

La primera linea es donde la función tick se encargará de llamarse a sí misma cuando se vuelva a pintar el escenario. requestAnimFrame es una función de una librería javascript de Google con utilidades para webGL, webGL-utils.js, incluída en una etiqueta <script> en la parte superior del HTML. La razón por la que usamos esa librería es que nos proporciona una manera, no dependiente del navegador web, de notificar al navegador que debe volver a pintar la escena webGL. En este momento, rodos los navegadores disponen de una función que hace ésto, pero tienen nombres diferentes (por ejemplo, en Firefox se llama mozRequestAnimationFrame, mientras que en Chrome es webkitRequestAnimationFrame). En el futuro, se espera que todos usen requestAnimationFrame. Hasta entonces, para no complicar el código, haremos uso de las utilidades de Google para que las llamadas a este tipo de funciones sin estándar funcionen en todos los navegadores (al menos los que soportan webGL).

Vale la pena señalar que se podría conseguir un efecto similar al uso de requestAnimFrame, pidiéndole que sea javascript es que llame a la función drawScene con regularidad, por ejemplo con la función (como hacíamos en las anteriores lecciones) setInterval. Un montón de los primeros demos y ejemplos de webGL están hechos así, y funcionan bien, salvo que algún usuario abra a la vez varias páginas con webGL en diferentes pestañas del navegador. Dado que el setInterval de javascript se ejecuta esté la pestaña abierta o no, ésto suponía un increíble desperdicio de rendimiento, que podría acabar perjudicando a la velocidad de ejecución de los javascripts. Sin embargo, requestAnimFrame sólo se llama cuando el canvas donde se dibuja la escene está visible.

Y para terminar con la función tick:

    drawScene();
    animate();
}

Una vez que hemos conseguido que el navegador llame a nuestra función cada vez que el sistema quiera repintar el frame (el marco, sobre nuestro lienzo canvas), lo que debemos a hacer es pintar los objetos y actualizar su estado para la siguiente rellamada. Veámos como cambian drawScene. Lo primero es indicar que necesitamos dos nuevas variables globales:

var rTri = 0;
var rSquare = 0;

Estas variables las usaremos para controlar la rotación del triángulo y el cuadrado, respectivamente. Ambas comienzan a rotar cero grados, y poco a poco irán incrementándose para que roten los objetos más y más. Quiero indicar que utilizar variables globales para cosas como ésta es una muy mala práctica en aplicaciones reales de programas que usen 3D. En lecciones posteriores mostraré una forma mucho más elegante y adecuada de hacerlo. Pero para este ejemplo sencillo, lo dejaremos así.

El siguiente cambio en drawScene se hace en el momento de dibujar el trángulo. Mostraré todo el código involucrado, con las partes que no se tocan comentadas, para que destaque lo nuevo:

    /*mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pMatrix);

    mat4.identity(mvMatrix);

    mat4.translate(mvMatrix, [-1.5, 0.0, -7.0]);*/


    mvPushMatrix();
    mat4.rotate(mvMatrix, 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);*/


    mvPopMatrix();

Recordemos lo que dije en la lección 1:

En openGL, cuando estás dibujando una escena, necesitas decir para cada cosa que vas a dibujar una posición actual con una rotación actual. Por ejemplo, dirías “muévete 20 unidades hacia delante, rota 32 grados, y a continuación dibuja el robot”, siendo “dibujar el robot” otra serie de complejos movimientos tipo “muévete mucho, gira un poco, dibuja este trozito de robot”. Esto es útil hacerlo así porque puedes encapsular todo el código de “dibujar el robot” en una sola función, y luego mover fácilmente el robot por el escenario con sólo especificar una posición y rotación inicial antes de llamar a la función “dibujar robot”.

Como recordarás, el estado actual está guardado en la matriz modelo-vista. Teniendo en cuenta ésto, veamos detenidamenet las lineas nuevas del código de antes:

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

Es bastante autoexplicativo: Estamos cambiando nuestro estado de rotación actual de la matriz modelo-vista, rotando rTri grados sobre el eje vertical Y (el cual es especificado con una lista en el tercer argumento). Con esto conseguimos que cuando el triángulo vuelva a ser dibujado, haya rotado horizontalmente rTri grados. Notar que la función mat4.rotate acepta ángulos en radianes, por lo que tenemos que hacer una pequeña conversión de grados a radianes con la función degToRad.

Ahora, ¿qué es eso de llamar a mvPushMatrix y a mvPopMatrix? Como habrás supuesto, ambas funciones están relacionadas con la matriz de modelo-vista (mvMatrix). Si has usado el tipo de lista pila, te serán muy familiares, porque son eso, una pila donde almacenaremos los estados que vaya teniendo la matriz de modelo-vista, para poder volver al estado anterior rápidamente. Volviendo al ejemplo de dibujar un robot, digamos que un código de más alto nivél debería tener una función para movernos al punto A, otra para dibujar al robot, luego hacer algún tipo de desplazamiento desde el punto A, y finalmente dibujar la famosa tetera. El código que dibuja el robot puede hacer todo tipo de cambios en la matriz de modelo-vista. Podría comenzar a dibujar las piernas, desplazarse para dibujar el cuerpo, los brazos, y la cabeza. El problema es una vez acabado el robot, me desplazo para dibujar la tetera, no me movería desde la posición A, como queríamos, si no desde la última posición que teníamos dibujando al robot, lo que significaría, suponiendo que lo último que se ha pintado del robot es la cabeza, que la tetera se dibujaría a saber en qué dirección, y flotando algunas unidades (la altura del robot) del suelo, lo que no parece demasiado bueno para nuestros intereses.

Así que es imperativo utilizar alguna forma de almacenar el estado de la matriz modelo-vista antes de empezar a dibujar el robot, y restaurarla justo después de acabar con el robot. Por supuesto ésto es lo que hace mvPushMatrix y mvPopMatrix. mvPushMatrix pone la matriz de modelo-vista actual en la cima de una pila, y mvPopMatrix se deshace de la matriz modelo-vista actual, y la reemplaza por la matriz que esté en la cima de la pila, sacándola. Usando pilas podemos tener cualquier número de llamadas recursivas (anidadas) a funciones de dibujo complejas, ya que cada llamada guardaría  la matriz actual antes de empezar a trabajar, y la restauraría antes de retornar el control a la función que llama. Así que una vez que hemos terminado de dibujar nuestro triángulo rotado, restauramos la matriz de modelo-vista con mvPopMatrix para que éste código antiguo…

    mat4.translate(mvMatrix, [3.0, 0.0, 0.0]);

…mueva correctamente la posición de la matriz actual hasta donde debemos pintar en cuadrado, sin que sea afectada por la rotación que hicimos con la matriz para dibujar el triángulo. Recomiendo copiar y modificar el código eliminando las funciones push y pop, para ver qué pasaría sin ellas, es una forma de entender rápidamente la utilidad de la pila de matrices.

Resumiendo, estos tres cambios hacen que el triángulo gire alrededor de su eje vertical central sin afectar al rectángulo. Ahora veamos los tres cambios similares que hay que hacer al cuadrado para que rote sobre su eje horizontal central, para variar un poco:

    mvPushMatrix();
    mat4.rotate(mvMatrix, degToRad(rSquare), [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);*/


    mvPopMatrix();
}

…y éstos son todos los cambios que hay que hacer a drawScene.

Otra cosa que tenemos que hacer para animar nuestra escena es cambiar los valores de rTri y rSquare a lo largo del tiempo, de modo que cada vez que se dibuje la escena, sea un poco diferente. Ésto es el trabajo que tendrá que realizar nuestra nueva función animate:

var lastTime = 0;
function animate() {
    var timeNow = new Date().getTime();
    if (lastTime != 0) {
        var elapsed = timeNow - lastTime;

        rTri += (90 * elapsed) / 1000.0;
        rSquare += (75 * elapsed) / 1000.0;
    }
    lastTime = timeNow;
}

La forma más sencilla de animar sería sumar una cantidad fija de grados a las variables de rotación de nuestro triángulo y cuadrado, pero nosotros vamos a usar un método más avanzado conocido como tiempo delta. El tiempo delta consiste en determinar el número de milisegundos transcurridos desde la última ejecución de la función animate, y mover  los objetos en consecuencia según la velocidad por segundo que nosotros le hayamos dicho que tienen. En nuestro ejemplo, para determinar el tiempo en milisegundos transcurridos, usamos una variable global, lastTime, que inicialmente está  a cero. Dentro de la función, lo primero que hacemos es calcular el número de milisegundos que han transcurrido desde la última vez que se ejecutó la función, y guardamos el valor en la variable local elapsed. Finalmente, calculamos el número de grados que debe rotar en ese tiempo transcurrido sabiendo que el triángulo rota a 90 grados por segundo, y el cuadrado lo hace a 75 grados por segundo. ¿Por qué usamos éste método más complicado, en vez de una suma directa de un pequeño número de grados? Pues es sencillo. Una máquina lenta tardará más que una máquina rápida en pintar cada frame. Eso quiere decir que las animaciones no se verían igual en las dos máquinas; en la lenta rotarían más lentamente, y en la rápida irían a toda pastilla. Incluso en una misma máquina, si por alguna razón se produce un retardo en llamar a la función, veremos una animación que de repente parece acelerar o frenar. Con el método de tiempo delta, nos aseguramos a que el objeto siempre mantenga una velocidad de animación constante, y que se vea igual en toda clase de ordenadores lentos o rápidos (siempre que tengan un mínimo de potencia para mover webgl con cierta fluidez, claro).

Vamos a ver el código de apoyo que hemos tenido que añadir para manejar las pilas de matrices, mvPushMatrix y mvPopMatrix:

/*var mvMatrix = mat4.create();*/
var mvMatrixStack = [];
/*var pMatrix = mat4.create();*/

function mvPushMatrix() {
    var copy = mat4.create();
    mat4.set(mvMatrix, copy);
    mvMatrixStack.push(copy);
}

function mvPopMatrix() {
    if (mvMatrixStack.length == 0) {
        throw "Invalid popMatrix!";
    }
    mvMatrix = mvMatrixStack.pop();
}

Espero que no haya nada que te sorprenda. Tenemos una lista que actuará de pila (stack) de matrices, y usamos las funciones push y pop que ya vienen hechas en javascript para manejar listas como si fueran pilas, junto con algunos métodos de glMatrix para crear matrices modelo-vista vacías, y un mat4.set para llenar esa matriz recién creada con la matriz modelo-vista actual.

Y por último sólo quedaría la función de conversión grados radianes, que no tiene ninguna complicación si sabemos cómo hacer la conversión matemáticamente:

function degToRad(degrees) {
        return degrees * Math.PI / 180;
}

¡Y ésto es todo para esta lección! Ahora deberías de ser capaz de animar escenas sencillas con webGL. Si tienes alguna pregunta o sugerencia, postea en los comentarios y veremos si entre todos podemos solucionarla.

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.

El código para mvPushMatrix y mvPopMatrix está adaptado del creador de criaturas del juego Spore de Vladimir Vukicevik. Gracias también a Google por publicar su muy útil fichero de ayudas webgl-utils.js y, por supuesto, estoy profundamente agradecido a NeHe por sus tutoriales de openGL que usé como base para el código de esta lección.

Etiquetas: , ,

¿Tienes algo que decir?

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