WebGL Tutorial 7 – Iluminación direccional y ambiental básica

Escrito por el 6 Noviembre, 2011

Bienvenido al séptimo 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. Vamos a aprender a añadir una iluminación básica a nuestra escena, para darle un toque más realista. Ésto requiere un poco más de trabajo en webGL comparado con lo que hay que hacer en openGL, pero esperemos que todo sea fácil de entender.

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.

Puedes usar la caja checkbox de debajo del canvas para usar o no la iluminación, para ver el efecto que produce. Además puedes cambiar de color la iluminación ambiental, y tanto la posición como el color de la luz direccional. Y como en la anterior lección, puedes usar los cursores para girar el cubo y AvPag y rePag para el zoom, pero ahora sólo usaremos el filtro de texturas mipmap, por lo que la F no cambiará nada.

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 seis 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 añadir los dos tipos de iluminación que vamos a tratar.

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.

Antes de entrar en los detalles de cómo manejar la iluminación en webGL, voy a empezar con una mala noticia. WebGL no tiene absolutamente ningúna librería de soporte para la iluminación. A diferencia de openGL, que permite especificar por lo menos 8 tipos de fuentes de luz, y que por supuesto gestiona para tí,en webGL tienes que programarlo tú todo. Pero no te asustes, simular iluminación es bastante fácil una vez se conoce los procedimientos básicos. Si te sientes cómodo con los shaders que hemos visto hasta ahora, no tendrás ningún problema con la iluminación. Además, al tener que codear nosotros (como principiantes) el tema de las luces, adquiriremos una experiencia que hará que sea mucho más fácil de entender futuros tutoriales avanzados de webGL. Depués de todo, añadir iluminación es una tarea básica para conseguir escenas realistas. Sin embargo hay un par de pegas. La primera, es que las sombras que arrojen nuestros objetos también tendremos que calcularlas nosotros, algo que no veremos en este tutorial. La segunda es que a veces la iluminación genera un efecto áspero en superficies con curvas; sin embargo de todas formas en las escenas complejas siempre hay que añadir algo de codificación manual para corregir algunos detalles.

De acuerdo. Empecemos por explicar lo que entendemos como iluminación. El objetivo es ser capaz de simular una serie de fuentes de luz dentro de la escena. Estas fuentes no deben ser visibles en sí mísmas, si no lo que tienen que hacer es iluminar a los objetos 3D de la escena de forma realista, de modo que el lado del objeto que esté expuesto a la luz se debe ver más brillante, y el que está de espaldas, se debe ver más oscuro. Resumiendo, queremos añadir algunas fuentes de luz y calcular el color de cada píxel de la escena según la posición de dichas fuentes. Estoy seguro que alguno se habrá olido que ésto obliga a trabajar con shaders. Efectívamente. En concreto, lo que haremos en esta lección es reescribir el vertex shader, que ahora también se encargará de la iluminación. Para cada vértice hay que averiguar cómo le afecta la luz, y usarlo para ajustar su color. Sólo lo vamos a hacer con una luz, por ahora. Añadir múltiples luces sólo implicaría repetir el mismo procedimiento para cada fuente de luz, sumando al final sus resultados.

Una nota: Los efectos de los píxeles que se encuentran entre los vértices se resolverán haciendo uso de nuestra querida interpolación lineal, ya que estamos trabajando en la iluminación en función de cómo actúa en cada vértice. Ésto significa que las caras de los objetos entre los vértices se iluminarán siempre como si fueran planas. En nuestro ejemplo, no es un problema, ya que estamos dibujando un cubo, pero para superficies curvas, donde hay que calcular cómo afecta la luz píxel a píxel, habrá que usar una técnia distina llamada iluminación por fragmento (o por píxel), que quizás veamos en un futuro tutorial. Lo que estamos haciendo aquí, se llama iluminación de vértices.

Ok, para el siguiente paso: Si nuestra tarea consiste en escribir un vertex shader que controle cómo afecta una fuente de luz a los colores de los vértices, ¿cómo lo hacemos?. Pues bien, un buen punto de partida es el modelo de reflexión Phong. Intentaré explicarlo de la forma más fácil que pueda en los siguientes puntos:

  • A pesar de que en el mundo real hay más tipos de luz, es conveniente pensar que para nuestra escena sólo hay dos tipos:
    1. Luz que viene de una dirección específica y sólo ilumina las caras de los objetos que reciben la luz directamente. La llamaremos luz direccional.
    2. Luz que proviene de todas partes e ilumina toda la escena por igual, sin importar la posición de las caras de los objetos. Ésta se conoce como luz ambiental. En el mundo real la luz ambiental sólo es luz direccional que ha sido dispersada por la reflexión de otros objetos, el aire, polvo, etc. Pero para nuestro propósito, la modelaremos por separado para simplificarla al máximo.
  • Cuando la luz toca una superficie, es reflejada de dos formas:
    1. Difusamente: es decir, independientemente del ángulo con el que llega, es rebotada uniformemente en todas las direcciones. No importa desde qué ángulo estás mirándolo, el brillo de la luz reflejada se rige exclusivamente por el ángulo con el que “golpea” la superficie. La reflexión difusa es lo que imaginamos cuando estamos pensando en un objeto que se ilumina.
    2. Especularmente: Es decir, de una forma semejante a cómo refleja un espejo. La porción de luz que se refleja de esta manera rebota en el mismo ángulo con el que golpeó. En este caso, el brillo de la luz que se ve reflejada por el material depende de si tus ojos están o no en la trayectoria del rebote. La reflexión especular es lo que provoca reflejos en los objetos, y la cantidad de reflexión especular depende directamente del material. Por ejemplo, un trozo de madera sin pulir tendría muy poca cantidad de reflexión, mientras que un metal pulido tendrá mucha.

El modelo Phong añade peculariedades adicionales a nuestro sistema de luces, diciendo que todas las luces tienen dos propiedades:

  1. El valor RGB para las luces difusas que producen.
  2. El valor RGB para las luces especulares que producen.

Y los materiales tienen cuatro más:

  1. El valor RGB para la luz ambiente que reflejan.
  2. El valor RGB de la luz difusa que reflejan.
  3. El valor RGB de la luz especular que reflejan.
  4. El brillo del objeto, que determina los detalles de la reflexión especular.

Para cada punto de la escena, el color es una combinación de los colores de la luz que le incide, los colores propios del material, y los efectos de iluminación. Por lo tanto, para especificar completamente la iluminación de la escena de acuerdo con el modelo Phong, necesitamos dos propiedades de la luz y cuatro de la superficie de nuestro objeto. La luz ambiental no está, por su propia naturaleza, ligada a ninguna fuente de luz particular, pero necesitamos también guardar su cantidad de alguna forma para la escena en su conjunto. A veces puede ser más fácil especificar sólo un nivel ambiental para cada fuente de luz y luego unirlas todas en una sola variable.

De todas formas, una vez que tenemos toda esta información, podemos trabajar con los colores relacionados con la reflexión ambiental, direccional y especular de la luz en cada punto, y luego sumarlas todas para calcular el color total. En la wikipedia hay un excelente diagrama que muestra cómo funciona. Todo lo que nuestros shaders necesitan calcular por cada vértice es la contribución de la componente roja, verde y azul (RGB) de los colores de las luces de ambiente, difusas y especulares, junto con su color propio, y producir un resultado, según el peso que tenga cada una en la combinación.

Ahora, para esta lección, vamos a simplificar las cosas. Sólo vamos a considerar la iluminación de ambiente y la difusa, ignorando la especular. Vamos a utilizar el cubo con la textura de la última lección, y vamos a suponer que los colores de la textura son los valores  que se utilizarán tanto para la reflexiónes difusas y de ambiente. Y por último, vamos a considerar sólo un tipo de iluminación difusa (el tipo más simple de dirección). Vale la pena explicarlo con unos diagramas.

La luz que viene hacia una superficie desde una dirección puede ser de dos tipos. Luz direccional simple, que se encuentra enla misma dirección en toda la escena, y la iluminación que viene de un sólo punto dentro de la escena, y por lo tanto con ángulo diferente en diferentes lugares (por ejemplo, una bombilla).

Para la iluminación direccional simple, el ángulo con el que incide la luz en los vértices de una cara, en el diagrama los puntos A y B, es siempre el mismo. Por ejemplo, pensar que con la luz del sol todos los rayos son prácticamente paralelos.

Si, en cambio, la luz proviene de un punto dentro de la escena, el ángulo de la luz hace que sea diferente en cada vértice. En el punto A, el ángulo con el que llega la luz es de aproximadamente 45º, mientras que en el B, es de 90º.

Esto significa que para puntos (o fuentes) de luz, cada vértice tiene que calcular la dirección con la que le llega, mientras que con la luz direccional simple, sólo tenemos un valor absoluto para todos los vértices afectados. Obviamente la iluminación por puntos de luz es un poco más difícil de manejar, asi que en esta lección sólo utilizaremos la iluminación direccional simple. Los puntos de luz los dejaremos para otra ocasión, aunque espero que no te resulte demasiado duro aprender a utilizarlos por tu cuenta con todo lo aprendido aquí.

Así que ahora que hemos detallado el problema un poco más, sabemos que toda la luz de nuestra escena va a venir de una sola dirección, y esta dirección va a ser la misma para todos los vértices. Ésto significa que podemos ponerla en una variable uniforme, para que el shader pueda recuperarla. También sabemos que el efecto que tiene la luz en cada vértice será determinado por el ángulo que forma la dirección con la de la superficie de nuestro objeto en ese vértice, por lo que tenemos que representar la orientación que tiene esa cara del objeto de alguna manera. La mejor forma de hacer esto con geometría 3D es mediante el uso de vectores normales en cada cara de los objetos, lo que nos permite especificar la dirección en la que se enfrenta la luz mediante un conjunto de 3 números. (En la geometría 2D, se podría utilizar la tangente, pero en la geometría 3D la tangente puede tener pendiente en 2 direcciones, por lo que necesitaríamos 2 vectores para describirla, mientras que con vectores normales sólo necesitamos uno).

Antes de empezar a escribir nuestro shader, y obtenido el vector normal de una superficie en un vértice junto con el vector que describe la dirección de la luz, tenemos que calcular la cantidad de luz que se reflejará difusamente. Éste resulta ser proporcional al coseno del ángulo formado entre los dos vectores. Si es de 0º, quiere decir que la luz está afectando a toda la superficie (está totalmente enfrentada a la luz) y reflejará la luz. A 90º, no se refleja nada. Y para el resto de ángulos, se sigue la curva del coseno. Si el ángulo es mayor de 90 grados, obtendríamos cantidades negativas de luz reflejada. Ésto obviamente es absurdo, asi que para no escoger nunca valores negativos, la función elegirá el valor más grande de entre el coseno del ángulo o cero.

Convenientemente para nosotros, otener el coseno del ángulo formado entre dos vectores normales es un cálculo trivial. Simplemente hay que realizar el producto escalar de los dos vectores, con la función dot, que para nuestro regocijo se ejecuta en la tarjeta gráfica.

Lo siento, pero esta aburrida teoría era necesaria para saber cómo se programa un algoritmo sencillo de cálculo de iluminación direccional simple. Resumimos:

  • Obtén una lista de vectores normales, uno por cada vértice.
  • Obtén un vector normal de la dirección de la luz.
  • En el vertex shader, calcular el producto escalar de los vectores normales del vértice y de la iluminación, junto con el peso de los colores. Además de añadir el componente que define la luz ambiental (que como dijimos, será el mismo para toda la escena).

Vamos a echar un vistazo a cómo se introduce todo ésto en el código, de abajo a arriba. Obviamente, el código HTML de esta lección es diferente al de la anterior, porque tenemos varios campos de entradas de datos para definir las luces, pero no voy a aburrir con sus detalles, que deberían de ser obvios. Nuestro primer punto de contacto será con initBuffers. Justo después de crear el buffer que contiene las posiciones de los vértices, pero antes de que haga lo mismo con las texturas, verás algo de código que se encarga de definir los vectores normales. El aspecto que tiene seguro que ahora te resulta muy familar:

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

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

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

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

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

      // Left face
      -1.0,  0.0,  0.0,
      -1.0,  0.0,  0.0,
      -1.0,  0.0,  0.0,
      -1.0,  0.0,  0.0,
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexNormals), gl.STATIC_DRAW);
    cubeVertexNormalBuffer.itemSize = 3;
    cubeVertexNormalBuffer.numItems = 24;

Como verás, los vectores normales de los vértices no se calculan. Los metemos nosotros adrede. Es importante acordarse de que en nuestro cubo, un vértice sólo pertenece a una cara, y no a tres, ya que en realidad lo que estamos definiendo eran seis cuadrados, cada uno con cuatro vértices, aunque juntos en una única estructura, que pintamos unidos en forma de cubo. Por lo tanto, como un vértice sólo pertenece a una cara, no hay problema alguno en determinar su vector normal. El siguiente cambio es un poco más abajo, en drawScene, y es sólo el código necesario para ligar el buffer con el atributo del programa shader adecuado, para que pueda recuperarlo en los shaders:

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

En la misma función también eliminamos el uso de las diferentes clases de texturas, dejando sólo una:

    gl.bindTexture(gl.TEXTURE_2D, crateTexture);

El siguiente cambio es un poco más complicado. En primer lugar, si el checkbox de HTML indica que la iluminación está marcada o no, hay que comunicárselo al shader:

    var lighting = document.getElementById("lighting").checked;
    gl.uniform1i(shaderProgram.useLightingUniform, lighting);

A continuación, si la iluminación está marcada, leemos los campos de entrada de HTML pertenecientes a las componentes rojo, verde y azul del color RGB que el usuario ha escrito, para pasárselas también al shader:

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

Y ahora, lo mismo con la dirección de la luz:

      var lightingDirection = [
        parseFloat(document.getElementById("lightDirectionX").value),
        parseFloat(document.getElementById("lightDirectionY").value),
        parseFloat(document.getElementById("lightDirectionZ").value)
      ];
      var adjustedLD = vec3.create();
      vec3.normalize(lightingDirection, adjustedLD);
      vec3.scale(adjustedLD, -1);
      gl.uniform3fv(shaderProgram.lightingDirectionUniform, adjustedLD);

Puedes ver cómo normalizamos el vector antes de pasárselo al shader, utilizando funciones de la librería vec3, que al igual que mat4, pertenecen a glMatrix. El primer ajuste, vec3.normalice, modifica la escala de las componenentes X, Y y Z del vector para que su módulo sea 1. Las normales que definimos para cada vértice, como podrá comprobar, también tienen módulo 1, pero como el vector de iluminación lo meterá el usuario, es un dolor obligarlo a que lo haga ya directamente normalizado. Es un requisito indispensable para poder realizar el producto escalar para obtener la cantidad de iluminación a reflejar en el color de la textura. El segundo ajuste es multiplicar el vector por un número escalar, el -1. En otras palabras, lo que estamos haciendo es invertir su dirección. Lo hacemos así porque al usuario le pedimos que introduzca la dirección donde va la luz, pero para los cálculos, lo que en realidad necesitamos saber, es de dónde viene (es decir, la dirección original invertida). Y finalmente le pasamos el vector al shader con gl.uniform3fv, que ya son del tipo Float32Array, pues al usar funciones de vec3, se convierte el vector a ese tipo automáticamente.

El siguiente trozo de código es simple; sólo copia los componentes del color de la luz direccional simple a la variable uniforme apropiada del shader:

      gl.uniform3f(
        shaderProgram.directionalColorUniform,
        parseFloat(document.getElementById("directionalR").value),
        parseFloat(document.getElementById("directionalG").value),
        parseFloat(document.getElementById("directionalB").value)
      );
    }

Y éstos son todos los cambios de drawScene. En la función de eventos del teclado, eliminamos el código que maneja la tecla F. Otro cambio más interesante está en la función setMatrixUniforms, que como recordarás, se encargaba de mover las variables uniformes de las matrices de modelo-vista y proyección de javascript a webGL. Tenemos que añadir cuatro líneas para crear ua nueva matriz, basada en la de modelo-vista:

    var normalMatrix = mat3.create();
    mat4.toInverseMat3(mvMatrix, normalMatrix);
    mat3.transpose(normalMatrix);
    gl.uniformMatrix3fv(shaderProgram.nMatrixUniform, false, normalMatrix);

Como se podría esperar de una matriz llamada normal, será usada para transformar (rotar, trasladar, excalar…) las normales. No podemos transformarlas de la misma forma que hacíamos con la posición de los vértices, usando la matriz modelo-vista, porque al aplicar rotaciones y traslaciones se obtendrían falsos (y desnormalizados) resultados. Por ejemplo, si hacemos una traslación de (0,0,-5) a un vector normal (0,0,1), se convertiría en (0,0,-4), que no sólo tiene un módulo no normalizado, además apunta a una dirección incorrecta.

El modo adecuado para conseguir vertores normales en la dirección correcta es usar una matriz inversa transpuesta, rellenada con una porción 3X3 (las 3 primeras filas con las 3 primeras columnas) de la matriz modelo-vista. La explicación algebraica del por qué es así queda fuera del objetivo de la lección; además me huelo que si continúo con las matemáticas, un montón de lectores se quedarán por el camino. Puedes encontrar algo de información en inglés aquí.

Asi que digamos que calculamos la matriz normalizada haciendo magia con las funciones mat4.toInverseMat3 y mat3.Transpose, y que el resultado lo ponemos en una variable uniforme del programa shader para que pueda utilizarla en el vertex shader.

Hay unos cambios triviales en el código que carga la textura, como fue eliminar los dos tipos de filtro de texturas para quedarme sólo con el mipmap, y que no comentaré, por su simplicidad. También hay un pequeño código nuevo en initShaders para definir el atributo vertexNormalAttribute en el programa shader para que drawScene pueda enviar los vectores normales al shader, y exactamente lo mismo con cada una de las nuevas variables uniformes que hemos necesitado. Ninguno de estos códigos vale la pena explicarlos con más detalle, basta con mirar la función, asi que pasemos directamente a los shaders.

El fragmetn shader es más simple, asi que veámoslo primero:

  #ifdef GL_ES
  precision highp float;
  #endif

  varying vec2 vTextureCoord;
  varying vec3 vLightWeighting;

  uniform sampler2D uSampler;

  void main(void) {
     vec4 textureColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
     gl_FragColor = vec4(textureColor.rgb * vLightWeighting, textureColor.a);
  }

Como puedes ver, estamos extrayendo el color de la textura justo como en el tutorial anterior, pero en vez de devolverlo directamente, estamos ajustando sus componentes R,G y B mediante una variables varying llamada vLightWeighting. Esta variable es un vector de 3 elementos y (como deberías haber supuesto) contiene los factores de rojo, verde y azul de la iluminación calculados por el vertex shader.

¿Cómo lo hace? Veámos el código del vertex shader:

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

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

  uniform vec3 uAmbientColor;

  uniform vec3 uLightingDirection;
  uniform vec3 uDirectionalColor;

  uniform bool uUseLighting;

  varying vec2 vTextureCoord;
  varying vec3 vLightWeighting;

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

    if (!uUseLighting) {
      vLightWeighting = vec3(1.0, 1.0, 1.0);
    } else {
      vec3 transformedNormal = uNMatrix * aVertexNormal;
      float directionalLightWeighting = max(dot(transformedNormal, uLightingDirection), 0.0);
      vLightWeighting = uAmbientColor + uDirectionalColor * directionalLightWeighting;
    }
  }

El nuevo atributo, aVertexNormal, por supuesto contiene los vértices normalizados que especificamos en initBuffers y que le pasamos a este shader en drawScene. uNMatrix es nuestra matriz normalizada, uUseLighting es la variable uniforme donde especificamos si la iluminación está activa o no, y uAmbientColor, uDirectionalColor y uLightingDirection son las variables para el color de la luz de ambiente, el color de la luz direccional simple, y la dirección de la luz direccional simple. Todas ellas son los valores que se especifican en los campos de texto del formulario de la página web.

Con toda la teoría que hemos visto, debería de ser algo más fácil entender todos los cambios que hemos hecho en el código para simular la luz. La principal salida del shader es la variable varying uLightWeighting, que se utiliza para ajustar el color de la imagen en el fragment shader. Si la iluminación está apagada, devolvemos el vector (1,1,1). Cualquier cosa multiplicada por este vector, no cambiará, es decir, el fragment shader utilizará los colores originales de la textura porque estará multiplicando por uno. Si la iluminación está encendida, calculamos el vector normalizado que representa la dirección de ese vértice que vamos a pintar a partir de la matriz normalizada, luego hacemos el producto escalar de esa orientación por la dirección de la luz direccional (con un resultado mínimo de 0.0, como dijimos anteriormente) para obtener el peso con el que la luz incide en ese punto. A continuación calculamos el peso de la luz final para el fragment shader sumándole a la luz de ambiente, el resultado de multiplicar el color de la luz direccional por el peso que tiene la luz para ese vértice. El resultado de ésto es justo lo que necesita el fragment shader. Y por fín, hemos iluminado la escena.

Ahora ya tienes unas nociones básicas de cómo funciona la iluminación en los sistemas gráficos 3D como webGL, como son la iluminación de ambiente y la luz direccional simple. No es fácil de comprender, sobre todo si no se tienen conocimientos matemáticos de los métodos algebraicos con el uso de matrices para hacer transformaciones, pero viendo el código, y sobre todo, experimentando con él, al final, al menos se consigue poder utilizarlo sin necesidad de entender profundamente todos los detalles.

Es posible que te hayas dado cuenta de un terrible defecto que tiene la iluminación direccional que hemos visto. Se trata de que nuetra luz es capaz de “atravesar” los objetos de la escena, es decir, la luz incide por igual en todos los vértices de todos los objetos de la escena, sin importarle si hay algún objeto que nos bloquee la luz total o parcialmente. Ésto se debe, como ya comenté al principio, a que no estamos dibujando las sombras que generan los objetos, que son en realidad quienes oscurecen los objetos que están detrás.

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 artículo del sombreado de Phong en la wikipedia me ayudó muchísimo para escribir esta lección, especialmente para entender las matemáticas que hay detrás de todo esto. Las diferencias entre las matrices necesarias para ajustar las posiciones de los vértices y sus correspondiente versión normalizada fue mucho más fácil de comprender por el tutorial de LightHouse 3D, especialmente una vez que el usuario Coolcat aclaró ciertos detalles en los comentarios. La caja giratoria de Chris Marrin (junto con la versión extendida de Jacob Seidelin) fue también una muy útil guía, y la caja giratoria de Peter Nitsch también ayudó. 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: , ,

Comentarios (2)

  • Hola!!

    Muy completo, la verdad es que como programador autodidacta de siempre, mis gustos por las matemáticas tuvieron su peor momento en los tiempos de escuela, por lo que he tenido que darme algunas vueltas por ahí, pero ahora de una forma mucho mas agradable.

    No entendí el por que quitar el botón de filtro y su funcionalidad, aunque lo comprobaré ahora a prueba limpia.

    Entiendo de que no estamos manejando la luz y controlando sus efectos en los objetos del espacio… si no que estamos controlando los efectos de la luz desde los objetos del espacio (por favor corrígeme si me equivoco). Ahora, ¿es así como se manejan en general las composiciones 3d?

    Muchas gracias.
    Saludos.

  • Lo primero que tengo que decir es que yo también soy un aprendiz de webGL, no soy ningún experto. Fui aprendiendo webGL al mismo tiempo que traducía los tutoriales, como dije en el primero, por lo que realmente mis opiniones pueden ser incorrectas.

    El filtro lo eliminó por cuestión de simplificar el código, dejando lo mínimo necesario para hacer el tutorial. Realmente la técnica mipmaping es el mejor filtro de texturas, por norma general, así que de ahora en adelante será el único que se utilice en los tutoriales.

    En cuanto a la la luz… sí, eso será así siempre, al menos a bajo nivel. La causa es muy simple. WebGL lo que hace es pintar objetos; su fragment y vertex shader se ejecutan, como hemos visto, por cada cara y cada pixel, respectivamente, de los objetos que halla en la escena. No por cada luz. Por lo tanto, cuando vamos a pintar un pixel concreto, debemos calcular cómo todas las fuentes de luz (y sombras en códigos más complicados) actúan sobre él.
    Por otra parte, el principal “problema” es que webGL aún no es estandar, está en fase experimental, y de momento, sus “librerías” de apoyo son muy escasas. Por ejemplo, openGL (o DirectX) realiza los cálculos de iluminación y sombreado por nosotros, indicándole en unos búfferes dedicados a la iluminación, su tipo, color, dirección e intensidad. Luego, su núcleo hará el trabajo duro de calcular los efectos de esas luces y sombras en la escena por nosotros.
    Pero ahora toda la iluminación la estamos programando manualmente, de la única forma posible. Si utilizas alguna librería de webGL de alto nivel, como por ejemplo THREE.js ( http://www.html5rocks.com/en/tutorials/three/intro/ ), las fuentes de luz las tratará como entidades independientes, pero a bajo nivel, es decir, cuando tenga que escribir los shaders, deberá hacer algo parecido (pero seguramente más completo y profesional) a lo que hemos visto aquí.

¿Tienes algo que decir?

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