WebGL Tutorial 6 – Eventos de teclado y filtros de textura

Escrito por el 2 noviembre, 2011

Bienvenido al sexto tutorial de la serie Aprende webGL. Es una traducción no literal de su respectivo tutorial en Learning webGL, que a su vez está basada en el capítulo 7 del tutorial sobre openGL de NeHe. En esta lección veremos cómo hacer que tu página en webGL detecte las pulsaciones de teclado, que usaremos para cambiar la velocidad y dirección de los giros de un cubo que usará texturas mapeadas, sobre las cuales aplicaremos algunos tipos de filtros que van desde una calidad baja, pero rápida, a una calidad muy alta, pero más lenta de calcular. El tutorial de NeHe va mas allá, incluyendo la gestión de la iluminación de la escena, pero ese tema lo veremos en un futuro porque supondría mucho trabajo para un sólo tutorial.

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

Abre este enlace para ver el ejemplo en vivo de webGL. Y aquí tienes la imagen GIF para descargarla y probarlo en local. 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 los cinco primeros tutoriales, deberías hacerlo, pues aquí pasaremos a comentar los cambios que tenemos que hacer al código de la última lección para detectar las pulsaciones de teclado y aplicar filtros a nuestra textura.

Como siempre, puede que hallan 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.

El mayor cambio que vamos a hacer con respecto al tutorial 5 de texturas, es que vamos a detectar los eventos de teclado, pero será más fácil de explicar cómo funciona si empezamos a ver el código. Definimos nuevas variables globales:

  var xRot = 0;
  var xSpeed = 0;

  var yRot = 0;
  var ySpeed = 0;

  var z = -5.0;

  var filter = 0;

xRot e yRot te deberían ser familiares (zRot desaparece). Ellas representan la rotación actual del cubo en los ejes X e Y. xSpeed e ySpeed permitirán al usuario variar la velocidad de la rotación del cubo usando los cursores del teclado (las flechas). Es decir, determinarán la velocidad de cambio de las variables xRot e yRot. Además, con las teclas de RePag y AvPag (retroceder y avanzar página) haremos que la coordenada Z del cubo cambie, es decir, hacer que el cubo se aleje o se acerque. Señaolo de que es el cubo el que se mueve, no la cámara (como en los videojuegos 3D). Ésa lección la dejaremos para otro día. Para ello usamos la variable z. Finalmente, filter contendrá un entero dentro del rango [0,2], incluídos los extremos (por eso uso corchetes en vez de paréntesis),  que indicará cuál de los 3 filtros se utilizará para renderizar la textura del cubo.

Pasemos ahora al código que maneja los filtros ahora. El primer cambio del código es para cargar la textura. Como se puede ver, el código ha cambiado tanto que no voy a comentar las partes antiguas para que destaque lo nuevo.

    gl.bindTexture(gl.TEXTURE_2D, textures[1]);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textures[1].image);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

    gl.bindTexture(gl.TEXTURE_2D, textures[2]);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textures[2].image);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST);
    gl.generateMipmap(gl.TEXTURE_2D);

    gl.bindTexture(gl.TEXTURE_2D, null);
  }

  var crateTextures = Array();

  function initTexture() {
    var crateImage = new Image();

    for (var i=0; i < 3; i++) {
      var texture = gl.createTexture();
      texture.image = crateImage;
      crateTextures.push(texture);
    }

    crateImage.onload = function() {
      handleLoadedTexture(crateTextures)
    }
    crateImage.src = "crate.gif";
  }

Mira primero la variable crateTextures en la función initTexture. Debe quedar claro que aunque el código parezca diferente, la única diferencia real es que estamos creando tres objetos de textura webGL en una lista, en vez de uno sólo, y estamos pasando dicha lista a handleLoadedTexture en aquella función que se ejecuta cuando se ha terminado de cargar la imagen (en el navegador). Y por supuesto, hemos cambiado la textura, ahora usamos una con apariencia de caja de madera (crate en inglés)

Filtro Nearest

La primera textura tienen los escalados gl.TEXTURE_MAG_FILTER (para ampliar) y gl.TEXTURE_MIN_FILTER (para reducir) establecidas a gl.NEAREST, como en la lección anterior. Significa que cuando la textura sea escalada webGL usará un filtro que determinará el color de cada punto sólo buscando el  pixel más cercano a dicha posición en la imagen original. Esto significa que se verá perfecto si la textura está a medida del objeto donde se dibujará, incluso aceptablemente bien si la textura se reducirá (presentará ciertos problemas de aliasing, que veremos más abajo), pero se pixelará (aparecerán cuadraditos) en el caso de que haya que ampliarla.Para ver el efecto de pixelación, abra el ejemplo en vivo, y acerca la caja (tecla AvPag, avanzar página) hasta casi chocar con la cámara. Verá esos cuadraditos.

Un detalle, la textura se ampliará o reducirá no sólo dependiendo del tamaño del objeto donde se dibujará, si no también de la posicióncon respecto a la cámara si utilizamos una proyección ortográfica. Por ejemplo, si nos acercamos a cualquier objeto (o es el objeto el que se acerca a la cámara), la textura se ampliará, y se utilizará el filtro que hayamos especificado.

Filtro Linear

Para la segunda textura, estableceremos los escalados a gl.LINEAR, también conocido como filtro lineal. El algoritmo del filtro Linear funciona mejor en los ampliados porque utilizará una interpolación lineal entre los píxeles de la imagen de la textura original que más cerca se encuentren (es decir, una suave degradación entre dos colores) Por ejemplo, un píxel que se encuentre entre uno blanco y otro negro, se dibujará gris. Ésto hace que la imagen ampliada sea más agradable a la vista, aunque en imagenes muy ampliadas puede notarse un cierto efecto borroso. Puedes ver ésto abriendo el ejemplo en vivo y acercando la cámara, y después pulsar la tecla F para cambiar al filtrado lineal. Pero para ser justos, no es posible hacer ampliaciones perfectas, ningún algoritmo puede inventar detalles que no están en la imagen original.

Mipmaps

Para la tercera textura, utilizaremos gl.LINEAR_MIPMAP_NEAREST, pero sólo para reducciones. Ésta es la más compleja de las tres opciones.

El filtrado lineal da resultados razonables cuando se amplía la textura, pero no mejora al filtro Nearest cuando se reduce, de hecho, este filtro puede empeorar el desagradable efecto aliasing. Abre el ejemplo en vivo, y aleja la caja (tecla RePag, retroceder página) hasta que empiece a ver una especie de líneas verticales que parpadean y afean mucho la textura. Eso es el aliasing que se produce con el filtro Nearest. Ahora pulse la F una vez, para cambiar al filtrado lineal, y repita la operación. Se producirá el mismo efecto, incluso ligeramente más desagradable. Vuelva a pulsar la F. Como verá, con el filtro MipMap, el aliasing desaparece casi completamente. En resumen, para las ampliaciones, el filtro mipmap trabaja igual que el Linear, pero para las reducciones, el filtro mipmap e utiliza su propio algoritmo y obtiene el mejor resultado.

¿Cómo funcionan las reducciones de Nearest y Linear? Pues imaginemos que la textura es reducida a una décima parte de la original. El filtro debe utilizar uno de cada diez píxeles de la imagen original para dibujar la reducida, que escogerá al azar. La textura que usamos simula madera, que simplificándola, es una serie de píxeles marrones claros con líneas verticales de píxeles marrones oscuros-negros a intervalos regulares que forman patrón que se repite. Imaginemos qe cada patrón (lo que vendría a ser un listón de madera de los 8 que componen la caja) mide 10 píxeles, de los cuales uno es el color negro, y los otros 9 de marrón claro. Cuando reduzcamos la textura quiere decir que cada pixel negro tiene una probabilidad de 1/10 de pintarse, mientras que los marrones tienen 9/10. Probablemente, habrá lineas verticales negras que no se pinten. Si alejamos o acercamos la caja, unas veces se pintarán, y otras no. Éso produce un horrible efecto de parpadeo, sobre todo si la textura tiene patrones que se repiten mucho, como es el caso de nuestra caja.

La técnica perfecta de reducción para hallar el color de un píxel  sería realizar un cálculo del color promedio sumando todos los colores de los píxeles que van a ser sustituídos, pero sería un algoritmo demasiado costoso computacionalmente para gráficos en tiempo real, y aquí es donde entra en juego el filtrado mipmap.

El filtrado mipmap soluciona el problema generando para cada textura una serie de imágenes auxiliares (llamadas niveles mip) a la mitad, a un cuarto, a un octavo, y así sucesivamente, de su tamaño original hasta llegar a una imagen de 1×1. El conjunto de todos esos niveles se llama mipmap. Cada nivel mip es una versión suavemente reducida del anterior nivel por el filtrado lineal. Cuando se debe realizar una reducción, el algoritmo elige a los dos niveles mip más cercanos, por encima y por debajo, y hace una interpolación lineal de ambos seleccionando el pixel intermedio más cercano al tamaño final de la imagen. Así evitamos en gran medida el aliasing.

Ahora que hemos explicado cómo funciona, entenderás mejor la línea añadida para el filtro mipmap:

    gl.generateMipmap(gl.TEXTURE_2D);

…es la línea requerida para que webGL genere el mipmap.

Ok, esto fue significativamente mucha más información sobre los mipmaps de lo que había planeado para la lección 🙂 pero ahora espero que se entienda mejor.

Volviendo al código restante, de momento hemos visto algunas variables globales nuevas y el cómo se cargan las texturas y configuran sus filtros. Ahora vamos a ver cómo utilizar estas cosas a la hora de dibujar la escena.

drawScene tiene tres cambios. El primero es sobre cómo posicionar nuestro cubo. En lugar de tener un punto fijo, utilizaremos nuestra variable global z:

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

El siguiente es borrar la rotación en el eje z de nuestra antigua demo, dejando sólo la rotación sobre los ejes X e Y:

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

Finalmente, cuando se dibuja el cubo, especificamos cuál de nuestras tres texturas vamos a utilizar:

    gl.bindTexture(gl.TEXTURE_2D, crateTextures[filter]);

Eso es todo. Ahora continuemos con algunos cambios menores en la función animate. En vez de asignar una rotación constante a xRot y a yRot, usaremos nuestras nuevas variables xSpeed e ySpeed:

      xRot += (xSpeed * elapsed) / 1000.0;
      yRot += (ySpeed * elapsed) / 1000.0;

Éstos son todos los cambios que hay que hacer al código antiguo en cuanto al uso de las texturas. Ahora mostraré el código nuevo para meter la detección de eventos de teclado.

El primer cambio relevante es detectar las pulsaciones (cuando se presiona una tecla y cuando se suelta). Lo hacemos en webGLStart:

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

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


    document.onkeydown = handleKeyDown;
    document.onkeyup = handleKeyUp;

    /*tick();
  }*/

Aquí le decimos a javascript que cuando se presione una tecla, llame a la función handleKeyDown, y cuando se suelte, a handleKeyUp.

A continuación:

  var currentlyPressedKeys = {};

  function handleKeyDown(event) {
    currentlyPressedKeys[event.keyCode] = true;

    if (String.fromCharCode(event.keyCode) == "F") {
      filter += 1;
      if (filter == 3) {
        filter = 0;
      }
    }
  }

  function handleKeyUp(event) {
    currentlyPressedKeys[event.keyCode] = false;
  }

Todo lo que hacemos es mantener un diccionario (o array asociativo) llamado currentlyPressedKeys de las teclas pulsadas en un momento dado, poniéndolas a TRUE. Si la tecla pulsada es la F, además actualiza la variable del tipo de filtrado a utilizar de entre las 3 disponibles, secuencialmente. Cuando se suelta, pone la tecla a FALSE.

Vale la pena tomarse un tiempo en explicar por qué manejamos los eventos de teclado de dos formas diferentes. En un juego 3D de ordenador o consola, las dos formas más comunes de manejar las pulsaciones son:

  1. Las que ejecutan una acción inmediata. Por ejemplo, disparar una pistola. Incluso si se pulsan repetidamente, podría fijarse algun tipo de tasa de acciones (en este caso disparos) por segundo.
  2. Las que tienen un efecto dependiendo del tiempo que se tiene pulsada la tecla. Por ejemplo, cuando se presiona la tecla de caminar hacia adelante, se espera seguir avanzando mientras se mantiene pulsada.

Es importante destacar que, con el segundo tipo, se tiene que seguir manteniendo la posibilidad de pulsar otras teclas, mientras la acción continúa ejecutándose, por ejemplo, correr hacia adelante, saltar un obstáculo, y seguir corriendo. Ésto produce situaciones totalmente diferentes a la lectura normal de teclado como hacen los editores de texto, que cuando mantiene una A pulsada, se concatenan un montón de aes, pero cuando se pulsa y se suelta la b (sin soltar la a), la cadena de aes se interrumpe, y se escribe una b. El equivalente es que una vez que saltas el obstáculo, te quedarías quieto, lo cuál sería extremadamente irritante para el jugador.

Por lo tanto, en el código hacemos que la F siga el primer tipo de manejar las pulsaciones, y con el diccionario de teclas pulsadas estamos cubriendo el segundo tipo. Nótese que en el diccionario se guardarán las pulsaciones de todas las teclas, aunque después sólo utilicemos unas pocas.

El diccionario será usado en una sola función, handleKeys, que será llamada en la función tick:

  /*function tick() {
    requestAnimFrame(tick);*/

    handleKeys();
    /*drawScene();
    animate();
  }*/

Y la función hace lo siguiente:

  function handleKeys() {
    if (currentlyPressedKeys[33]) {
      // Page Up
      z -= 0.05;
    }
    if (currentlyPressedKeys[34]) {
      // Page Down
      z += 0.05;
    }
    if (currentlyPressedKeys[37]) {
      // Left cursor key
      ySpeed -= 1;
    }
    if (currentlyPressedKeys[39]) {
      // Right cursor key
      ySpeed += 1;
    }
    if (currentlyPressedKeys[38]) {
      // Up cursor key
      xSpeed -= 1;
    }
    if (currentlyPressedKeys[40]) {
      // Down cursor key
      xSpeed += 1;
    }
  }

Lo único que hace esta larga función es comprobar cuáles teclas han sido pulsadas, y realiza las operaciones asociadas a ellas, como son el gestionar las velocidades de rotación vertical y horizontal del cubo, y su posición en el eje Z.

Y ésto es todo por ahora. Si has seguido todo el tutorial, deberías ser capaz de entender la importancia de utilizar un tipo de filtro u otro según las necesidades de escalado que tenga tu escena, y cómo leer las entradas de teclado de tus usuarios para hacer todo tipo de animaciones o tares, el límite es tu imaginación.

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.

La guía de programación de OpenGL ES me fue un recurso de incalculable valor para obtener información sobre las texturas y el mipmapping. Un post de Matthew Casperson en Bright Hub fue una buena ayuda sobre consejos de uso de los eventos de teclado, y este artículo de Javascript de Kata me ilustró sobre la utilización de diccionarios. Y como siempre, 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)