WebGL Tutorial 1 – Un triángulo y un cuadrado

Escrito por el 24 octubre, 2011

Bienvenidos al primer tutorial de webGL. Esta lección es una traducción libre al castellano de Learning WebGL, la cuál está basada en el segundo capítulo del tutorial NeHe OpenGL, una popular forma de aprender a usar gráficos 3D para el desarrollo de juegos. La lección te enseñará cómo pintar un triángulo y un cuadrado en una página web. Tal vez el tema no suena demasiado excitante, pero es una interesante introducción para conocer los fundamentos de WebGL; si entiendes cómo funciona este ejemplo, el resto te resultará bastante simple.

Éste es el resultado de lo que se verá 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.

Una advertencia rápida: 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. Estoy escribiendo las lecciones al mismo tiempo que aprendo webGL por mí mismo, así que probablemente cometa errores; usa el código de las lecciones bajo tu propio riesgo. Sin embargo, estoy arreglando errores y corrigiendo conceptos erróneos cuando los veo, pero si veis algún detalle mejorable, no dudeis en comentarlo.

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. Es bastante desalentador a primera vista, incluso teniendo nociones básicas de openGL. Desde el principio estamos usando un par de shaders, en castellano, sombreados, que es una tecnología relativamente avanzada… pero no te desesperes, en realidad es mucho más simple de lo que parece.

Si quieres un poco más de información sobre shaders, léete ésto.

Miremos el siguiente código HTML:

<body onload="webGLStart();">
  <canvas id="lesson01-canvas" style="border: none;" width="500" height="500"></canvas>
</body>

Ésto es el BODY al completo de la página. Todo lo demás está en JavaScript. Obviamente nosotros podríamos poner más código HTML dentro del BODY si queremos construir una página web decente, y luego colocar la etiqueta CANVAS allí donde deseemos. Pero para el ejemplo, nos basta con sólo el elemento CANVAS. Éste elemento, que en castellano significa lienzo, es una de las novedades de HTML5 y será la zona de la página web donde se pinten los gráficos a través de los nuevos métodos de javascript para el dibujo gráfico en 2D y (a través de webGL) 3D. No especificamos nada más que los atributos básicos para el CANVAS, sin borde y un ancho y un alto, y dejamos que todo el código de configuración del webGL se cargue en la función webGLStart, que automáticamente se llamará cuando cargue la página.

Ahora busca la función webGLStart y obsérvala:

function webGLStart() {
    var canvas = document.getElementById("lesson01-canvas");
    initGL(canvas);
    initShaders();
    initBuffers();

    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.enable(gl.DEPTH_TEST);

    drawScene();
}

Ésta llama a otra función para inicializar webGL y los shaders que mencioné antes, pasándole un objeto que apunta al elemento canvas, y luego inicializa algunos buffers usando initBuffers. Los buffers son unas memorias donde se mantendrán los detalles de los objetos (el triángulo y el cuadrado en este ejemplo) que van a ser dibujados. Hablaremos más de ellos dentro de un rato. A continuación, hacemos una configuración básica de webGL, diciendo que cuando ordenemos limpiar el canvas, lo haga de negro, y que cada vez que se pinte la escena se hagan comprobaciones de profundidad (los objetos que se pinten delante ocultarán a los que haya detrás). Estos pasos se implementan haciendo llamadas a los métodos de un objeto gl (el cual veremos cono inicializar después). Finalmente, se llama a la función drawScene. Ésta, como se puede esperar por el nombre, dibuja el triángulo y el cuadrado en el canvas usando los buffers.

Volveremos a initGL e initShaders más tarde, ya que son importantes para entender cómo funciona la página, pero primero veámos qué hacen initBuffers y drawScene.

Empezaremos con initBuffers, paso a paso:

var triangleVertexPositionBuffer;
var squareVertexPositionBuffer;

Declaramos dos variables globales donde mantener los buffers. En una página del mundo real nunca se utilizará una variable para cada objeto de la escena, porque sería impensable manejar individualmente cada buffer de objeto cuando se tienen decenas o cientos; pero en el ejemplo lo haremos así para que sea más fácil entender cómo se usan.

A continuación:

function initBuffers() {
    triangleVertexPositionBuffer = gl.createBuffer();

Creamos un buffer donde almacenar las posiciones de los vértices del triángulo. Los vértices son puntos en un espacio (3D, en este caso) que definen la forma del objeto a dibujar. Para nuestro triángulo nos bastará con 3 vértices, que configuraremos enseguida. Este buffer se guardará en la memoria de la tarjeta gráfica. Al guardar los vértices en la memoria de la tarjeta, cuando queramos dibujar la escena, esencialmente sólo necesitamos decir a webGL “oye, dibuja todo lo que te tienes en la memoria”. Así conseguimos hacer nuestro código muy eficiente, especialmente cuando queramos hacer animaciones con los objetos de la escena, al necesitar pintar cada objeto decenas de veces en un segundo para dar una buena sensación de movimiento. Por supuesto, cuando sólo son 3 posiciones de vértices como en este caso, no tendría mucho costo de rendimiento sacar esta información de la tarjeta gráfica, pero cuando estés tratando con modelos enormes con decenas de miles de vértices, será una enorme ventaja hacer las cosas de esta forma. Siguiente:

    gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);

Esta línea le dice a webGL que cualquier operación posterior que actúe sobre buffers deberá de aplicarse sobre el buffer que le acabamos de indicar. Este concepto de tener un apuntador implícito al “buffer actual” sobre el que ejecutar los métodos en vez de decirle a la función directamente en los parámetros el buffer con el que debe trabajar, es un poco extraño, pero estoy seguro de que se hace así porque se obtiene un mejor rendimiento.

    var vertices = [
        0.0, 1.0, 0.0,
       -1.0, -1.0, 0.0,
        1.0, -1.0, 0.0
    ];

Por fin definimos las posiciones de los vértices como en una lista de javascript. Espero que te hayas dado cuenta de que estos vértices corresponden a un triángulo isósceles con el centro en (0,0,0).

    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

Ahora creamos un objeto del tipo Float32Array basado en nuestra lista de vértices en javascript, y le decimos a webGL que la use para rellenar el buffer actual, que por supuesto será nuestro triangleVertexPositionBuffer, como dijimos antes. Hablaremos más del tipo Float32Array en futuras lecciones, pero por ahora todo lo que necesitas saber es que es una forma de convertir una lista (tabla, matriz, array, arreglo) de javascript en una estructura de datos compatible con webGL para que llene los buffers.

    triangleVertexPositionBuffer.itemSize = 3;
    triangleVertexPositionBuffer.numItems = 3;

La última cosa que haremos con buffers es establecer dos de sus atributos. No se trata de algo que se configura con webGL, pero será útil más adelante. Una cosa buena (otros dirían que mala) de javascript es que permite que establezcamos atributos nuevos en cualquier instancia de objeto, aunque ese atributo no esté definido para ese objeto. Así pues, aunque el objeto buffer no tenía previamente definidos los atributos itemSize y numItems, ahora sí los tiene. Los usaremos para decir que los 9 elementos (números) del buffer actualmente representan tres vértices (numItems), y cada uno de ellos está formado por 3 números (itemSize).

Ahora que hemos configurado completamente el triángulo, hacemos lo mismo con el cuadrado:

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

Todo esto debería serte bastante obvio (el cuadrado tiene 4 vértices en lugar de 3, por lo tanto la lista es mayor y el numItems diferente.

Ok, éste era el modo de meter los vértices de los dos objetos en la tarjeta gráfica, ahora veámos cómo en drawScene se utilizan los buffers para pintar la imagen en blanco y negro que tenemos arriba. Vayamos por partes:

function drawScene() {
    gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);

El primer paso es decirle a webGL en qué parte del CANVAS puede pintar. Como queremos utilizar todo su tamaño, indicamos con la función viewport (en castellano, vista) que puede utilizar para pintar la zona que va desde la coordenada (0,0), que corresponde a la esquina superior izquierda, tantos píxeles de anchura y altura como tenga el canvas. Explicaremos esta importantísima función en otra lección, pero de momento sólo nos interesa saber que con esta función podemos hacer que webGL sólo dibuje en la zona exacta del canvas que queramos, que en la mayoría de las veces, será todo en toda la superficie del canvas. A continuación, limpiamos el trozo de canvas que acabamos de definir, y lo preparamos para dibujar en él:

    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

… Y luego:

    mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pMatrix);

Aquí estamos montando la perspectiva con la que queremos ver la escena. Por defecto, webGL dibuja las cosas que están cerca del mismo tamaño que las que están lejos (este estilo 3D de perspectiva se llama proyección ortográfica, basada en la proyección ortogonal). Con el fin de hacer que las cosas que estén más lejos se vean más pequeñas que las que tenemos cerca, tenemos que aprender un poco sobre perspectivas. De momento diré que para esta escena, estamos diciendo que tenemos un campo de visión (vertical) con un ángulo de 45º, la relación que hay entre la anchura y la altura del lienzo, que no queremos ver las cosas que hay a menos de 0,1 unidades del punto de vista, ni tampoco las que estén a más de 100.

Como puede verse, esta forma de definir la perspectiva usa  una función que pertenece a un módulo llamado mat4, e implica la necesidad de usar una intrigante variable llamada pMatrix. Hablaremos de ello en otra ocasión, pero espero que de momento esté claro como usar esta función sin necesidad de conocer todos los detalles.

Ahora que hemos configurado la perspectiva (también conocida como punto de vista), podemos pasar a dibujar algunas cosas:

    mat4.identity(mvMatrix);

El primer paso es “movernos” al centro de la escena 3D. En openGL, cuando estás dibujando una escena, necesitas indicar para cada cosa que se quiere dibujar una posición actual con una rotación actual. Entonces webGL tomará esa posición actual como si fuera la coordenada (0,0,0) relativa para ejecutar órdenes posteriores. Por ejemplo, dirías “muévete 20 unidades hacia delante, rota 32 grados, y a continuación dibuja el robot”, siendo “dibujar el robot” otra serie de complejos movimientos tipo “muévete mucho, gira un poco, dibuja este trozito de robot”. Y cada trozo de robot tendría sus vértices un eje de coordenadas donde (0,0,0) es el centro del trozo. Esto es útil hacerlo así porque puedes encapsular todo el código de “dibujar el robot” en una sola función, y luego mover fácilmente el robot por el escenario con sólo cambiar la “posición actual” a una coordenada con una posición y rotación determinados antes de llamar a la función “dibujar robot”. Así evitamos tener que recalcular las posiciones de todos los vértices de dicho robot en el eje de coordenadas absoluto de la escena.

La posición actual y la rotación actual se almacenan ambas en una matriz, y como probablemente hayas aprendido en la escuela, si prestaste algo de atención, las matrices pueden representar translaciones (mover puntos de un lugar a otro), rotaciones y otras transformaciones geométricas. Por razones que no voy a explicar en este momento, tú puedes usar una sola matriz de 4×4 (no de 3×3) para representar cualquier número de transformaciones en un espacio 3D; se comienza con la matriz de identidad, esto es, la matriz que representa una transformación que no hace nada, luego se multiplica por la matriz que representa la primera transformación, a continuación se multiplica por la que representa su segunda transformación, y así sucesívamente. El resultado final es una matriz combinada que representa todas las transformaciones. La matriz que usaremos para contener la posición/rotación actual se llama matriz modelo-vista (model-view matrix), y como quizás hayas adivinado, se corresponde con la variable mvMatriz utilizada arriba, y la función mat4.identity que acabamos de llamar la prepara para que podamos empezar a realizar multiplicaciones de translación y rotación sobre ella. O, en otras palabras, nos traslada a un punto de origen desde el cual nos podemos empezar a mover para dibujar el mundo 3D.

Los lectores más perspicaces se habrán dado cuenta de que en el inicio de este bloque de texto dije “en openGL”, y no “en webGL”. El motivo es que webGL no tiene estas capacidades programadas dentro de la librería de gráficos. En su lugar, utilizaremos una biblioteca hecha por terceros, glMatrix, hecha por Brandon Jones, que junto a algunos ingeniosos trucos conseguiremos el mismo efecto en webGL.

Bien, ahora sigamos con el código que dibuja el triángulo en el lado izquierdo de nuestro lienzo.

    mat4.translate(mvMatrix, [-1.5, 0.0, -7.0]);

Habiéndonos movido al centro de nuestro espacio 3D com mvMatrix establecida con la matriz de identidad, se inicia el triángulo moviéndonos 1.5 unidades a la izquierda (desplazándolos hacia la izquierda de donde estábamos, x negativo), y alejándonos 7 unidades de la escena (es decir, más lejos del espectador, z negativo). mat4.translate, como habrás adivinado, hace la multiplicación de translación a la matriz de modelo-vista con los parámetros que le hemos pasado.

Y por fin, en el siguiente paso realmente dibujamos algo:

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

Así, como recordarás de cuando hablamos sobre los buffers, tenemos que llamar a gl.bindbuffer para indicarle a webGL el “buffer actual”, y luego llamamos al código que opera sobre él. Aquí estamos seleccionando el buffer que contiene los vértices del triángulo, triangleVertexPositionBuffer, y a continuación diciéndole a webGL los valores que se deben de utilizar para las posiciones de los vértices. Explicaré cómo funciona este método después. De momento nótese que estamos haciendo uso del itemSize que definimos antes.

A continuación:

    setMatrixUniforms();

Ésto le dice a webGL que almacene en la memoria de la tarjeta gráfica nuestra matriz modelo-vista actual (y también de la matriz de proyección, de la que hablaremos más adelante). Ésto es necesario hacerlo adrede ya que las transformaciones de la matrices no vienen programadas en webGL. Podemos verlo de esta manera: La matriz con el resultado de las traslaciones que hemos aplicado hasta el momento están en el ámbito privado de javascript.  setMatrixUniform, una función que hemos creado nosotros, copiará esas matrices en la tarjeta gráfica.

Una vez hecho esto, webGL tendrá en memoria una lista de números que representan las posiciones de los vértices en la posición actual del triángulo. El siguiente paso es dibujarlo:

    gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);

O, escrito de otra forma, “dibuja los vértices que te dí antes como triángulos, empezando con el elemento 0 del array y siguiendo con los siguientes numItems elementos.

Con ésto, se debe haber dibujado el triángulo. A continuación, hacemos lo mismo con el cuadrado:

    mat4.translate(mvMatrix, [3.0, 0.0, 0.0]);

Empezamos moviendo la matriz de modelo-vista tres unidades hacia la derecha. Recuerda que actualmente estábamos a 1.5 unidades hacia la izquierda y 7 hacia dentro de la escena, por lo que esta orden nos deja a 1.5 unidades a la derecha (eje x positivo) del centro absoluto de la escena (0,0,0) y a 7 unidades de distancia (eje z negativo).

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

Igual que antes, le indicamos a webGL que el buffer a usar es el de las posiciones de los vértices del cuadrado.

    setMatrixUniforms();

Actualizamos la matriz modelo-vista de la memoria de la gráfica otra vez (para que tenga en cuenta el último mvTranslate), y finalmente:

    gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems);
}

Dibujamos los vértices. Quizás te habrás preguntado ¿Qué diablos es un TRIANGLE_STRIP? Bien, es una forma de dibujar polígonos que consiste en pintar triángulos sucesivos. Funciona así: Si tenemos 4 vértices, A,B, C y D, el primer triángulo que pintará es el que forman los puntos A, B y C. El segundo, los que forman B, C y D. Y así sucesivamente, si hubiera más puntos. Como antes, le estamos diciendo que empieze por el vértice con índice 0 en la lista (el primero), y que continúe con los  numItems elementos posteriores (incluído el de índice 0). En resumen, una rápida y sucia manera de crear un cuadrado. Sin embargo, con objetos más complicados, este método de aproximación por triángulos se convertirá en una útil forma de definir complejas superficies. Y una vez hecho ésto, hemos acabado con nuestra función de dibujar la escena.

Si has sido capaz de comprender la lección y llegar hasta aquí, definitivamente estás listo para empezar a experimentar con webGL. Cópiate nuestro ejemplo, demo1.html y la librería glMatrix.js. Ejecútalo en local, no es necesario tener ningún servidor web. Prueba a cambiar las posiciones de los vértices, y observa lo que pasa. En concreto, nuestra escena es demasiado plana. Cambia todos los valores Z del cuadrado a 2, o a -3, y mira cómo se acerca o se aleja la figura. O cambia sólo agunos de ellos, y mira si se deforma con la perspectiva. Juega, vuelvete loco, y no te preocupes por mí, te esperaré.

Bien, ahora que has regresado, echaremos un vistazo a las funciones de apoyo que facilitaron un poco la vida a la hora de usar webGL. Como dije antes, probablemente seas muy feliz ignorando ciertos detalles, y sólamente copiando y pegando en tu página las funciones de apoyo que vienen por encima de initBuffers, al principio del código javascript. Sin embargo no son muy difíciles de entender, y hacerlo significa poder escribir mejor código webGL en el futuro.

¿Sigues ahí? Gracias :-) Empezemos con la más aburrida de las funciones, la de la primera llamada que hace webGLStart, initGL. Es la función que está más arriba del todo en el código javascript:

var gl;
function initGL(canvas) {
    try {
        gl = canvas.getContext("experimental-webgl");
        gl.viewportWidth = canvas.width;
        gl.viewportHeight = canvas.height;
        } catch(e) {
    }
    if (!gl) {
        alert("No puede iniciarse webGL en este navegador");
    }
}

Es muy simple. Como ya hemos visto, las funciones initBuffers y drawScene usaban constántemente un objeto llamado gl, que claramente suponíamos que apuntaba a alguna cosa del nucleo de webGL. Esta función obtiene “esa cosa”, que llamaremos contexto webGL, que se obtiene pidiéndole al CANVAS un determinaado tipo de contexto. Y sí, como espero que hayas supuesto, el nombre “experimental-webgl” cambiará a “webgl” cuando se cierren los estándares de html5 y javascript. Una vez obtenido el contexto, usamos algunos métodos de javascript para obtener el ancho y el alto del lienzo, y haciendo uso del débil tipado de javascript, que como dijimos despierta tantas pasiones como odios, creamos dos nuevos atributos a gl para guardar esa valiosa información. Luego la usaremos para definir el tamaño de la zona donde pintar en el canvas, en viewport al comienzo de la función drawScene. Con ésto, ya tenemos configurado nuestro contexto webGL.

Después de llamar a initGL, webGLStart llama a initShaders. Ésta, por supuesto, inicializa los shaders (quién lo iba a decir). Pero dejemos todavía el misterio en el aire, porque antes tenemos que mirar a nuestra matriz de modelo-vista y a la matriz de proyección que mencionamos no hace mucho.

var mvMatrix = mat4.create();
var pMatrix = mat4.create();

Definimos una variable llamada mvMatrix, que contendrá la matriz modelo-vista, y otra variable pMatrix, que será nuestra matriz de proyección. Inicialmente están llenas de ceros. Hablemos un poco de la matriz de proyección. Como espero que te acuerdes, se aplicó la función mat4.perspective a esta variable para establecer nuestro punto de vista, al principio de drawScene. Esto se debió a que webGL no admite directamente la gestión de la perspectiva, al igual que tampoco soportaba directamente la matriz modelo-vista. Y si necesitábamos una matriz donde guardar los resultados de las translaciones y rotaciones, también necesitamos otra matriz que maneje el proceso de hacer más grandes o pequeñas los objetos del espacio según la perspectiva. Y como habrás adivinado, eso es exactamente lo que hace la matriz de proyección. La función mat4.perspective rellena la matriz adecuadamente según la relación de aspecto y campo de visión indicados en los parámetros que le hemos puesto a la perspectiva.

De acuerdo, hasta el momento lo hemos visto todo excepto la función setMatrixUniforms, que, como dije en su momento, mueve las matrices de modelo-vista y proyección de javascript a webGL, y las terroríficas funciones de uso de shaders. Ambas están relacionadas, asi que empezaremos por explicar algunas nociones básicas.

¿Qué es un shader? Bueno, en algún momento de la historia de la informática gráfica en 3D, alguien necesitó de funciones que calculasen el color y las sombras de los objetos de la escena, antes de empezar a dibujarlos. Con el tiempo, los shaders han ido creciendo en utilidades, y actualmente podríamos decir que son una porción de código que puede modificar todos y cada uno de los bits finales de una escena, antes de dibujarla. Y ésto es realmente útil, porque además de que se ejecuta en la tarjeta gráfica (por lo que funciona muy rápido), el tipo de transformaciones que puede realizar son verdaderamente útiles, incluso en un ejemplo tan pequeño e insignificante como el que tenemos entre manos.

La razón por la que vamos a utilizar shaders en lo que pretende ser un sencillo ejemplo de introducción a webGL, es que se encargarán de que el sistema webGL aplique nuestras matrices modelo-vista y de proyección a nuestra escena con la rapidísima GPU y la memoria de la tarjeta gráfica, evitando que sea la CPU la que tenga que estar calculando con un (en comparación) lento javascript las posiciones finales de cada uno de los vértices de los objetos de la escena. Ésto es increíblemente útil.

Por lo tanto, aquí tenemos la forma de utilizarlo, paso a paso. Recordemos de que la llamada a la función initShaders está dentro de webGLStart:

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("No pueden iniciarse los shaders");
    }

    gl.useProgram(shaderProgram);

Como puede verse, se usa una función llamada getShader para obtener dos objetos, algo lamado “fragmento shader”, fragmentShader, y otro llamado “vértice shader”, vertexShader, y luego los une para obtener algo llamado “programa shader”, shaderProgram. Un “programa” es un trozo de código que reside en el lado webGL del sistema; puedes verlo como una forma de especificar que algo debe funcionar en la tarjeta gráfica. Cada programa debe enlazar con un vértice shader y un fragmento shader. Veremos más de ellos en un momento.

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

Una vez que hemos configurado el programa shader, y unido a él los shaders, ahora necesita que le asociemos una referencia a uno atributo, llamado vertexPositionAttribute, aprovechándonos, como ya lo hemos dicho dos veces, de que javascript permite inventarnos nuevos atributos en cualquier momento. Lo hacemos así porque es conveniente tener esos dos valores en el mismo objeto (programa shader) para usarlos más fácilmente.

Pero… ¿Qué es un vertexPositionAttribute? A lo mejor no recuerdas de que lo usamos en la función drawScene (éste es un buen momento para buscarla y repasarla). Se utilizaba en el código que establecía los vértices del triángulo al buffer adecuado. Hablaremos un poco más en un momento, por ahora basta con notar que también usamos gl.enableVertexAttribArray para decirle a webGL que queremos proporcionarle valores al atributo usando una lista.

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

Lo último que hacemos en initShaders es recuperar dos valores más del programa shader: Las localizaciones de dos cosas llamadas matrices uniformes. De momento sólo hay que saber que las guardamos en el objeto programa por comodidad, aprovechándonos de que en javascript se pueden crear variables… Lo de siempre.

Ahora veamos la función getShader al completo:

function getShader(gl, id) {
    var shaderScript = document.getElementById(id);
    if (!shaderScript) {
        return null;
    }

    var str = "";
    var k = shaderScript.firstChild;
    while (k) {
        if (k.nodeType == 3)
            str += k.textContent;
        k = k.nextSibling;
    }

    var shader;
    if (shaderScript.type == "x-shader/x-fragment") {
        shader = gl.createShader(gl.FRAGMENT_SHADER);
    } else if (shaderScript.type == "x-shader/x-vertex") {
        shader = gl.createShader(gl.VERTEX_SHADER);
    } else {
        return null;
    }

    gl.shaderSource(shader, str);
    gl.compileShader(shader);

    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
        alert(gl.getShaderInfoLog(shader));
        return null;
    }

    return shader;
}

Ésta es otra de esas funciones que parece más complicada de lo que en realidad es. Lo único que hacemos aquí es buscar en el árbol DOM de nuestra página HTML algún elemento que tenga la ID especificada como parámetro de la función, extraer todo su contenido (que será la creación de un fragmento o el sombreado de los vértices, hablaremos de ello en otra lección), y pasarle el código a webGL para que lo compile y pueda ejecutarse en la tarjeta gráfica. Después, el código controla cualquier posible y error, y acaba. Como alternativa, podríamos crear el código como un string (cadena de caracteres), en vez de utilizar un elemento HTML y perder el tiempo buscándolo y tomando su contenido, pero de esta forma se hace mucho más fácil de leer y modificar, ya que aparentan ser trozos de código javascript, pero ojo, no lo son.

Aquí los tenemos:

<script id="shader-fs" type="“x-shader/x-fragment">
    // <![CDATA[
   #ifdef GL_ES
   precision highp float;
   #endif
   void main(void) {
       gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
   }
   // ]]>
</script>

<script id="shader-vs" type="x-shader/x-vertex">
    // <![CDATA[
   attribute vec3 aVertexPosition;
   uniform mat4 uMVMatrix;
   uniform mat4 uPMatrix;
   void main(void) {
       gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
   }
   // ]]>
</script>

Lo primero que hay que destacar es que NO están escritos en lenguaje javascript, a pesar de que pueda resultarnos familiar. En realidad, están escritos en un lenguaje llamado GLSL, claramente inspirado en C. El primero de ellos, que corresponde al  fragment shader, comunica a la tarjeta gráfica de que use el tipo float como grado de precisión para todas las operaciones, e indica que todo los píxeles que se dibujen, lo hagan en color blanco. Cómo pintar con varios colores será motivo para una nueva lección.

El segundo shader es un poquito más interesante. Se trata de un vertex shader (codigo que se ejecuta en la tarjetra gráfica que puede hacer cualquier cosa con los vértices). Asociados a ella, tiene dos variables uniformes, uMVMatrix y uPMatrix. Las variables uniformes son útiles porque se pueden acceder a ellas desde fuera del shader, y como quizás hayas recordado, se extrajo su ubicación al final de initShaders, y será con ellas la forma de pasarle a webGL los valores de nuestras matrices de modelo-vista y proyección que calculamos en javascript.

El vertex shader es llamado una vez por cada vértice, y el vértice se pasa al código del shader a través de aVertexPosition, gracias al uso del vertexPositionAttribute en el drawScene. ¿Comienzas a ver la relación? Los shaders deben tener una rutina principal llamada main, y ésta en concreto sólo multiplica la posición del vértice por la matriz de modelo-vista y por la de proyección devolviéndolos la posición final del vértice en la escena.

Por lo tanto, webGLStart llama a initShaders, que usará getShader para cargar los shaders de fragment y vertex contenidos en dos <script> (con ID propio) de la página web, de modo que puedan ser compilados por la tarjeta gráfica y utilizados más adelante cuando se renderice la escena 3D.

Y finalmente, lo último que nos queda por comentar es setMatrixUniforms, el cual es muy fácil de entender si mas o menos se ha comprendido la lección hasta este punto.

function setMatrixUniforms() {
    gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false, pMatrix);
    gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false, mvMatrix);
}

Simplemente actualizamos las matrices uniformes de modelo-vista y proyección que están en la gráfica con las matrices de modelo-vista y proyección que tenemos en la memoria de javascript.

Uff. De momento ésto es todo lo que tenía que decir en la primera lección. Siento el rollazo que he soltado con algunas explicaciones, pero era necesario para empezar con lo realmente interesante de webGL: Dar colorido a los objetos, hacer que las cosas se muevan, y usar modelos realmente tridimensionales. Todo ello y mucho más, en futuras lecciones, ¡hasta la próxima!.

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.

Obviamente, estoy profundamente en deuda con NeHe por sus tutoriales de openGL, que seguí como guión para esta lección, pero también me gustaría agradecer a Benjamín DeLillo y a Vladimir Vukicevic por sus códigos de ejemplo webGL, que he investigado, analizado, y probablemente totalmente incomprendido y finalmente destrozados en el código que usamos para esta lección. Gracias también a Brandon Jones por glMatrix. Finalmente, también me gustaría recordar a James Coglan, que escribió sylsvester.js, una librería de matrices de propósito general, que utilicé en las primeras versiones de esta lección, antes de pasarme a glMatrix, mucho más centrada en webGL. Sin embargo, esta versión actual no habría existido si aquélla primera lbrería.

Etiquetas: , ,

Enlaces entrantes

Comentarios (18)

  • Hace un tiempo estuve siguiendo los tutoriales de learningwebgl.com y developer.mozilla.org/es/WebGL, ya que estoy dispuesto a aprender sobre programación gráfica con javascript, y hace una semana descubrí este lugar.
    Escribo mas que nada para dar las gracias, porque me está quedando todo muchísimo mas claro.
    Es un honor seguir los pasos para dominar la programación de gráficos tridimensionales en la web, ya que como tu también supondrás, esto va a ser muy importante conocerlo de cara a un futuro muy cercano en cuanto al desarrollo de videojuegos y entornos virtuales en general, sin importar el sistema operativo…

    Esperaré de forma paciente la próxima lección.

    Un saludo y gracias de nuevo

  • Sí, por eso me he propuesto, como digo en los tutoriales, a aprender webGL al mismo tiempo que traduzco al tipo que aprendió webGL escribiendo tutoriales; una situación abstante graciosa.
    El único puntito negro que le veo al futuro de webGL es que Microsoft dijo que no implementaría webGL en su navegador web porque dejar que las páginas tengan acceso directo al hardware, en este caso, a la tarjeta gráfica, presentará problemas de seguridad, pero las malas lenguas dicen que en realidad a lo que le tienen miedo es a la competencia que openGL le presenta a su DirectX y a su tecnología web SilverLight. Pero vamos, a mí me la trae al pairo porque hace lustros que no uso el explorer.
    Además siempre quise hacer cosas con openGL, y compartirlas de alguna forma con los demás. Con webGL mato dos pájaros de un tiro, aunque pagamos un precio: El código fuente está abierto a todo aquél que quiera verlo. Se puede ofuscar, sí, pero también hay herramientas para realizar el proceso inverso.

    Gracias por tus palabras, me animan a seguir traduciendo los tutoriales añadiéndole detalles de cosecha propia. Cuando hagas tus primeros pinitos con openGL no dudes en dejar comentarios con los enlaces con tus trabajos. Cuando sea rico y famoso gracias a esta web quizás incluso incluya una sección para enlazar con demos hechos por aprendices.
    Hasta entonces, disfruta con el resto de tutoriales. Un saludo.

  • Te agradezco por haber traducido estas lecciones. Aunque uno pueda entender inglés, es mejor para uno tenerlo en la lengua madre para concentrar los pensamientos al aprendizaje de un nuevo lenguaje de programación que al de traducción.

    Gracias. Abrazo.
    Cristian.

  • Muchas gracias, justo lo que buscaba, a hora podre aprender webgl si usar el traductor de google.

    saludos

  • Hoy empeze con webGL, empeze a buscar informacion al respecto. Por ello mismo te agradesco por el tiempo que te tomaste en hacer este trabajo. Muchas gracias y seguire leendo los siguientes capitulos que me resultan interesantes XD.

  • Oye tio..muchas gracias por el aporte.
    Pero ya empezamos con problemas..no se me ven las figuras (triangulo y rectangulo)..porque puede ser ??

    gracias

  • Buenas, realize el tutorial paso por paso, y me tiraba un error diciendo “no pueden inicializarse los shared”, esto se produce por que el codigo de definicion de los shared-fs y vs, dicen type=”text/javascript” y comparando con el ejemplo que subiste, en estos salen “x-shader/x-fragment” y “x-shader/x-vertex” respectivamente…
    al arreglar esto funciona impecable.
    Otra acotacion, el lenguaje es GLSL y no GSLS
    saludos

  • Gracias por el aviso, Matías. Corregido.

    Merc, ¿Estás usando la última versión de Mozilla Firefox o Google Chrome?

  • hola!
    Este tutorial es fantástico. A mi me pasa algo parecido a Merc, con el mismo navegador (mozilla), puedo visualizar perfectamente tus ejemplos e interactuar con las figuras y todo, pero si voy y copio el código fuente a mi disco duro, y luego des del navegador lo ejecuto, entonces no se ven las figuras, y el código es el mismo… que misterio.

  • bueno, solucionado, en lugar de hacer cut&paste hice guardar como…

  • Después de trabajar un tiempo con 2d, me topé con unos problemas en un proyecto, que seguro que WebGL los puede solucionar, mañana mismo me pongo a tope con ello que ahora con el verano hay tiempo libre, muchísimas gracias por los tutoriales. =D

  • hola excelte tutorial pero tengo algunas dudas la primera es puedo interactuar con php, la razon es que desearia realizar graficos dinamicos me refiero si puedo hacer una torta 3d dinamica y si es asi el navegador tardaria en cargar esta pagina ? y esta tecnologia es libiana ?, yo manejo visual studio 2010 y desearia saber si existe compatibilidad, bueno estoy aprendiendo esto de 3d manejo php y vs2010 y quiciera saber la compatibilidad de estos gracias y sigue adelante es un gran tutorial espero tu respuesta pronto bye..
    saludos cordiales
    Omar Rojas

  • perdon me olvide tengo otra pregunta en este tutor dice que solo tener los navegadores chrome o fire fox basta, bueno lo pregunto por que lo hice en mi casa y no tengo internet al realizar la leccion no me sale nada incluso segui la correccion que dice martin creo pero nada no me muestra nada, por eso es mi pregunta no necesito una libreria com jquery o algo por estilo gracias deberas

  • Me da error, me dice que mat4 no esta definido

  • Eso es porque hay que descargase también el glMatrix-0.9.5.min.js ( http://code.google.com/p/glmatrix/downloads/detail?name=glMatrix-0.9.5.min.js ) y guardarlo en un carpeta llamada “js” en la misma altura del directorio donde se encuentra el html.

    O modificar el código del tag script con el src que le quieras colocarle

    si lees el código html te das cuenta :

  • Cielos, se ve desalentador XD
    Esperemos que implementen algún formato de archivo 3d para la web (si aún no lo han hecho), aunque claro, esto es demasiado pedir en el mundo de los navegadores

  • Hola he leído hasta la mitad pero al principio estaba claro pero ahora no lo entiendo muy bien lo explicas muy poco . Puedes explicar me lo algo con mas detalles ya que quiero mucho aprender webgl con canvas pero no hay tutoriales en español, tu eres el único que lo explica mas o menos bien :)

    ???

    Saludos

  • Hola, gracias por estos estupendos tutoriales!

    Yo quería decir…

    Que Opera, que es el que mas uso, también soporta WebGL, aunque hay que habilitar esa propiedad en las opciones del mismo, por que la trae deshabilitada por defecto, de hecho y curiosamente Opera es el que consigue mayor numero de FPS en comparación con otros como Firefox, Chrome o Safari.

¿Tienes algo que decir?

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