Visualización de Geometría

Informática Gráfica

José Ribelles

Departamento de Lenguajes y Sistemas Informáticos

Universitat Jaume I

En la práctica anterior aprendiste a compilar y enlazar un shader, y a ubicarlo en la GPU. Se visualizaba un único triángulo y probaste a modificar sus coordenadas. Sin embargo, la parte correspondiente a cómo visualizar dicho triángulo corresponde a esta nueva práctica. Y vamos a empezar justo por ahí.

Última revisión: 20/9/2016

Define la Geometría

Habitualmente solemos asociar el concepto de vértice con las coordenadas que definen la posición de un punto en el espacio. En OpenGL, el concepto de vértice es más general entendiéndose como una agrupación de datos a los que llamamos atributos. Estos pueden ser de cualquier tipo: reales, enteros, vectores, etc. Los atributos más utilizados son la posición, la normal y el color. Pero OpenGL permite que el programador pueda incluir como atributo cualquier información que para él tenga sentido y que necesite tener disponible en el shader.

var exampleTriangle = {


  "vertices" : [

    -0.7, -0.7, 0.0,

     0.7, -0.7, 0.0,

     0.0,  0.7, 0.0],


  "indices" : [ 0, 1, 2]


};

OpenGL no proporciona mecanismos para describir o modelar objetos geométricos complejos, sino que proporciona mecanismos para especificar cómo dichos objetos deben ser dibujados. Es responsabilidad del programador definir las estructuras de datos adecuadas para almacenar la descripción del objeto.  Sin embargo, como OpenGL requiere que la información que vaya a visualizarse se disponga en vectores, lo habitual es utilizar también vectores para almacenar los vértices así como sus atributos y utilizar índices a dichos vectores para definir las primitivas geométricas. Por ejemplo así :

Dibujado

En primer lugar, el modelo se ha de almacenar en buffer objects. Un buffer object no es mas que una porción de memoria reservada dinámicamente y controlada por el propio procesador gráfico. Siguiendo con el ejemplo anterior necesitaremos dos buffers, uno para el vector de vértices y otro para el de índices. Después hay que asignar a cada buffer sus datos correspondientes. El siguiente listado recoge estas operaciones, examínalo y consulta la especificación del lenguaje para conocer más detalles sobre las funciones utilizadas.

  model.idBufferVertices = gl.createBuffer();

  gl.bindBuffer(gl.ARRAY_BUFFER, model.idBufferVertices);

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


  model.idBufferIndices = gl.createBuffer();

  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, model.idBufferIndices);

  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(model.indices), gl.STATIC_DRAW);

En segundo lugar, hay que obtener los índices de las variables del shader que representan los atributos de los vértices, que en nuestro ejemplo son solo las coordenadas, y habilitar el vector correspondiente.

  program.vertexPositionAttribute = gl.getAttribLocation(program, "VertexPosition");


  gl.enableVertexAttribArray(program.vertexPositionAttribute);

Ahora que ya tenemos el modelo almacenado en la memoria controlada por la GPU, el shader compilado y enlazado, y obtenidos los índices de los atributos de los vértices, ya sólo nos queda el último paso, su visualización. Primero, para cada atributo hay que especificar dónde y cómo se encuentra almacenado. Después ya se puede ordenar el dibujado, indicando tipo de primitiva y número de elementos. Los vértices se procesarán de manera independiente, pero siempre en el orden en el que son enviados al procesador gráfico. El siguiente listado muestra esta operación. De nuevo, acude a la especificación del lenguaje para conocer más detalles de las órdenes utilizadas.

  gl.bindBuffer(gl.ARRAY_BUFFER, model.idBufferVertices);

  gl.vertexAttribPointer(program.vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);


  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, model.idBufferIndices);

  gl.drawElements(gl.TRIANGLES, 3, gl.UNSIGNED_SHORT, 0);

Tipos de primitivas

Las primitivas básicas de dibujo son el punto, el segmento de línea y el triángulo. Cada primitiva se define especificando sus respectivos vértices. En OpenGL, la primitiva geométrica se utiliza para especificar cómo han de ser agrupados los vértices tras ser operados en el procesador de vértices y así poder ser visualizada. Son las siguientes:

- Dibujo de puntos:

     - gl.POINTS

- Dibujo de líneas:

     - Segmentos independientes: gl.LINES

     - Secuencia o tira de segmentos: gl.LINE_STRIP

     - Secuencia cerrada de segmentos: gl.LINE_LOOP

- Triángulos

     - Triángulos independientes: gl.TRIANGLES

     - Tira de triángulos: gl.TRIANGLE_STRIP

     - Abanico de triángulos: gl.TRIANGLE_FAN

Una tira de triángulos es una serie de triángulos conectados  a través de aristas compartidas. Se define mediante una secuencia de vértices, donde los primeros tres vértices definen el primer triángulo. Cada nuevo vértice define un nuevo triángulo utilizando ese vértice y los dos últimos del triángulo anterior. El abanico de triángulos es igual que la tira excepto que cada vértice nuevo sustituye siempre al segundo del triángulo anterior.

var examplePentagon = {


  "vertices" : [ 0.0,  0.9, 0.0,

                -0.95, 0.2, 0.0,

                -0.6, -0.9, 0.0,

                 0.6, -0.9, 0.0,

                 0.95, 0.2, 0.0],


  "indices" : [ 0, 1, 2, 3, 4]


};

  Primero así:

    gl.drawElements(gl.POINTS, 5, gl.UNSIGNED_SHORT, 0);

  

  Y después así:

    gl.drawElements(gl.LINE_LOOP, 5, gl.UNSIGNED_SHORT, 0);

Ejercicio 3

Ahora piensa las modificaciones necesarias para pintar el pentágono como triángulos independientes. De esta manera pasarás a verlo relleno (en lugar de solo las aristas). Piensa primero y haz los cambios después. Recuerda que aunque los vértices son los mismos, tendrás que averiguar la nueva secuencia de índices.

"vertices" : [

    0.0,   0.9,  0.0,

   -0.95,  0.2,  0.0,

   -0.6,  -0.9,  0.0,

    0.6,  -0.9,  0.0,

    0.95,  0.2,  0.0,

    0.0,  -0.48, 0.0,

    0.37, -0.22, 0.0,

    0.23,  0.2,  0.0,

   -0.23,  0.2,  0.0,

   -0.37, -0.22, 0.0]


(placeholder)

Variables Uniform

Imagina que quieres dibujar la estrella cada vez de un color diferente. Para conseguirlo necesitas que el color que figura en el shader de fragmentos sea una variable. Por ejemplo así:

  // Fragment shaders have no default float precision,

  // and as such a precision statement must occur

  // before any statements using floats


  precision mediump float;


  uniform vec4 myColor;


  void main() {

    gl_FragColor = myColor;

  }

La variable myColor es de tipo uniform porque su valor será constante para todos los fragmentos que reciba procedentes de procesar la estrella, pero podemos hacer que sea diferente cambiando su valor antes de dibujarla. Observa el siguiente fragmento de código. Las dos primeras líneas son las encargadas de obtener el índice de la variable myColor en el shader y de especificar su valor. Al dibujar la estrella, tercera línea, la observaremos de color RGBA (1, 0, 1, 1).

  var idMyColor = gl.getUniformLocation(program, "myColor");


  gl.uniform4f(idMyColor, 1, 0, 1, 1);


  drawGeometry...();

Variables Varying

Hasta el momento, cada vértice consta únicamente de sus coordenadas. Ahora vamos a añadir un segundo atributo, por ejemplo, un valor de color para cada vértice. Parte de nuevo de visualiza.zip para realizar las modificaciones.

La primera modificación corresponde al vector de vértices, que siguiendo con el ejemplo inicial del triángulo, podría ser algo así donde se han añadido las cuatro componentes de color RGBA a cada uno:

  "vertices" : [

      -0.7, -0.7, 0.0,    1.0, 0.0, 0.0, 1.0,  // rojo

       0.7, -0.7, 0.0,    0.0, 1.0, 0.0, 1.0,  // verde

       0.0,  0.7, 0.0,    0.0, 0.0, 1.0, 1.0   // azul

  ]

También, cada vértice consta ahora de dos atributos, coordenadas y color, por lo que también hay que modificar los shaders:

  //

  // Shader de vértices

  //

  attribute vec3 VertexPosition;

  attribute vec4 VertexColor;

    

  varying vec4 colorOut;

    

  void main()  {

    colorOut    = VertexColor;

    gl_Position = vec4(VertexPosition, 1);

  }



  //

  // Shader de fragmentos

  //

  precision mediump float;

     

  varying vec4 colorOut;


  void main() {

    gl_FragColor = colorOut;

  }

Y también habilitar los dos atributos correspondientes:

  program.vertexPositionAttribute = gl.getAttribLocation(program, "VertexPosition");

  gl.enableVertexAttribArray(program.vertexPositionAttribute);


  program.vertexColorAttribute = gl.getAttribLocation(program, "VertexColor");

  gl.enableVertexAttribArray(program.vertexColorAttribute);

Y por último la función de dibujo. Presta atención a los dos últimos parámetros de las dos llamadas a gl.vertexAttribPointer.

  function draw(model) {


    gl.bindBuffer(gl.ARRAY_BUFFER, model.idBufferVertices);

    gl.vertexAttribPointer(program.vertexPositionAttribute, 3, gl.FLOAT, false, 7*4, 0);

    gl.vertexAttribPointer(program.vertexColorAttribute,    4, gl.FLOAT, false, 7*4, 3*4);


    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, model.idBufferIndices);

    gl.drawElements(gl.TRIANGLES, 3, gl.UNSIGNED_SHORT, 0);


}

Si has hecho bien todos los cambios debes obtener un resultado como este:

¿Serías capaz de dar una explicación de qué ha ocurrido en el pipeline del sistema gráfico?

La variable colorOut se ha declarado con el identificador varying tanto en el shader de vértices como en el de fragmentos. Una variable de tipo varying toma un valor en el shader de vértices para cada vértice, y se propaga a través del pipeline. En el shader de fragmentos, la variable colorOut tendrá un valor convenientemente interpolado para cada fragmento de la primitiva que se esté dibujando. La interpolación la realiza automáticamente la GPU a partir de los valores asignados a colorOut en cada vértice.

(placeholder)

Ejercicio 1

Descarga el código visualiza.zip. Contiene todos los fragmentos de código anteriores.  Estúdialo detenidamente:

1. Comprueba dónde aparecen los diferentes trozos de código.

2. Verifica el orden de ejecución de las diferentes funciones.

3. Aclara con tu profesor todas las dudas que te surjan.

(placeholder)
(placeholder)

Ejercicio 2

Utiliza el código de visualiza.zip como punto de partida. Lo primero que vas a hacer es cambiar el triángulo por un pentágono. Prueba a pintarlo de dos maneras diferentes, primero solo con puntos (te costará distinguirlos en la pantalla) y después con líneas.

(placeholder)

Ejercicio 4

Dibuja el pentágono utilizando la primitiva abanico de triángulos (de nuevo los vértices son los mismos pero no la secuencia de índices).

Llegados a este punto, has utilizado cuatro primitivas diferentes. Has visto que para cada primitiva la secuencia de índices puede cambiar. Y eso conlleva no solo modificar el vector de índices sino también uno de los parámetros de la orden drawElements.

(placeholder)
(placeholder)

Ejercicio 5

Vuelve a dibujar con líneas y, utilizando todavía los mismos vértices del pentágono, realiza las modificaciones necesarias para obtener una estrella como la de la figura de la derecha.

Prueba también a cambiar el grosor del trazo mediante la orden gl.lineWidth(5.0) justo antes de ordenar el dibujo de la geometría (en Windows no suele funcionar).

Ejercicio 6

¿Qué modificaciones tendrías que hacer si en su lugar quisieras obtener el resultado que se muestra en la figura de abajo? Crea una función drawGeometryLines() que la dibuje. A su derecha tienes los vértices como ayuda.

(placeholder)

Ejercicio 7

Escribe una nueva función drawGeometryTriangles() que dibuje la estrella con triángulos independientes de manera que obtengas el resultado que se muestra en la siguiente figura.

(placeholder)
(placeholder)
(placeholder)

Ejercicio 8

Dibuja dos estrellas, una la utilizarás para pintar el interior (utiliza drawGeometryTriangles), y la otra para pintar el borde (utiliza drawGeometryLines). Cada una de un color diferente de manera que contrasten entre sí. Fíjate en el ejemplo de la derecha. Ten en cuenta también que el orden de dibujado es importante, piensa qué estrella deberías dibujar en primer lugar. Recuerda que el shader está en visualiza.html, sustituye el shader de fragmentos por el que figura en la sección previa. Nota: puedes definir dos vectores de índices con diferente nombre y crear un buffer adicional en la función initBuffers.

(placeholder)

Ejercicio 9

Observa la imagen de la derecha, ¿serías capaz de determinar qué colores se han asignado a los vértices? Pruébalo!

(placeholder)