WebGL Tutorial 2 – Añadiendo color

Escrito por el 25 octubre, 2011

¡Bienvenidos al segundo tutorial de webGL! Esta vez aprenderemos a dar color a la escena. Es una traducción no literal de su respectivo tutorial en Learning webGL, que a su vez está basada en el capítulo 3 del tutorial openGL de NeHe.

Éste es el aspecto que tendrá el ejemplo cuando lo ejecutes en un navegador que soporte webGL:


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 tutorial, deberías hacerlo, pues aquí pasaremos a comentar los cambios que tenemos que hacer a aquél ejemplo, para añadir colores.

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.

La mayor parte del códio es bastante parecida al de la primera lección. Haciendo un pequeño resumen:

  • Definimos el vertex y fragment shader, usando etiquetas <script> de HTML con los tipos “x-shader/x-vertex” y “x-shader/x-fragment“.
  • Inicializamos el contexto webGL en initGL.
  • Cargamos los shaders en el objeto programa webgl usando getShader e initShaders.
  • Definimos la matriz modelo-vista mvMatrix y la matriz de proyección pMatrix, junto con la función setMatrixUniforms, para moverlos de javascript a webGL de manera que los shaders puedan verse.
  • Cargamos los buffers con información sobre los vértices de los objetos de la escena usando initBuffers.
  • Dibujamos la escena, a través de drawScene.
  • Definimos la función webGLStart para que lo configure todo en primer lugar.
  • Finalmente, proporcionamos el contenido HTML mínimo requerido para mostar los gráficos.

Las únicas cosas que hemos cambiado en este código con respecto a la primera lección son los shaders, el initBuffers y la función drawScene. Con el fin de explicar dichos cambios, necesitamos hablar un poco sobre el canal de renderizado en webGL. Aquí un diagrama:

El diagrama muestra, de forma muy simplificada, cómo los datos pasan de la función drawScene de javascript a convertirse en píxeles dibujados en la pantalla dentro de un canvas de webGL. Sólo muestro los pasos que vamos a explicar en esta lección. En futuras lecciones veremos una versión más detallada del proceso.

A más alto nivel, el proceso funciona así: cada vez que se llama a una función de gl como drawArrays, webGL procesa los datos que previamente le han dado en forma de atributos (como los buffers que utilizamos en la lección 1) y las variables uniformes (utilizadas en las matrices de modelo-vista y en la proyección) y se los manda al código vertex shader.

Ésto se hace llamando al vertex shader una vez por cada vértice, cada vez con los atributos configurados correctamente para el vértice; las variables uniformes también se pasan, pero al contrario que con los vértices, las variables uniformes no cambian de llamada a llamada. El vertex shader hace algún tipo de cálculo con los datos (en la lección 1, se aplicó la proyección y el modelo-vista para que todos los vértices se vieran en perspectiva) y pone los resultados en unas variables  definidas por webGL (varying variables). Una de ellas, utilizada en la lección 1, es gl_Position, que contiene las coordenadas del vértice una vez que el shader ha terminado de juguetear con él.

Una vez que finaliza el proceso vertex shader, webGL hace la magia necesaria para convertir un espacio 3D en una imagen 2D a partir de las variables “varying”, llamando al proceso fragment shader una vez por cada pixel a dibujar, excepto con aquellos píxeles que sean vértices. (Otros programadores conocen al fragment shader como pixel shader precisamente por esta razón). Para los píxeles con vértices, se llenan de puntos las posiciones entre los vértices en un proceso llamado interpolación lineal. Este proceso rellena el espacio delimitado por los vértices con puntos para formar un triángulo visible. El propósito de esto es devolver el color de cada uno de esos puntos interpolados a través de una variable que también usamos, aunque muy básicamente, en la lección 1: gl_FragColor.

Una vez se termina el proceso fragment shader, webGl hace algunos retoques, los cuales veremos en otras lecciones, y termina metiendo esas variables en el buffer del frame, que es lo que se mostrará en pantalla en ese instante.

Espero que por ahora más o menos se conozca el camino que recorren los vértices y sus colores desde el código javascript hasta el fragment shader de la tarjeta gráfica, cuando no contamos con un camino directo que lleve lo uno hasta lo otro.

La forma de simular ésto es aprovecharnos de que podemos utilizar variables varying del proceso vertex shader, no sólo la posición, y luego poder recuperarlas en el fragment shader. Así, podemos pasar un color al vertex shader, donde la podemos meter en una variable varying que el fragment shader podrá utilizar.

Convenientemente, esta forma de funcionar nos proporciona una forma inmediatamente de crear gradientes de colores (convertir gradualmente un color en otro en una misma cara del objeto). Todas las variables varying establecidas por el vertex shader se interpolan linealmente cuando generamos los fragmentos entre los vértices, no sólo las posiciones. La unterpolación lineal de los colores nos da degradados suaves, como podemos ver en el triángulo del ejemplo.

Empecemos a mirar el código; sólo nos fijaremos en los cambios con respecto a la lección 1. En primer lugar, el código del proceso vertex shader ha cambiado mucho:

attribute vec3 aVertexPosition;
attribute vec4 aVertexColor;

uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;

varying vec4 vColor;

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

Ahora tenemos dos atributos, que varían de un vértice a otro vértice, llamadas aVertexPosition y aVertexColor, dos variables uniformes (pero no del tipo varying) llamadas uMVMatrix y uPMatrix, y una salida en forma de variable varying llamada vColor.

En el cuerpo del shader estamos calculando gl_Position (que es implícitamente definida como variable varying para cada vertex shader) exactamente de la misma forma que hacíamos en la lección 1, y todo lo que hacemos con el color es pasarlo directamente a través del atributo de entrada aVertexColor a la variable varying de salida vColor.

Una vez que ésto ha sido ejecutado por cada vértice, la interpolación está preparada para generar los fragmentos, y éstos se transmiten al fragment shader:

#ifdef GL_ES
precision highp float;
#endif

varying vec4 vColor;

void main(void) {
    gl_FragColor = vColor;
}

Aquí, después de la elección del tipo de precisión a utilizar, tomamos la variable varying vColor, que contiene el color mezclado que ha salido de la interpolación lineal, y lo único que hace es devolverlo como el color de ese fragmento, es decir, para ese pixel.

Eso es todo lo que hay que cambiar en los shaders con respecto a la primera lección. Hay otro dos cambios más. El primero es muy pequeño, y se hace en initShaders, donde ahora hay que hacer referencia a dos atributos, en lugar de sólo uno. El código que no se modifica lo dejaré comentado, asi que el editor de texto que utilizo resaltará el código nuevo:

/*var shaderProgram;
function initShaders() {
    var fragmentShader = getShader(gl, "shader-fs");
    var vertexShader = getShader(gl, "shader-vs");

    shaderProgram = gl.createProgram();
    gl.attachShader(shaderProgram, vertexShader);
    gl.attachShader(shaderProgram, fragmentShader);
    gl.linkProgram(shaderProgram);

    if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
        alert("Could not initialise shaders");
    }

    gl.useProgram(shaderProgram);

    shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
    gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);*/


    shaderProgram.vertexColorAttribute = gl.getAttribLocation(shaderProgram, "aVertexColor");
    gl.enableVertexAttribArray(shaderProgram.vertexColorAttribute);

    /*shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
    shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");
}*/

En la primera lección era un poco confuso el cómo se obtenían la ubicación de los atributos, pero ahora seguramente se vea más claro. Es la forma de obtener una referencia de los atributos que queremos transmitir a los vertex shaders para cada vértice. En la lección 1 sólo teníamos el atributo de posición de los vértices, ahora también tenemos el de su color.

El resto de los cambios están en initBuffers, donde ahora hay que definir los colores que vamos a utilizar, y en drawScene, donde tenemos que pasárselos a webGL.

Empecemos con initBuffers primero, donde definimos nuevas variables globales donde mantener los buffers de color para el triángulo y el cuadrado:

/*var triangleVertexPositionBuffer;*/
var triangleVertexColorBuffer;
/*var squareVertexPositionBuffer;*/
var squareVertexColorBuffer;

Luego, justo después de crear el buffer de posiciones de los vértices del triángulo, especificamos sus colores:

/*function initBuffers() {
    triangleVertexPositionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
    var vertices = [
        0.0, 1.0, 0.0,
       -1.0, -1.0, 0.0,
        1.0, -1.0, 0.0
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    triangleVertexPositionBuffer.itemSize = 3;
    triangleVertexPositionBuffer.numItems = 3;*/


    triangleVertexColorBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexColorBuffer);
    var colors = [
        1.0, 0.0, 0.0, 1.0,
        0.0, 1.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
    triangleVertexColorBuffer.itemSize = 4;
    triangleVertexColorBuffer.numItems = 3;

Los valores que hemos proporcionado para los colores están en una lista, exactamente igual que con las posiciones. Sin embargo, hay una interesante diferencia entre los dos buffers. Mientras que el de las posiciones de los vértices se especifican tres números para cada uno, las coordenadas X, Y y Z, en el buffer de los colores utilizamos 4 números. Ésto es porque se usa el formato de color RGBA, donde el primer número es la cantidad de rojo, el segundo de verde, el tercero de azul, y el cuarto el canal alfa, también conocido como transparencia. 0 es totalmente invisible y 1 completamente opaco, algo que nos será muy útil en futuras lecciones. Por eso el itemSize debe ser cambiado a 4.

A continuación, hacemos lo equivalente con el cuadrado; pero esta vez, usaremos el mismo color para todos los vértices, generando el valor para el buffer usando un bucle for:

    /*squareVertexPositionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
    vertices = [
        1.0, 1.0, 0.0,
       -1.0, 1.0, 0.0,
        1.0, -1.0, 0.0,
       -1.0, -1.0, 0.0
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    squareVertexPositionBuffer.itemSize = 3;
    squareVertexPositionBuffer.numItems = 4;*/


    squareVertexColorBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer);
    colors = []
    for (var i=0; i < 4; i++) {
        colors = colors.concat([0.5, 0.5, 1.0, 1.0]);
    }
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
    squareVertexColorBuffer.itemSize = 4;
    squareVertexColorBuffer.numItems = 4;

Ahora tenemos los datos de los objewtos metidos en cuatro buffers, asi que lo siguiente es hacer que drawScene utilice la nueva información de los colores:

/*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, [-1.5, 0.0, -7.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);

    mat4.translate(mvMatrix, [3.0, 0.0, 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);
}*/

Y el siguiente cambio es… Nada. ¡Por fín hemos terminado! Esto era todo lo que necesitábamos para introducir un poco de color en nuestro mundo 3D, y espero que ahora estén un poco más familiarizados con los conceptos de shaders y el cómo pasar y recuperar información entre ellos. Seguro que esta segunda lección ha sido más fácil de seguir que la primera, y si todo marcha bien, cada vez será más fácil seguir las lecciones. O al menos, ése es el objetivo.

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 diagrama del trabajo que realiza exactamente el canal de renderizado de webGL se hizo mucho más fácil de entender gracias a la guía de programación de openGL ES 2.0, cuya recomendación encontré en el blog sobre webGL de Jim Pick. Por supuesto, estoy profundamente agradecido con los tutoriales de NeHe para openGL, en los que se basan los códigos para estas lecciones.

 

Etiquetas: , ,

¿Tienes algo que decir?

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