Conceptos básicos

Sistema de coordenadas 3D

El espacio tridimensional se ubica en un sistema de coordenadas 3D, en donde adicionalmente a los ejes X e Y del 2D, se incorpora el eje Z el cual describe la profundidad (cuan cerca o lejos de la pantalla es dibujado un objeto). El sistema de coordenadas para WebGL es descrito tal como aparece en la figura, con el eje X, en dirección horizontal y sentido de izquierda a derecha, el eje Y en dirección vertical y sentido de abajo hacia arriba y el eje Z en sentido positivo hacia fuera de la pantalla.

Sakurambo, via Wikimedia Commons

Mallas, polígonos y vértices

La forma más común de dibujar objetos 3D es mediante una malla (mesh). Una malla es un objeto compuesto por una o más figuras poligonales, construidas por vértices (coordenadas X, Y, Z) que definen posiciones en el espacio tridimensional. Los polígonos más usados en las mallas son los triángulos (grupos de tres vértices) y los cuadriláteros (quads, grupos de cuatro vértices). A las mallas 3D también se les llama modelos.

La figura muestra una malla 3D. Las coordenadas X, Y, Z de los vértices de la malla sólo describen la figura. Las propiedades de la superficie de la malla, tales como el color y la tonalidad son definidas usando atributos adicionales que describiremos más adelante.

SaphireS, Blender3D UVTexTut1, CC BY-SA 3.0

Materiales, texturas y luces

La superficie de una malla o modelo se define usando atributos que van más allá de las coordenadas de los vértices X, Y y Z. Los atributos de superficie pueden ser tan simples como un color o tan complejos como definir de qué manera las luces se reflejan en el objeto o cuan brillantes se verán. La información de superficie puede también ser representada usando uno o más mapas de bits, conocidos como mapas de texturas o simplemente texturas (textures). Las texturas pueden definir tal cual cómo se verá una superficie (como en una imagen impresa en una polera), o pueden ser combinadas con otras texturas para alcanzar efectos más sofisticados. En la mayoría de los sistemas gráficos, las propiedades de superficie de una malla son llamadas materiales (materials). Los materiales típicamente dependen de la presencia de una o más luces (lights), las cuales definen cómo se ilumina una escena.

La figura anterior tiene un material de color púrpura y un sombreado definido por una fuente de luz que emana desde la izquierda del modelo (notar las sombras en el lado derecho de la cara).

Transformadas y matrices

Las mallas 3D son definidas por las posiciones de sus vértices. La mayoría de los sistemas 3D soporta transformadas (transforms), que son operaciones que mueven la malla en una cantidad relativa, sin tener que intervenir cada vértice que la contiene. Las transformadas le permiten a un modelo renderizado ser escalado, rotado y trasladado (movido), sin cambiar ningún valor de sus vértices.

Una transformada es típicamente representada a través de una matriz (matrix), que corresponde a un objeto matemático que contiene un arreglo de valores usados para calcular las posiciones transformadas de los vértices.

Cámaras, perspectivas, puntos de vista y proyecciones

Cada escena renderizada requiere de un punto desde el cual el usuario pueda contemplarla. Los sistemas 3D típicamente usan una cámara (camera), que es un objeto que define donde (en relación a la escena) el usuario es posicionado y orientado, así como otras propiedades de las cámaras "reales" tales como el tamaño del campo de visión, lo que define la perspectiva (perspective) (objetos lejanos se ven más pequeños). Las propiedades de la cámara se combinan para generar la imagen renderizada final de una escena 3D dentro de un punto de vista (viewport) definido por la ventana o canvas.

Las cámaras son representadas por un par de matrices. La primera matriz define la posición y orientación de la cámara, al estilo de la matriz usada para las transformadas. La segunda es una matriz que representa la traslación desde las coordenadas 3D de la cámara hacia el espacio 2D del punto de vista (viewport). A esto se le llama la matriz de proyección (projection matrix).

Mayor información sobre los conceptos camera, viewport y projection, se pueden encontrar en el siguiente link.
https://obviam.github.io/archive/3d-programming-with-android-projections-perspective/

Sombreadores (Shaders)

Para renderizar la imagen de una malla o modelo el desarrollador debe definir exactamente cómo van a interactuar los vértices, transformadas, materiales, luces y cámaras con el objeto de crear esa imagen. Esto se realiza usando sombreadores (shaders). Un sombreador, en adelante shader, también conocido como programmable shader es un trozo de código que implementa algoritmos que llevan los pixeles de una malla a la pantalla. Los shaders son definidos en un lenguaje de alto nivel tipo C y compilado en un código usable para la Unidad de Procesamiento Gráfico (GPU, Graphics Processor Unit).

Por definición WebGL requiere shaders, de lo contrario no se verá nada en pantalla. La implementación de WebGL asume la presencia de una GPU. La GPU procesa sin problemas vértices, texturas y un poco más; no maneja los conceptos de material, luces y transformadas. La traducción entre esos conceptos y lo que pone la GPU en pantalla es hecho por el shader y éste es creado por el desarrollador.

Los shaders le dan al programador gráfico el control total sobre todo vértice y pixel que es renderizado.

API WebGL

OpenGL es un estándar de programación gráfica que nació a fines de los '80. Ha sobrevivido a fuertes competencias, como la de Microsoft DirectX. Se han diseñado diversas versiones de OpenGL para las diferentes plataformas donde ha sido necesario implementarlo (computadores de escritorio, televisores, tablets, etc.). OpenGL ES (por "embedded systems") es la versión de OpenGL diseñada para correr en dispositivos pequeños tales como tablets y smartphones. Sin querer, OpenGL ES se transformó en el núcleo (core) ideal para WebGL. Es pequeño y liviano, lo que significa que no sólo puede implementarse una app WebGL directamente para un browser, sino que al implementarse y funcionar para uno, funcionará para todos.

Todas estas bondades de alto rendimiento y portabilidad tienen una contraparte. La naturaleza liviana de WebGL pone todo el peso en el desarrollador para definir sus modelos de objetos, escenas gráficas, listas y otras estructuras. Esto implica, para el desarrollador web promedio, una curva de aprendizaje elevada, llena de conceptos ajenos a él. Pero existen varias bibliotecas de código fuente abierto que facilitan esta tarea. Más adelante trabajaremos con una de ellas (Three.js). Pero antes de eso revisaremos grosso modo los fundamentos de WebGL.

Estructura de una aplicación WebGL

WebGL usa el elemento HTML5 <canvas> para obtener gráfica 3D en el browser.

Para renderizar WebGL en una web page, una aplicación debe como mínimo ejecutar los siguientes pasos:

  1. Crear un elemento canvas.
  2. Obtener un contexto (drawing context) para el canvas.
  3. Inicializar el viewport
  4. Crear uno o más buffers que contengan los datos a renderizar (típicamente vértices).
  5. Crear una o más matrices para definir las transformaciones desde los vértices a la pantalla.
  6. Crear uno o más shaders para implementar el algoritmo de dibujo.
  7. Inicializar los shaders con sus respectivos parámetros.
  8. Dibujar.

A continuación se describirán los pasos anteriores en detalle. El código a analizar es parte de un ejemplo de WebGL del libro "WebGL Up And Running" de Tony Parisi, que dibuja un cuadrado blanco en el canvas WebGL.
El ejemplo se puede ver aquí: example1-1.html
El código fuente aquí: https://github.com/tparisi/WebGLBook/blob/master/Chapter%201/example1-1.html

El canvas y el contexto (drawing context)

Todo lo que se renderiza en WebGL toma lugar en un contexto, que es un objeto JavaScript que provee la API WebGL. Esta estructura es parecida al tag HTML5 <canvas>.

Para tener WebGL en una webpage se debe:

  1. Crear un tag <canvas>
  2. Llamar el objeto JavaScript asociado, usando document.getElementByID()
  3. Llamar el contexto WebGL
El ejemplo 1.1 muestra cómo obtener el contexto WebGL desde un elemento canvas.

Ejemplo 1.1. Cómo obtener un contexto WebGL desde un canvas

        function initWebGL(canvas) {
        var gl;
        try 
        {
            gl = canvas.getContext("webgl");
        } 
        catch (e)
        {
            var msg = "Error creating WebGL Context!: " + e.toString();
            alert(msg);
            throw Error(msg);
        }
        return gl;        
     }
     

El Viewport

Una vez obtenido un contexto WebGL válido para el canvas, deben definirse los contornos rectangulares donde dibujar. En WebGL a esto se le llama viewport. Setear el viewport en WebGL es sencillo, sólo basta llamar al método viewport(), como se ve en el ejemplo 1.2.

Ejemplo 1.2. Setear el viewport

        function initViewport(gl, canvas)
        {
            gl.viewport(0, 0, canvas.width, canvas.height);
        }
    

Buffers, ArrayBuffer y Typed Arrays

Las figuras en WebGL se dibujan mediante primitivas, tipos de objetos para dibujar, tales como arreglos de triángulos, tiras (strips) de triángulos, puntos y líneas. Las primitivas usan arreglos de datos, llamados buffers, los cuales definen la posición de los vértices a ser dibujados. El Ejemplo 1.3 muestra cómo crear un buffer de vértices para dibujar un cuadrado de 1x1. Los resultados son llevados a un objeto JavaScript que contiene el buffer de vértices, el tamaño de éstos (en este caso tres números en punto flotante para almacenar x, y y z), el número de vértices a ser dibujado y el tipo de primitiva a ser usado para dibujar el cuadrado, en este caso una tira de triángulos (una tira de triángulos es una primitiva que define una secuencia de triángulos usando los tres primeros vértices para el primer triángulo, y cada vértice siguiente en combinación con los dos anteriores para los triángulos siguientes).

Ejemplo 1.3. Creación del buffer de datos para los vértices

    // Función que crea un cuadrado a partir de vértices
    function createSquare(gl) {
        var vertexBuffer;
    	vertexBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
        var verts = [
             .5,  .5,  0.0,
            -.5,  .5,  0.0,
             .5, -.5,  0.0,
            -.5, -.5,  0.0
        ];
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verts), gl.STATIC_DRAW);
        var square = {buffer:vertexBuffer, vertSize:3, nVerts:4, primtype:gl.TRIANGLE_STRIP};
        return square;
    }
    

El tipo Float32Array es un nuevo tipo de dato introducido en los web browsers para ser usado en WebGL. Float32Array es un tipo de ArrayBuffer, también conocido como typed array. Corresponde a un tipo JavaScript que almacena datos binarios compactos. Los typed arrays pueden ser accesados usando la misma sintaxis que los arreglos comunes, pero son más rápidos y consumen menos memoria. Son ideales para usar en datos binarios donde el rendimiento es crítico. La introducción de los typed arrays en los web browsers fue producto del trabajo de WebGL. La última especificación de typed array puede ser encontrada en el website de Kronos en:
https://www.khronos.org/registry/typedarray/specs/latest/

Matrices

Antes de dibujar el cuadrado se deben crear un par de matrices. Primero, se necesita una matriz para definir donde se ubicará el cuadrado en el espacio coordenado 3D, en relación a la cámara. Esta matriz es conocida como ModelView matrix, porque combina transformaciones del modelo (malla 3D) y la cámara. En el ejemplo, se transforma el cuadrado trasladándolo alrededor del eje z negativo (moviéndolo lejos de la cámara en -3.333 unidades).

A la segunda matriz se le llama projection matrix, la cual es requerida por el shader para convertir el espacio coordenado 3D del modelo en un espacio 2D de la cámara dibujado en el espacio del viewport. En el ejemplo la projection matrix define un campo de visión de 45 grados en la perspectiva de la cámara. Este tipo de matriz comúnmente se construye vía funciones preprogramadas de biblioteca, por ejemplo glMatrix la cual se puede acceder en: https://github.com/toji/gl-matrix

El ejemplo 1.4 muestra el código necesario para setear las Modelview y projection matrix

Ejemplo 1.4. Seteo de matrices Modelview y projection matrix

    function initMatrices()
    {
	   // La modelview matrix para el cuadrado - desplaza hacia atrás con respecto al eje Z
	   modelViewMatrix = new Float32Array(
	           [1, 0, 0, 0,
	            0, 1, 0, 0, 
	            0, 0, 1, 0, 
	            0, 0, -3.333, 1]);
       
	   // La projection matrix (para una projección de 45 grados)
	   projectionMatrix = new Float32Array(
	           [2.41421, 0, 0, 0,
	            0, 2.41421, 0, 0,
	            0, 0, -1.002002, -1, 
	            0, 0, -0.2002002, 0]);
	
    }
    

El Shader

Los shaders son pequeños programas, escritos en un lenguaje de alto nivel tipo C, que definen de qué manera los pixeles para objetos 3D son dibujados en pantalla. WebGL necesita que el programador provea un shader por cada objeto que es dibujado. El shader puede ser usado para múltiples objetos, por lo que muchas veces basta con proveer sólo un shader para toda la aplicación, reusándolo con diferentes parámetros cada vez que es llamado.

Un shader está compuesto por dos partes: el vertex shader y el fragment shader (también conocido como pixel shader). El vertex shader es responsable de transformar las coordenadas del objeto en el espacio 2D para despliegue; el fragment shader es responsable de generar el color resultante de cada pixel para los vértices transformados, basados en entradas tales como color, textura, luminosidad y material. En nuestro ejemplo, el vertex shader combina los valores de la modelViewMatrix y la projectionMatrix para crear los vértices transformados para cada entrada y el fragment shader sólo entrega un color blanco.

En WebGL configurar un shader requiere una secuencia de pasos, incluyendo compilar cada parte para posteriormente linkearlas. Para brevedad, sólo mostraremos el código fuente GLSL ES, no el código completo de nuestros dos shaders.

Ejemplo 1.5. El vertex y fragment shaders

    var vertexShaderSource =
		
		"    attribute vec3 vertexPos;\n" +
		"    uniform mat4 modelViewMatrix;\n" +
		"    uniform mat4 projectionMatrix;\n" +
		"    void main(void) {\n" +
		"   // Retorna los valores transformados y proyectados de los vértices\n" +
		"        gl_Position = projectionMatrix * modelViewMatrix * \n" +
		"            vec4(vertexPos, 1.0);\n" +
		"    }\n";
	var fragmentShaderSource = 
		"    void main(void) {\n" +
		"    // Retorna el color de los pixeles: blanco\n" +
        "    gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);\n" +
    	"}\n";
    

Dibujo de primitivas

Ahora estamos en condiciones de dibujar nuestro cuadrado. Ha sido creado el contexto, el viewport ha sido configurado, el buffer, matrices y shader han sido creados e inicializados. Se definirá la función draw() la que tomará el contexto WebGL y el objeto square previamente creado. En primer lugar la función limpia el canvas con un color de fondo negro. Luego se asocia (binds) el buffer (vertex) del cuadrado a dibujar, se configura (uses) el shader y se conectan el buffer y las matrices al shader como entradas. Finalmente se llama al método WebGL drawArrays() para dibujar el cuadrado. Para esto simplemente se indica qué tipo de primitivas y cuantas matrices hay en ellas; WebGL sabe qué más hacer porque hemos configurado los otros ítems (vértices, matrices y shaders) como estado en el contexto.

Ejemplo 1-6. El código que dibuja

        function draw(gl, obj)
        {
            // limpia el background (lo deja negro)
            gl.clearColor(0.0, 0.0, 0.0, 1.0);
            gl.clear(gl.COLOR_BUFFER_BIT);
            // asocia el vertex buffer a dibujar
            gl.bindBuffer(gl.ARRAY_BUFFER, obj.buffer);
            // setea el shader a usar
            gl.useProgram(shaderProgram);
            // conexión de los parámetros del shader: vertex position y matrices projection/model
            gl.vertexAttribPointer(shaderVertexPositionAttribute, obj.vertSize, gl.FLOAT, false, 0, 0);
            gl.uniformMatrix4fv(shaderProjectionMatrixUniform, false, projectionMatrix);
            gl.uniformMatrix4fv(shaderModelViewMatrixUniform, false, modelViewMatrix);
            // dibujar el objeto
            gl.drawArrays(obj.primtype, 0, obj.nVerts);
      }
    

| Home |