WebGL Tutorial 12 – Punto de iluminación

Escrito por el 21 noviembre, 2011

Bienvenido al duodécimo tutorial de la serie Aprende webGL. Es una traducción no literal de su respectivo tutorial en Learning webGL. Ésta lección, como la anterior, no va a estar basada en ningún tutorial de OpenGL de NeHe. En ella, aprenderemos una forma sencilla de usar puntos de luz, es decir, iluminación que proviene de un sólo punto de dentro la escena, que ilumina hacia todas direcciones, con lo que afectará a cada vértice con un ángulo diferente. No confundir con iluminación direccional, que era la iluminación que incidía con la misma dirección en todos los vértices, ya que suponíamos que la luz venía de muy lejos, fuera de la escena.

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 una y aquí otra de las texturas utilizadas, para descargarlas 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. Se ve la luna y una caja gigante orbitando en el espacio (se verán blancos hasta que la textura se cargue completamente). Ambos están iluminados por un punto de luz situado entre ellos. Si quieres puedes cambiar la posición de dicha luz, su color, etc, mediante el formulario que hay debajo del canvas.

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 tutoriales anteriores, deberías hacerlo, pues aquí sólo comentaremos el código que necesitamos para programar las novedades específicas para esta lección. En particular, en esta lección partiremos del código que usamos en el tutorial 11.

Vamos a comenzar describiendo exactamente lo que entendemos como punto de iluminación. La principal diferencia entre este tipo de luz y la iluminación direccional que vimos hace algunas lecciones radica en que la primera proviene de un punto de dentro de la escena que ilumina en todas direcciones. Ésto significa que el ángulo con el que esta luz incide en todos los vértices de la escena son diferentes entre sí. Por lo tanto, la forma obvia de programar ésto es calcular para cada vértice, con qué dirección llega la luz, y luego aplicar exactamente los mismos cálculos que hacíamos con la iluminación direccional. Y eso es lo que haremos. Pero también se arrastra un defecto importante que limita la utilidad de dicho tipo de iluminación: No tenemos en cuenta si hay objetos por delante que bloqueen total o parcialmente la iluminación, como también pasaba en la iluminación direccional, ya que no tenemos en cuenta las sombras.

Quizás podrías estar pensando que, en lugar de calcular la dirección en cada vértice, deberíamos calcular la dirección en cada pixel (fragmento) de la escena, para obtener una luz mucho más real. Bien, este tipo de iluminación se conoce como iluminación por fragmentos, y haría que la tarjeta gráfica deba realizar muchos más cálculos de iluminación. Probablemente veámos este tipo de luz en un tutorial posterior.

Ahora que hemos descrito lo que queremos conseguir, vale la pena volver a jugar el ejemplo en vivo y teniendo en cuenta una cosa más: No hay ningún objeto real que represente la luz (es decir, como una “bombilla”), la luz surge de una coordenada que está vacía. Si quieres tener un objeto que represente el emisor de la luz, por ejemplo, ua esfera amarilla en mitad de la escena que simule ser el sol, habría que definir la fuente de luz y el objeto por separado. Hacer el objeto debería serte muy sencillo, si has ido siguiendo todas las lecciones anteriores, con lo que en este tutorial sólo explicaré cómo programar el punto de iluminación. Y para que percibas la sencillez de hacer ésto, la gran mayoría de los cambios que hemos hecho en el código son para crear el cubo y hacer girar ambos objetos alrededor del centro de la escena. El punto de luz apenas tiene trabajo.

El HTML ha cambiado ligeramente, ahora en el formulario recuperamos la información del punto de luz, en lugar de la de una iluminación direccional. Por su sencillez, no tiene sentido incluir aquí el código resultante, así que vamos a continuar, hasta webGLStart. Y de nuevo, los cambios son tan sencillos que no los voy a mostrar. Sólo eliminamos los eventos del ratón, con lo que no tendremos la posibilidad de girar o mover nada, y también hemos cambiado el nombre de initTexture a initTextures, ya que ahora cargamos dos. No es un cambio muy emocionante.

En la función tick incluímos de nuevo una llamada a la función animate, ya que ahora sí habrá una animación independiente en la escena.

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

    animate();
  /*}*/

Y dicha función, simplemente actualiza dos variables globales que describen el ángulo actual de rotación de sus órbitas, a una tasa de giro de 50º / segundo.

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

      moonAngle += 0.05 * elapsed;
      cubeAngle += 0.05 * elapsed;
    }
    lastTime = timeNow;
  }

La suguiente función implicada es drawScene, que tiene algunos cambios interesantes. Comienza con el mismo código de siempre que limpia el canvas y establece una perspectiva, y luego tiene un código idéntico al del tutorial 11 donde se comprueba si la casilla de iluminación está marcada, en cuyo caso enviará el color de la luz de ambiente definida en el formulario de la página web a la tarjeta gráfica:

  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);

    var lighting = document.getElementById("lighting").checked;
    gl.uniform1i(shaderProgram.useLightingUniform, lighting);
    if (lighting) {
      gl.uniform3f(
        shaderProgram.ambientColorUniform,
        parseFloat(document.getElementById("ambientR").value),
        parseFloat(document.getElementById("ambientG").value),
        parseFloat(document.getElementById("ambientB").value)
      );

A continuación, enviamos la posición de nuestro punto de luz a la tarjeta gráfica en una variable uniforme, tal y como hacíamos con la iluminación direccional. Pero con una notable diferencia: Ahora no es necesario convertirlo en un vector normal (vector con módulo 1), ni invertirlo, pues ahora estamos definiendo una posición, y no una dirección. Basta con enviar sus coordenadas directamente:

      gl.uniform3f(
        shaderProgram.pointLightingLocationUniform,
        parseFloat(document.getElementById("lightPositionX").value),
        parseFloat(document.getElementById("lightPositionY").value),
        parseFloat(document.getElementById("lightPositionZ").value)
      );

Y también enviamos el color que tiene el punto de luz, tal y como hicímos con el color de la iluminación ambiental.

      gl.uniform3f(
        shaderProgram.pointLightingColorUniform,
        parseFloat(document.getElementById("pointR").value),
        parseFloat(document.getElementById("pointG").value),
        parseFloat(document.getElementById("pointB").value)
      );
    }

Después, dibujamos la esfera y el cubo en sus posiciones adecuadas:

    mat4.identity(mvMatrix);

    mat4.translate(mvMatrix, [0, 0, -20]);

    mvPushMatrix();
    mat4.rotate(mvMatrix, degToRad(moonAngle), [0, 1, 0]);
    mat4.translate(mvMatrix, [5, 0, 0]);
    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, moonTexture);
    gl.uniform1i(shaderProgram.samplerUniform, 0);

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

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

    gl.bindBuffer(gl.ARRAY_BUFFER, moonVertexNormalBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexNormalAttribute, moonVertexNormalBuffer.itemSize, gl.FLOAT, false, 0, 0);

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

    mvPushMatrix();
    mat4.rotate(mvMatrix, degToRad(cubeAngle), [0, 1, 0]);
    mat4.translate(mvMatrix, [5, 0, 0]);
    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, cubeVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexNormalBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexNormalAttribute, cubeVertexNormalBuffer.itemSize, gl.FLOAT, false, 0, 0);

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

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

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

Es el código para dibujar figuras de siempre, y con él terminamos drawScene. También hay cambios en initBuffers, donde ahora se incluyen los búferes necesarios para crear la caja, y en initTextures, donde también cargamos la textura de la caja. Ambos fragmentos son idénticos a los del tutorial 7, asi que los omitiremos.

El siguiente cambio es el más importante de cara al punto de iluminación, y tiene lugar en el vertex shade. Por trozos:

  attribute vec3 aVertexPosition;
  attribute vec3 aVertexNormal;
  attribute vec2 aTextureCoord;

  uniform mat4 uMVMatrix;
  uniform mat4 uPMatrix;
  uniform mat3 uNMatrix;

  uniform vec3 uAmbientColor;

  uniform vec3 uPointLightingLocation;
  uniform vec3 uPointLightingColor;

Tenemos dos nuevas variables uniformes que contienen la posición del punto de luz, y de su color.

  uniform bool uUseLighting;

  varying vec2 vTextureCoord;
  varying vec3 vLightWeighting;

  void main(void) {
    vec4 mvPosition = uMVMatrix * vec4(aVertexPosition, 1.0);
    gl_Position = uPMatrix * mvPosition;

Como se puede ver, hemos partido la forma de obtener gl_position en dos pasos, ya que necesitamos el valor intermedio mvPosition por separado, para no realizar dos veces la misma multiplicación. Antes teníamos ésto:

    gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);

Y el nuevo valor, mvPosition, se utiliza para ésto:

    vTextureCoord = aTextureCoord;

    if (!uUseLighting) {
      vLightWeighting = vec3(1.0, 1.0, 1.0);
    } else {
      vec3 lightDirection = normalize(uPointLightingLocation - mvPosition.xyz);

La posición de la luz está definida según las coordenadas finales del mundo, y la posición del vértice, una vez que se ha multiplicado sólo por la matriz modelo-vista, también. Necesitamos que ambas estén situadas en el mismo eje de coordenadas X, Y y Z para hallar la dirección con el que incide la luz para ese vértice, por eso necesitamos a mvPosition sin multiplicarlo por la matriz de proyección (lo que cambiaría el eje de coordenadas finales a otro equivalente, pero distinto, acorde a la perspectiva definida). Y para obtener la dirección de la luz con la que llega a ese vértice, lighDirection, sólo tenemos que restar ambas posiciones y normalizar el resultado, como hacíamos antes para la dirección de la luz direccional, pero ésta vez dentro de la tarjeta gráfica.

Una vez hecho ésto, sólo nos queda hacer un cálculo idéntico al de la iluminación direccional, utilizando la dirección recién obtenida:

      vec3 transformedNormal = uNMatrix * aVertexNormal;
      float directionalLightWeighting = max(dot(transformedNormal, lightDirection), 0.0);
      vLightWeighting = uAmbientColor + uPointLightingColor * directionalLightWeighting;

Y con ésto, ya conoces todo lo necesario para incluir puntos de luz en tus escenas, con el defecto de que dicha iluminación es capaz de “atravesar” los objetos que tiene delante para iluminar de la misma forma a los que están detrás, como ya dijimos antes.

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 antes, la textura para la luna viene de la página JPL de la NASA. El código para generar la esfera está basado en la versión anterior (ha sido sustituído por el uso de una librería de webgl lamada J3DI) de esta demo de la página de Krhonos, del equipo desarrollador de webGL. ¡Muchas gracias a ambos!

Etiquetas: , ,

¿Tienes algo que decir?

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