WebGL Tutorial 8 – El buffer de profundidad, transparencias y blending

Escrito por el 8 noviembre, 2011

Bienvenido al octavo 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 8 del tutorial sobre openGL de NeHe. En él veremos la técnica blendig (mezclas) y sus útiles efectos secundarios, todo ello relacionado con el funcionamiento del buffer de profundidad.

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 el checkbox de debajo del canvas para encender o apagar el código “blending”, es decir, en este demo, el efecto de transparencia. También puedes ajustar el factor alfa (que explicaremos después), y por supuesto, la iluminación.

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 siete 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 la transparencia.

Pero antes de empezar con el código, hay que hablar un poco de la teoría que hay detrás de estos efectos. Me gustaría empezar por hablar sobre qué es la técnica blending, y cómo se usa en la actualidad, pero para ello primero hemos de explicar cómo funciona el buffer de profundidad.

El Buffer de Profundidad

Cuando le dices a webGL que dibuje algo, como deberías de recordar de la lección 2, los datos pasarán por una serie de etapas. A brocha gorda, las etapas eran:

  1. Se ejecuta el vertex shader una vez por cada uno de los vértices que haya en la escena, para calcular todo lo relacionado con ellos para dibujarlos.
  2. Se realizan interpolaciones lineales entre los vértices, que indica qué fragmentos (que por ahora podemos entender como si fueran píxeles) necesitan ser pintados.
  3. Por cada fragmento, se ejecuta el fragment shader para calcular el color o colores de relleno de cada pixel que contiene.
  4. Se escribe el resultado en el buffer de frame.

Por último, el buffer de frame contiene la imagen que se dibujará en el canvas. ¿Pero qué pasa si dibujas dos cosas? Por ejemplo, ¿Qué pasaría si dibujas un cuadrado con el centro en (0,0,-5) y otro cuadrado del mismo tamaño en (0,0,-10)? Tú no quieres que el segundo cuadrado sobrescriba al primero, porque el segundo cuadrado está detrás, y debe ser ocultado por el primero.

El modo en que webGL resuelve esta situación es mediante el uso de un buffer de profundidad. Cuando los fragmentos están siendo escritos en el buffer de frame (memoria de video), justo después de que el fragment shader  los haya tratado, además del color RGB también se almacena un valor de profundidad relacionado con el valor Z ligado a ese fragmento, aunque no tiene exactamente el mismo valor. Al buffer de profundidad a veces también se le conoce como buffer Z.

¿Qué quiero decir con que es un “valor relacionado”? Bien, a webGL le gusta que todos los valores Z estén en un rango de 0 a 1, con el 0 para los puntos más cercanos, y el 1 para los más lejanos. Ésto está oculto a nuestros ojos por la matriz de proyección que creábamos según la perspectiva elegida en el drawSceme. De momento basta con sabes que cuanto mayor sea el valor del Z-buffer, más lejos estará. Nótese que funciona justo al contrario que las coordenadas del eje Z, de ahí que esté relacionado, pero no sea exactamente lo mismo.

Ok, éso es el buffer de profundidad. Ahora recordemos un trozito del código que usamos al inicializar el contexto webGL en la lección 1, donde hacíamos lo siguiente:

    gl.enable(gl.DEPTH_TEST);

Esta intrucción del sistema webGL dice qué hacer cuando se va a escribir un nuevo fragmento en el buffer de frame, y básicamente significa “usa el buffer de profundidad”. Y así, junto con otros comandos de webGL que se combinarán, se utilizará la función de cálculo de profundidad. Dicha función usará un algoritmo por defecto, aunque podemos indicarle expresamente cuál utilizar con:

    gl.depthFunc(gl.LESS);

Ésto significa “si nuestro fragmento tiene un valor Z menor (less) que el que haya en el buffer para esa misma posición, usa el nuevo fragmento para sustituir el viejo”. Éste simple código basta para dar un comportamiento razonable a la escena, las cosas que están cerca ocultarán a las que están lejos. También se puede utilizar otro tipo de valores para el algoritmo, aunque sospecho que muy rara vez se utilizan.

Blending

El blending (la mezcla) es una alternativa a éste proceso. Con la comprobación de profundidad, usaremos la función de profundidad para elegir entre reemplazar o no un fragmento existente en el buffer por uno nuevo. Cuando estamos haciendo blending, usaremos una función de mzclado para combinar los colores de los fragmentos en conflicto para crear un fragmento nuevo, que será el que se escriba en el buffer.

Por fin, echémos un vistazo al código. Después de todo, es exactamente lo mismo que hicimos en la lección 7,  la mayoría de los cambios importante son un pequeño código que añadiremos al drawScene. Pero primero, comprobamos si el la mezcla está activada n el HTML:

    var blending = document.getElementById("blending").checked;

Si lo está, usamos la función de mezclado para combinar los dos fragmentos:

    if (blending) {
      gl.blendFunc(gl.SRC_ALPHA, gl.ONE);

El primer parámetro de esta función define cómo se tiene que hacer la mezcla. Es enrevesado, pero no difícil. Primeramente, para entender mejor la explicación, definimos dos términos: el fragmento fuente es el fragmento que queremos dibujar ahora, y el fragmento de destino es el que hay en el buffer para esa posición. El primer parámetro de gl.blendFunc determina el factor fuente, y el segundo determinal el factor destino. Estos factores son números que se usan en la función de mezcla. En este caso, estamos indicando que que el factor fuente es el valor alfa del fragmento fuente, y el factor destino es el valor constante 1. Hay otras posibilidades, por ejemplo, si se usa un gl.SRC_COLOR para especificar el color fuente, trabajarás por separado con cada uno de los componentes de los colores, es decir, con el rojo, verde, azul y el valor alfa (indica el porcentaje de transparencia), cuyo valor inicial es el componente del color correspondiente del fragmento fuente.

Imaginemos que webGL está intentando calcular el color de un fragmento, cuando el fragmento destino tiene los valores RGBA (Rd, Gd, Bd, Ad) y el fragmento fuente tiene (Rf, Gf, Bf, Af). Además, también tenemos los factores fuente (Fr, Fg, Fb, Fa) y destino (Dr, Dg, Db, Da).

Para cada componente del color, webGL lo calculará como sigue:

  • Rresultado = Rf * Fr + Rd * Dr
  • Gresultado = Gf * Fg + Gd * Dg
  • Bresultado = Bf * Fb + Bd * Db
  • Aresultado = Af * Fa + Ad * Da

Pero en nuestro caso, gl.RSC_ALPHA, estamos diciendo (el resto de los componentes del color es lo similar):

  • Rresultado = Rf * Af + Rd

Normalmente esto no sería la forma ideal de crear transparencias, pero funciona muy bien para este ejemplo, donde la iluminación se puede desactivar. Y en este punto vale la pena enfatizar que el “blending” (mezclas) no es lo mismo que “transparencia”, el blending es simplemente una técnica que se puede utilizar para obtener efectos de transparencias, entre otros muchos usos.

Ok, continuemos:

      gl.enable(gl.BLEND);

La técnica blending está desactivada por defecto, así que como es obvio, la tenemos que activar.

      gl.disable(gl.DEPTH_TEST);

Éste es un poco más interesante. Desactivamos el buffer de profundidad para comprobar la escena. Si no lo hacemos entonces ocurrirá que el blending se producirá en algunos casos, y en otros no. Por ejemplo, si dibujamos una cara trasera de nuestro cubo que estará total o parcialmente oculta por una de las caras delanteras que todavía no se han dibujado, se producirá el blending, cuando webGL pinte la cara de delante. Por el contrario, si pintamos primero una de las caras delanteras, todas las caras traseras quedarán ocultadas sin blending. Ésto no es lo que queremos.

Los lectores más perspicaces se abrán dado cuenta de que el orden con el que se pintan los objetos es decisivo para el blending, algo que nunca había ocurrido en nuestras lecciones anteriores. Hablaremos más sobre ello en un momento. Ántes,

      gl.uniform1f(shaderProgram.alphaUniform, parseFloat(document.getElementById("alpha").value));

Aquí estamos cargando el valor alfa del campo de texto correspondiente en el HTML, y mandándolo al programa shader relacionándolo con una variable uniforme creada expresamente para ello, como ya sabemos hacer. El principal motivo por el que estamos especificando el valor de alfa de transparencia a mano, es porque la imagen que usamos para la textura no tiene un canal alfa propio (es decir, sólo tiene el RGB, por lo que por defecto implícitamente tomará un 1 como valor alfa en cada pixel). Así también tenemos la oportunidad de poder modificar el valor alfa y ver cómo afecta a la imagen del cubo.

El código restante en drawScene sólo restaura el modo normal con uso del buffer de profundidad que ya conocemos, para en caso de que se desactive el blending:

    } else {
      gl.disable(gl.BLEND);
      gl.enable(gl.DEPTH_TEST);
    }

También hay un pequeño cambio en el fragment shader para usar el valor alfa que mandamos anteriormente, uAlpha:

  #ifdef GL_ES
  precision highp float;
  #endif

  varying vec2 vTextureCoord;
  varying vec3 vLightWeighting;

  uniform float uAlpha;

  uniform sampler2D uSampler;

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

Y con ésto acabamos los cambios que había que hacer en el código.

Ahora explicaré aquél punto que pospuse anteriormente. El efecto de transparencia que se obtiene en este ejemplo es bastante bueno, parece una vidriera de verdad. Sin embargo, pruebe ésto: En la demo html, cambia la dirección Z de la luz direccional, poniéndola en sentido inverso, es decir, elimina el símbolo negativo “-“. Verás que todo el bonito efecto de vidriera desaparece.

La razón por la que ocurre ésto es que con la iluminación original, la cara posterior del cubo recibe poca luz. Ésto significa ue los valores R,G y B de dichas caras son bajos, por lo que cuando la ecuación

  • Rresultado = Rf * Ra + Rd

…es calculada, se obtiene un valor bajo también. Por decirlo de otro modo, tenemos una iluminación cuya dirección hace que las cosas que estén en la parte trasera sean menos visibles. Si iluminamos la escena desde detrás, el efecto de transparencia no funciona tan bien, no es realista.

Entonces, ¿cómo podemos arreglar la transparencia?. Bueno, el FAQ (preguntas frecuentes) de OpenGL dice que es necesario utilizar un factor fuente SRC_ALPHA y un factor destino ONE_MINUS_SRC_ALPHA. Pero todavía tendríamos el problema de que el origen y el destino de los fragmentos son tratados de forma diferente según el orden con el que pintamos los objetos de la escena, y éste es amigos, el sucio secreto de la transparencia en OpenGL-WebGL. Veámos el resto de lo que dice el FAQ:

Cuando se utiliza el buffer de profundidad en una aplicación, es necesario ser cuidadoso con el orden con el que se renderizan las primitivas. Las primitivas totalmente opacas deben ser dibujadas en primer lugar, seguidas por las primitivas parcialmente opacas, que a su vez deberán dibujarse desde las más lejanas hasta las más cercanas, según estén en la escena.  Si no renderizas las primitivas en este órden, podría ocurrir que las primitivas que debieran verse a través de la transparencias de otra u otras primitivas, se perdieran.

Así que ahí lo tienen. La transparencia mediante el blending es tan difícil como incómoda, pero si se controlan algunos aspectos de la escena, como por ejemplo hemos hecho nosotros con la dirección de la iluminación direccional, se puede conseguir el efecto deseado sin demasiada complejidad. O puede hacerlo bien, como indica el FAQ de OpenGL, pero tendrás que llevar cuidado en dibujar las cosas en ese orden específico para conseguir un buen aspecto final.

Afortunadamente, el blending también es útil para otros efectos, que veremos en posteriores tutoriales. Pero de momento, ya tenemos una base sólida con todo lo que hay que saber sobre el buffer de profundidad y sobre cómo aprovecharse del blending para simular transparencia.

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.

Hablar con Jonathan Hartley hizo que el buffer de profundidad y el blending mucho más fácil de entender para mí; y la descripción de Steve baker del buffer-Z también me fue muy útil. 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 (1)

¿Tienes algo que decir?

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