WebGL Tutorial 5 – Una introducción a las texturas

Escrito por el 28 octubre, 2011

Bienvenido al quinto 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 6 del tutorial sobre openGL de NeHe. Esta vez vamos a añadir texturas a los objetos 3D, más concretamente, una imagen que está contenida en un fichero independiente. Las texturas se utilizan para añadir mucho detalle a los objetos sin necesidad de dibujar figuras geométricas increíblemente complejas. Por ejemplo, imagina que quieres dibujar una pared de ladrillos, com el cemento y los ladrillos a la vista. Puede modelar cada ladrillo  de color naranja y el cemento que lo rellena de color grisáceo, definiendo sus posiciones uno a uno, en un trabajo increíblemente pesado o simplemente hacer una pared con un solo cubo (aplastado) y ponerle una imagen de una pared de ladrillos que cubra toda las caras visibles de nuestro objeto.

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 cuatro 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 meterle texturas.

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

El truco para entender cómo trabajan las texturas es pensar que son un camino especial de definir el color de los puntos del objeto 3D. Como deberías recordar de la lección 2, los colores están especificados por el fragment shader, asi que lo que tenemos que hacer es cargar la imagen y enviársela de alguna forma. El fragment shader también tiene que saber qué trozo de la imagen debe utilizar para pintar el fragmento del objeto con el que está trabajando en ese momento, asi que también tenemos que suministrarle ese tipo de información.

Vamos a empezar a mirar el código con el que se carga la textura. A la función de carga, la llamaremos en el webGLStart (sin comentar, los trozos nuevos o modificados de código para que se resalten sobre el resto):

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

    initTexture();

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

Y lo siguiente, la nueva función initTexture que acabamos de llamar:

var neheTexture;
function initTexture() {
    neheTexture = gl.createTexture();
    neheTexture.image = new Image();
    neheTexture.image.onload = function() {
        handleLoadedTexture(neheTexture)
    }
    neheTexture.image.src = "nehe.gif";
}

Como podemos ver, estamos creando una nueva variable global donde almacenar la textura. En un ejemplo del mundo real,donde habría múltiples texturas, nunca se utilizarían variables globales, pero ahora lo hacemos así para no complicarnos la vida. Utilizamos gl.createTexture para crear una textura de referencia para poner en la variable global, luego creamos un objeto Image javascript y lo metemos en un atributo (que como siempre, nos acabamos de inventar aprovechándonos de que javascript permite hacer este tipo de cosas), para tener los datos relacionados con la imagen bien cerca de la textura. A continuación definimos la función que debe dispararse cuando la imagen se cargue en la memoria de javascript, que simplemente llamará a otra llamada hlandleLoadedTexture, pasándole como parámetro el objeto textura. Finalmente establecemos la ruta donde está la imagen, en el src. En cuanto se ejecute esta asignación, un subproceso asíncrono de javascript empezará a cargar la imagen, y cuando esté cargada completamente se ejecutará el onload que pertenece a esa imagen, si existe. En nuestro caso, el onload es justo lo que definimos antes de cargar la imagen.

function handleLoadedTexture(texture) {
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, texture.image);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.bindTexture(gl.TEXTURE_2D, null);
  }

Y en dicha función, lo primero que hay que hacer es decirle a webGL que nuestra textura es la textura actual (exactamente el mismo modo de funcionar que con los buffers). Todas las funciones de tratamiento de texturas de webGL usarán la textura “actual”, y bindTexture es la forma de establecerla.

A continuación, le diremos a webGL que todas las imágenes que cargamos en las texturas deben ser volteadas verticalmente (flip y). Hacemos ésto porque el sistema de coordenadas de la escena no corresponde con el de la imagen. Las coordenadas de la escena web, por ejemplo la X, aumenta cuando nos desplazamos a la derecha, y disminuye cuando nos desplazamos a la izquierda. La coordenada Y aumenta al ir hacia arriba, y disminuye al ir hacia abajo. En las imágenes, la coordenada X funciona igual, pero la Y es justo al revés. Hacia abajo aumenta, y hacia arriba disminuye. Por eso volteamos verticalmente la imagen.

El siguiente paso es subir la imagen recién cargada al trozo de memoria para las texturas de la tarjeta gráfica, usando la función textImage2D. Los parámetros son, en orden, qué clase de imagen vamos a usar, el nivel de detalle (que veremos en lecciones posteriores), el formato con el que queremos subir la imagen a la tarjeta gráfica (dos veces, por motivos que después veremos), el tamaño de cada “canal” de la imagen (una imagen se descompone en tres canales, rojo, verde y azul), y finalmente, la variable que contiene la imagen.

En las siguientes dos líneas especificamos un escalado especial para la textura. El primero le dice a webGL qué hacer cuando con la textura estamos rellenando un área superior al tamaño de la imagen, es decir, cómo debe ampliarla. El segundo sirve para indicar qué hacer cuando el área es más pequeña, cómo reducirla. Hay varios tipos de escalado que puedes usar; NEAREST es el menos atractivo de todos, ya que aplica ampliaciones y reducciones de la textura con algoritmos muy básicos. Sin embargo tiene la ventaja de ser el más rápido de todos. En una próxima lección explicaremos más detalles de los otros tipos de escalado para que cada uno utilice el que más le convenga teniendo en cuenta la apariencia final y el rendimiento que tiene cada uno.

Una vez terminado ésto, ponemos a nulo la textura “actual”. No es estrictamente necesario, pero es una buena práctica. Es como limpiar la mesa después de haberla utilizado para comer.

Y ésto es todo lo necesario para cargar las texturas en la tarjeta. Ahora veámos qué hay que tocar en initBuffers. Por supuesto, vamos a eliminar todo el código asociado a la pirámide de la anterio lección. En cuanto al cubo, el cambio más interesante es la sustitución de su buffer de color por otro buffer nuevo, el que contiene las coordenadas de la textura:

    cubeVertexTextureCoordBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexTextureCoordBuffer);
    var textureCoords = [
      // Front face
      0.0, 0.0,
      1.0, 0.0,
      1.0, 1.0,
      0.0, 1.0,

      // Back face
      1.0, 0.0,
      1.0, 1.0,
      0.0, 1.0,
      0.0, 0.0,

      // Top face
      0.0, 1.0,
      0.0, 0.0,
      1.0, 0.0,
      1.0, 1.0,

      // Bottom face
      1.0, 1.0,
      0.0, 1.0,
      0.0, 0.0,
      1.0, 0.0,

      // Right face
      1.0, 0.0,
      1.0, 1.0,
      0.0, 1.0,
      0.0, 0.0,

      // Left face
      0.0, 0.0,
      1.0, 0.0,
      1.0, 1.0,
      0.0, 1.0,
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoords), gl.STATIC_DRAW);
    cubeVertexTextureCoordBuffer.itemSize = 2;
    cubeVertexTextureCoordBuffer.numItems = 24;

Espero que ésta vez no se asuste tanto con el código, de hecho espero que ya estés completamente familiarizado a cómo llenar y configurar buffers.  Lo que estamos haciendo ahora es definir un nuevo atributo para cada vértice de cada cara del cubo, compuesto por dos valores cada uno. Lo que éstos valores indican son las coordenadas para decir en qué dirección pintar la textura, tomando la textura como si midiera 1 de alto y 1 de ancho. El [0,0] significa la esquina inferior izquierda de la imagen, el [1,1] es la esquina superior derecha, el [0,1] la esquina superior izquierda, y el [1,0] la esquina inferior derecha. Si utilizamos todo el rango, es decir, del 0 al 1, ajustará la textura al tamaño del objeto donde se ubicará, deformándola en algunos casos, en los cuales se aplicará el filtro de textura gl.NEAREST especificado anteriormente. La conversión de esta resolución a la real, lo hace webGL.

Eso fue el único (gran) cambio de initBuffers, por lo que ahora pasamos a drawScene. La diferencia más interesante es, por supuesto, la forma de usar la textura. Sin embargo, ántes de echarle mano, hay que hacer una serie de cambios simples, como la eliminación de cualquier cosa relacionada con la pirámide, y la forma en la que vamos a mover el cubo por la pantalla. No voy a explicar en detalle este cambio ni a mostrar la función animate, pues viendo el código es fácil de entender. A pesar de que la animación es compleja, lo único que hacemos es rotar el objeto en todos sus ejes (X, Y y Z) a la misma velocidad.

var xRot = 0;
var yRot = 0;
var zRot = 0;
/*function drawScene() {
    gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

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

    mat4.identity(mvMatrix);*/


    mat4.translate(mvMatrix, [0.0, 0.0, -5.0]);

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

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

Como se ve, hacemos uso de un par de variables nuevas, para manejar la rotación del cubo desde tres ejes, xRot, yRot y zRot.

Lo siguiente, simplemente vemos que usamos el buffer de la textura como buffer actual.

    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexTextureCoordBuffer);
    gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, cubeVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);

…y ahora webGL conoce qué trozo de textura debe usar para dibujar cada vértice, cuando pinte el el cubo:

    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, neheTexture);
    gl.uniform1i(shaderProgram.samplerUniform, 0);

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

Explicar exactamente lo que está pasando es algo complejo. WebGL puede tratar hasta 32 texturas en una sola llamada drawElements, numeradas desde gl.TEXTURE0 a gl.TEXTURE31. Lo que estamos haciendo en las dos primeras líneas es decir que la textura cero es la que nosotros cargamos antes, y en la tercera línea le estamos pasando el valor cero al shader con una variable uniforme (que, como con las matrices uniformes, sacamos del programa shader en el initShaders), así le decimos al shader que estamos usando la textura cero. Veremos más sobre ésto después.

De todas formas, una vez que esas tres líneas se ejecutan, estamos listos para empezar a dibujar, por lo que basta con utilizar el mismo código que pintaba el cubo a través de triángulos.

Y sólo falta cambiar un poco el vertex shader y el fragment shader, empecemos con el vertex:

attribute vec3 aVertexPosition;
attribute vec2 aTextureCoord;

uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;

varying vec2 vTextureCoord;

void main(void) {
    gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
    vTextureCoord = aTextureCoord;
}

El cambio es muy parecido a cómo metíamos el color en la lección 2; todo lo que hacemos es aceptar la coordenada de la textura (de la misma forma que antes recuperábamos el color) de cada vértice y pasársela al fragment mediante una variable varying.

Una vez que el vertex shader ha sido llamado una vez por cada vértice, webGL trabajará con los valores para cada fragmento (que recordemos,  son básicamente píxeles) entre los vértices usando interpolaciones lineales, al igual que hacía con los colores en la lección 2. Por lo tanto, un fragmento a mitad de camino entre los vértices con las coordenadas de textura [1, 0] y [0, 0] recibirá la coordenade de textura [0.5, 0] y otra a medio camino entre [0, 0] y [1, 1], tomará la de [0.5, 0.5]. La siguiente parada, el fragment shader:

#ifdef GL_ES
precision highp float;
#endif

varying vec2 vTextureCoord;

uniform sampler2D uSampler;

void main(void) {
    gl_FragColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
}

Aquí tomamos las coordenadas de la textura interpolada en una variable varying, y también tenemos otra variable tipo sampler2D, que es el modo que tiene el shader de leer la textura. En drawScene,  nuestra textura estaba identificada como gl.TEXTURE0, y la variable uniforme uSample fue asignada con el valor cero; por lo tanto este sampler representa nuestra textura. Lo que hace el shader es utilizar la función Texture2D para obtener el color adecuado de la textura usando las coordenadas. Las texturas usan tradicionalmente coordenadas s y t en lugar de X e Y, pero son lo mismo. De hecho también podríamos haber usado vTextureCoord.x y vTextureCoord.y, pero somos unos románticos.

Una vez que tenemos el color del fragmento, lo devolvemos en el gl_FragColor, y  acabamos. Al fin, tenemos un objeto con textura en la pantalla. Y eso es todo por de momento.

Ahora ya sabes cómo agregar texturas a los objetos 3D en webGL, sólo queda practicar y jugar con el código  de ejemplo hasta coger soltura. Intenta poner 6 imágenes diferentes al cubo, por ejemplo, y busca otros ejemplos y mira su código. Así es como de verdad se aprende.

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 caja giratoria de Chris Marrin fue una increíble gran ayuda para escribir este tutorial, que a su vez es una modificación del demo de Jacob Seidelin. 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.

Nota: Me han llegado algunos comentarios quejándose de que el tutorial no funciona en local. El problema es que WebGL no utiliza texturas que estén almacenadas en local, ya que puede suponer un problema de seguridad grave, ya que entonces tu tarjeta gráfica podría ser atacada con técnicas XSS (cross site scripting) o similares. Sólo carga los recursos (ficheros) por el protocolo HTTP o HTTPS. La solución es alojar éstos tutoriales en servidor web cualquiera. Si tienes un hosting contratado, cuelga tus pruebas allí. Si no lo tienes, instala alguno en local, como XAMPP, que usa Apache, y que es muy fácil de usar porque ya viene todo configurado y listo para arrancarlo rápidamente.

Etiquetas: , ,

Comentarios (5)

  • Están muy buenos tus tutoriales, los estoy siguiendo todos los días, sin ningún problema.

    Gracias.

  • disculpa tengo una dudaa,.. no me sale esteee.. el codigoo esta igual al del ejempplo, pero creo que mi error es donde guardo la imagen??.. solo acepta formato .gif?? o tambien .jpg. ??? agradeceria tu ayuda… solo necesito saber donde colocar mi imagen :) saludos buena tarde.

  • Buenas.
    Primero agradecerte estos tutoriales, son increíbles ;)
    Al lío, soy incapaz de hacer funcionar bien las texturas, incluso cogiendo el código ejemplo, los .js y la misma imagen. Como has dicho que no funciona en local, me he creado un hosting en http://260mb.org/ y he subido los archivos y tampoco funciona. (Es posible que haga algo mal del hosting, es la primera vez que hago algo parecido, pero el resto de tutoriales funciona y con las texturas no..).

    Gracias, un saludo

  • Lo he solucionado… pero modificando mucho código a la vez, así que no se que fallaba. Me he fijado en el tutorial de https://sites.google.com/site/desarrolloenwebgl/tutorial-1/6-texturas
    que habrá cogido el codigo del mismo sitio, porque se parece bastante.
    Gracias de todas formas ;)

  • ¡Buenas! Respecto a lo de las texturas estuve como 3 horas intentando mil cosas sin que nada funcionara con una textura GIF que yo mismo había creado.

    Por servidor local de Apache no iba, después también leí otra cosa de cambiar no se que parámetro del tipo imagen de javascript para poder usar imágenes locales y tampoco.

    Hasta que por fin leí que las texturas debía tener tamaños de potencias de 2, cambié el tamaño de la imagen de 500 a 512 y todo fue como la seda, accediendo directamente a la imagen sin servidores ni nada.

¿Tienes algo que decir?

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