Tutorial WebGL 16 – Texturas renderizadas

Escrito por el 14 diciembre, 2011

Bienvenido al decimosexto tutorial de la serie Aprende webGL. Es una traducción no literal de su respectivo tutorial en Learning webGL.En él vamos a enseñar una técnica que os resultará muy útil: La representación de una escena 3D como si fuera una textura, que puede ser “pegada” a la superficie de cualquier objeto en otra escena 3D diferente. Éste es un recurso muy importante, no sólo porque hace posible crear escenas dentro de una escena, como vamos a hacer con el ejemplo de hoy, sino también porque es la base necesaria para la selección de objetos 3D con el ratón de nuestra escena (picking), el cálculo de sombras, algunos tipos de reflexiones de la luz (cuyos rebotes iluminan a otros objetos), y muchos otros efectos 3D más complicados.

Si tu navegador soporta webGL, este video muestra lo que deberías ver:

Abre este enlace para ver el ejemplo en vivo de webGL, aquí tienes una textura, aquí la otra, y aquí el modelo del portátil, para descargarlos 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. Podemos ver un ordenador portátil blanco girando en el vacío, con todos los efectos de iluminación que hemos visto hasta ahora (incluyendo un reflejo especular en la pantalla). Pero lo más interesante, es que en la pantalla del portátil se está mostrando otra escena 3D (la luna con la caja en órbita) que vimos en el tutorial 13. Queda claro que lo que está pasando es que estamos dibujando una escena completa en 3D en una textura, y luego utilizamos esa textura para “pintar” la superficie que representa el monitor.

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 de los códigos que usamos en los tutoriales 13 y 14, por lo tanto asegúrate de que entendiste correctamente todo lo que explicamos en ellos.

El código de este tutorial tiene bastantes cambios, así que vamos a empezar por el principio del todo, como en el primer tutorial. La primera función que se ejecuta, cuando se carga la página, es nuestra vieja webGlStart. He comentado los trozos que son iguales, para que se destaquen los nuevos, como siempre:

Aquí hacemos nuestra configuración habitual. Iniciamos webGL, cargamos nuestros shaders, creamos los búffers de vértices, cargamos las texturas (la caja y la luna) y hacemos una solicitud JSON para que se cargue el modelo del portátil (loadLaptop es realmente es una función nueva, pero idéntica a lo que hicimos para cargar la tetera del tutorial 14, por lo que no entraré mucho en detalle). La nueva función, initTextureFrameBuffer, se encargará de crear una memoria intermedia donde almacenar una escena 3D independiente como si fuera una imagen 2D (textura), para ser usada allí donde queramos. Antes de mostrar el código, explicaré qué significa tener una memoria intermedia (o framebuffer).

Al hacer algo con webGL, es obvio que necesitamos algún tipo de memoria en la tarjeta gráfica para ir almacenando los resultados de los cálculos antes de ser pintados en pantalla. WebGL nos proporciona un control muy preciso sobre qué tipo de memoria se ha de usar para realizar éste proceso. Es necesario, como mínimo, un espacio suficiente para guardar los colores de cada píxel que compone el resultado final. Es también muy importante, aunque no esencial, más espacio para almacenar los datos del buffer de profundidad, para conseguir que los objetos de delante oculten a los de detrás, como explicamos en el tutorial 8. Y hay también más tipos de buffers que pueden ser muy útiles, como el stencil buffer (para la generación de sombras y reflexiones), algo más complicados de utilizar por la naturaleza de webGL (que no trae soporte de luces).

El framebuffer, es un “objeto” en donde podemos renderizar (pintar) una escena 3D, que está compuesto por varios trozos de memoria. Por ejemplo, el framebuffer por defecto, es la superficie del CANVAS donde hemos estado dibujando la escena en todos los tutoriales anteriores. Pero nada nos impide renderizar toda la escena en otro framebuffer que no se muestra en la pantalla en su lugar. En este tutorial, vamos a crear nuestro propio framebuffer (memoria intermedia) que estará compuesto de dos búfferes adicionales, uno donde almacenar los colores de cada píxel, y el otro donde almacenar la información necesarioa para realizar los cálculos de profundidad.

Una vez explicado brevemente lo que necesitamos, veámos  por partes lo que hace initTextureFrameBuffer:

Nuestra función requerirá de dos variables globales nuevas: una donde almacenar el framebuffer con todo lo que se muestra en el monitor del portátil, y otra donde guardar la textura que almacena el resultado del renderizado de este framebuffer, que necesitaremos cuando pintemos la superficie del monitor. Dentro de la función:

El primer paso es crear el framebuffer, siguiendo el patrón característico de webGL (igual que hacíamos con el resto de buffers de texturas, vértices, etc), y seleccionarlo como framebuffer actual, para que todas las funciones posteriores que trabajen sobre framebuffers, lo hagan sobre el que acabamos de crear. Como en ocasiones anteriores, el framebuffer no tiene atributos declarados, pero javascript permite añadir atributos nuevos a cualquier objeto, por lo que le asociamos al framebuffer un par de ellos que contienen el ancho y el alto de la textura donde se pintará la escena de la luna y la caja. He escogido 512 x 512 píxeles como tamaño porque como tendrías que recordar, las texturas han de tener anchuras y alturas que sean potencias de 2, y ví que 256 x 256 daba un resultado demasiado borroso, y 1024 x 1024 no mejoraba casi nada el actual (pero que obviamente consumiría más memoria y tiempo de procesamiento para calcularlo).

A continuación, creamos un objeto textura en la variable global, y le configuramos los parámetros de siempre:

Pero con una pequeña diferencia, la llamada a gl.textImage2D tiene unos parámetros muy distintos:

Normalmente, cuando creábamos texturas para mostras las imágenes que cargábamos en javascript, hacíamos una llamada a gl.textImage2D pasándole en uno de los parámetros la imagen, para “unirlos” y que webGL almacenara esa imagen en la memoria de la tarjeta gráfica. Sin embargo, ahora no temenos ninguna imagen, con lo que tenemos que llamar a a misma función con otros parámetros (aprovechando que javascript permite la sobrecarga de métodos, aunque de forma simulada). En los nuevos parámetros tenemos que decirle a la tarjeta gráfica que nos gustaría reservar un espacio vacío de la su memoria; para ello indicamos el tamaño de la textura y la información y precisión que tiene cada píxel de la misma. Además, el último parámetro que le pasamos a la función es un array (lista, tabla, arreglo, etc) que será copiado al principio de ese espacio de memoria. Dándole un null, es una forma de decirle que no tiene que copiar nada aún.

Ok, ahora tenemos una textura vacía que puede almacenar los colores finales de cada píxel de nuestra escena, cuando sea renderizada. A continuación, creamos un búffer de profundidad para almacenar, obviamente, la información de la profundidad de los distintos objetos de la escena.

Lo que hemos hecho aquí es crear localmente un objeto renderbuffer, que es un tipo genérico de buffers que sirven para almacenar en la tarjeta gráfica cualquier tipo de información asociada con los framebuffers que tengamos. Como siempre, webGL trabaja con un puntero al renderbuffer “actual”, para trabajar sobre él de forma implícita. Por último, llamamos a gl.renderbufferStorage, para que nuestro buffer auxiliar reserve memoria suficiente para almacenar por cada píxel del nuestra textura (proporcionándole su anchura y altura) un valor de profundidad de 16 bits.

Luego:

Así unimos los dos buffers que acabamos de crear con el framebuffer (que recuerde, señalamos como framebuffer “actual” en la segunda línea de ésta función). En el primero, le estamos diciendo que el espacio de renderizado para los colores del framebuffer (gl.COLOR_ATTACHMENT0) es nuestra textura, y que la memoria que debe utilizar para la información ed profundidad (gl.DEPTH_ATTACHMENT) es el renderbuffer para la profundidad que acabamos de crear.

Con ésto ya tenemos reservada toda la memoria necesaria para nuestro framebuffer. WebGL sabrá que hacer cuando empezemos a usarlo. Como en alguna ocasión anterior, aunque lo siguiente no es del todo necesario, nunca está de más limpiar la mesa después de haber comido en ella. Es decir, ponemos los punteros “actuales” a los valores por defecto, es decir, a null.

…Y con ésto ya está configurado nuestro framebuffer. ¿Cómo lo usamos? Pues bien, el primer lugar que deberíamos mirar es drawScene:

Viendo el código, debería ser bastante obvio deducir lo que estamos haciendo. Como dije antes, el framebuffer por defecto (cuando vale null), es dibujar directamente sobre el CANVAS. Por lo tanto, antes de empezar a pintar la escena del portatil giratorio, es necesario precalcular la textura de la caja y la luna. Para ello elegimos nuestro framebuffer creado para ese menester, llamamos a una función drawSceneOnLaptopScreen (que simplemente es una versión muy parecida al drawScene del tutorial 13, por lo que no mostraré aquí). Cuando termine de ejecutarse, volvemos a elegir el framebuffer por defecto, y pintamos la escena del portátil, y utilizará la textura contenida en el framebuffer como imagen para pintar la superficie de la pantalla. Empezamos con un poco de código para establecer la matriz de modelo-vista, y para hacer girar el portátil una cantidad determinada en laptopAngle (que como en otros tutoriales, se actualiza contínuamente en la función animate):

Ahora enviamos las posiciones y colores de las fuentes de luz a la tarjeta gráfica como de costumbre:

Luego pasamos le pasamos la información de los parámetros relacionados con el portátil y sobré cómo influye la iluminación en él. Puedes recordar de que ya en el tutorial 7, cuando describíamos el modelo de Phong, mencionábamos que los materiales tenían diferentes brillos para cada tipo de luz (ambiental, difusa, especular). A continuación, como en todos los tutoriales anteriores, supondremos de forma simplificada que todos los colores que se van a usar eran fijos: blanco en el caso de que no se utilice una textura, o el color de algún punto de la textura en concreto. Por razones que veremos después, ésta simplificación no nos vale para la pantalla del portátil, donde tendremos que usar un nuevo tipo de color, el color de emisión. Sin embargo, para el cuerpo del portátil, nos basta con decirle a webGL que el portátil es blanco:

El siguiente paso es, si la información de los vértices del portátil se ha terminado de descargar por JSON, dibujarlo. El código, sobre todo después del tutorial 14 (prácticamente ha sido un corta y pega), debería de serte muy familiar:

Y con ésto, el cuerpo del portátil ha sido dibujado completamente. Ahora toca dibujarle la pantalla. La configuración de la iluminación relacionada con ella se realiza en primer lugar, y en ella requeriremos del nuevo tipo de color de emisión, almacenado en materialEmissiveColorUniform:

¿Y qué diantres es el color de emisión? Pues bien, las pantallas de los portátiles no sólo reflejan la luz, también la emiten. Queremos que el color de la pantalla, que será determinada por el color de la textura que pegaremos en ella, tenga un peso mucho mayor que los efectos de iluminación en ella. Podríamos haber apagado la iluminación de la escena antes de pintar la pantalla mediante una variable uniforme que ya conocemos, para después volver a encenderla, pero el resultado no habría sido tan bueno como el que proporciona la técnica que vamos a usar a continuación. Para ello, de momento nos basta con especificar un color de emisión de la pantalla, que será usado en los shaders más adelante.

(Una nota al margen: Vale la pena señalar de que el color de emisión de un objeto debería afectar a los objetos de la escena que tenga alrededor, como si fuera una fuente de luz, pero por simplicidad haremos que ese color sólo afecte a la pantalla.)

El requisito fundamental de los colores de emisión es que el color final de emisión no es fijo, si no que viene determinado por el color que tenga la textura en esa zona(píxel) de la pantalla. Por el contrario, el color del reflejo especular sobre la pantalla sigue dependiendo del color de la fuente que provoca dicho reflejo.

De acuerdo, si seguimos adelante, toca seleccionar los buffers que contienen la información de los vértices de la pantalla del portátil:

A continuación, le decimos a webGL que queremos utilizar la textura que hicimos antes:

Y finalmente, dibujamos la pantalla:

¡Y con ésto, hemos acabado con todo el código necesario para dibujar una textura que a su vez representa a otra escena!

Aún quedan unos cuantos cambios que no hemos visto, pero que bastará con comentar brevemente lo que hacen para entenderlas. Tenemos a loadLaptop y handleLoadedLaptop que cargan el modelo del portátil por JSON, como hicimos con la tetera. También hay unas líneas extra al final de initBuffers, para iniciar los buffers (una superficie plana cuadrada) de la pantalla del portátil por separado, para pintar la textura sobre ella y trabajarla por separado, como hemos visto antes. Queda un poco feo, pues debí meter esa información también en el modelo JSON, algo que quizás modifique en un futuro remoto.

Y por último, están los cambios que hemos tenido que hacerle al fragment shader, que tiene que manejar el nuevo tipo de color de emisión que le suministramos a través de una variable uniforme uMaterialEmissiveColor. Pero es un cambio sencillo, lo único que hacemos, en la parte final del código, es obtener el color correspondiente de se fragmento de la textura, multiplicarlo por el factor incremento (que pusimos a 0.0 antes de dibujar la la caja, la luna, y el portátil; y a 1.5 para la pantalla del portátil, para cada componente RGB del color)  que le enviamos en una variable uniforme, y sumárselo color final que tiene ese fragmento. Así, el color de la textura realmente se suma dos veces y pico, con lo que el peso del color de la textura es mucho mas relevante para el color final que el de la iluminación, haciendo que su color sea más intenso que tendría de forma normal, dando sensación de que emite luz:

¡Y ahora sí, hemos acabado!

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.

Necesité de mucha ayuda para que ésto funcionara, en particular, debido a que la primera versión tenía errores que no aparecían cuando se ejecutaba en mi ordenador portátil. Particularmente, me gustaría dar las gracias a Marco Di Benedetto, el creador de SpiderGL, y Paul Brunt, de GLGE, por corregirme lo que había hecho mal. Pero también le debo mucha gratitud a las personas que probaron una y otra vez las distintas versiones corregidas que iba sacando conforme aparecían errores, hasta que finalmente creo que conseguí hacer funcionar en todos los sistemas: Stephen White (quien también me dejó claro que el uso de texturas renderizadas era básico para la selección de objetos por parte de la GPU), Denny (creador de EnergizeGL), blinblin, sin nombre, Jacob Seidelin, Pyro Technick, ewgl, Peter, Springer, Christofer, Thormme y Titán.

Otros de los sitios donde me inspiré fueron la guía de programación de OpenGL ES 2.0, la biblioteca GLGE de Paul Brunt, y de diversos foros de consulta como éste, éste y éste. Obviamente, la especificación oficial de webGL ayudó también…

El modelo 3D del portátil fue suministrado gratuítamente por Xedium (y transformado en un formato JSON mío mediante unos scripts que puedes ver aquí) y la textura de la luna es cortesía de la página JPL de la NASA.

Maldita sea, ésto empieza a sonar como un discurso de aceptación de los Óscars…

Etiquetas: , , , , , ,

¿Tienes algo que decir?

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