Escuela Técnica Superior de Ingeniería

 

Grado en Ingeniería Informática

Realidad Virtual

Curso 2023/2024

 

Práctica 10

Teselado

 

Objetivos

 

En esta práctica se muestra como utilizar los shaders de teselado para generar una figura descrita por medio de superficies de Bezier, de modo que la precisión con la que se dibuja la figura se adapte a la distancia a la que se encuentre.

 

 

Código de la práctica

 

 

Superficies de Bezier

 

Las superficies de Bezier son una técnica de modelado de superficies que permiten construir superficies suaves por medio de puntos espaciales. La fórmula general de estas superficies es la siguiente:

Bezier

Los valores Pij se refieren a los puntos de control de la superficie mientras que las funciones B se refieren a polinomios de Bernstein cuya fórmula general es la siguiente:

Bernstein

En aplicaciones gráficas se suelen utilizar polinomios de Bernstein de tercer grado, que vienen dados por las siguientes expresiones.

Bernstein

Las superficies de Bezier de tercer grado necesitan 16 puntos de control. El aspecto final de estas superficies es el siguiente:

Bezier

 

 

El programa gráfico

 

Para generar las primitivas de las figuras por medio de teselado los vértices se limitan a almacenar los parámetros requeridos por el algoritmo de teselado. Las superficies de Bezier cúbicas se configuran por medio de 16 puntos. El  VertexShader se limita a propagar la posición de los vértices para utilizarlas como parámetros en los patches. Las transformaciones al sistema de coordenadas clip se realizarán en la etapa de teselado.

#version 400

layout (location = 0 ) in vec3 VertexPosition;

void main()
{
  gl_Position = vec4(VertexPosition, 1.0);
}

El TessellationControlShader es el responsable de fijar los niveles de teselado y de propagar la posición de los vértices. Para calcular el nivel de teselado se estudia la distancia a la que se encuentra el vértice (que corresponde a la componente Z de la posición del vértice en coordenadas de la cámara). El nivel de teselado se obtiene considerando la distancia máxima y mínima a la que los niveles de teselado son mínimos y máximos.

#version 400

layout( vertices=16 ) out;

uniform int MinTessLevel;
uniform int MaxTessLevel;
uniform float MaxDepth;
uniform float MinDepth;

uniform mat4 ModelViewMatrix;

void main()
{
  // Position in camera coordinates
  vec4 p = ModelViewMatrix *gl_in[gl_InvocationID].gl_Position;

  // "Distance" from camera scaled between 0 and 1
  float depth = clamp( (abs(p.z) - MinDepth)/(MaxDepth-MinDepth), 0.0, 1.0 );

  // Interpolate between min/max tess levels
  float tessLevel = mix(MaxTessLevel, MinTessLevel, depth);

  gl_TessLevelOuter[0] = float(tessLevel);
  gl_TessLevelOuter[1] = float(tessLevel);
  gl_TessLevelOuter[2] = float(tessLevel);
  gl_TessLevelOuter[3] = float(tessLevel);
  gl_TessLevelInner[0] = float(tessLevel);
  gl_TessLevelInner[1] = float(tessLevel);

  gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
}

El TessellationEvaluationShader es el responsable de generar los atributos de los vértices asociados a las primitivas creadas en el proceso de teselado. Las coordenadas de teselado se encuentran en la variable de entrada predefinida gl_TessCoord. A partir de estas coordenadas se calcula la posición y la normal del vértice por medio de una superficie de Bezier. Los parámetros de la superficie se obtienen de la información recopilada por la primitiva patch (16 posiciones). La función basisFunctions() calcula los parámetros de los polinomios de Berstein y sus derivadas en una dirección. Los polinomios de Berstein permiten calcular la posición de cada vértice mientras que la derivada permite obtener el vector tangente en cada dirección (u y v). La normal de cada vértice se calcula como el producto vectorial de las dos tangentes.

#version 400

layout( quads ) in;

out vec3 TENormal; // Vertex normal in camera coords.
out vec4 TEPosition; // Vertex position in camera coords

uniform mat4 MVP;
uniform mat4 ModelViewMatrix;

void basisFunctions(out float[4] b, out float[4] db, float t)
{
  float t1 = (1.0 - t);
  float t12 = t1 * t1;

  // Bernstein polynomials
  b[0] = t12 * t1;
  b[1] = 3.0 * t12 * t;
  b[2] = 3.0 * t1 * t * t;
  b[3] = t * t * t;

  // Derivatives
  db[0] = -3.0 * t1 * t1;
  db[1] = -6.0 * t * t1 + 3.0 * t12;
  db[2] = -3.0 * t * t + 6.0 * t * t1;
  db[3] = 3.0 * t * t;
}

void main()
{
  float u = gl_TessCoord.x;
  float v = gl_TessCoord.y;

  // The sixteen control points
  vec4 p00 = gl_in[0].gl_Position;
  vec4 p01 = gl_in[1].gl_Position;
  vec4 p02 = gl_in[2].gl_Position;
  vec4 p03 = gl_in[3].gl_Position;
  vec4 p10 = gl_in[4].gl_Position;
  vec4 p11 = gl_in[5].gl_Position;
  vec4 p12 = gl_in[6].gl_Position;
  vec4 p13 = gl_in[7].gl_Position;
  vec4 p20 = gl_in[8].gl_Position;
  vec4 p21 = gl_in[9].gl_Position;
  vec4 p22 = gl_in[10].gl_Position;
  vec4 p23 = gl_in[11].gl_Position;
  vec4 p30 = gl_in[12].gl_Position;
  vec4 p31 = gl_in[13].gl_Position;
  vec4 p32 = gl_in[14].gl_Position;
  vec4 p33 = gl_in[15].gl_Position;

  // Compute basis functions
  float bu[4], bv[4]; // Basis functions for u and v
  float dbu[4], dbv[4]; // Derivitives for u and v
  basisFunctions(bu, dbu, u);
  basisFunctions(bv, dbv, v);

  // Bezier interpolation
  TEPosition = p00*bu[0]*bv[0] + p01*bu[0]*bv[1] + p02*bu[0]*bv[2] +
               p03*bu[0]*bv[3] + p10*bu[1]*bv[0] + p11*bu[1]*bv[1] + 
               p12*bu[1]*bv[2] + p13*bu[1]*bv[3] + p20*bu[2]*bv[0] + 
               p21*bu[2]*bv[1] + p22*bu[2]*bv[2] + p23*bu[2]*bv[3] +
               p30*bu[3]*bv[0] + p31*bu[3]*bv[1] + p32*bu[3]*bv[2] +
               p33*bu[3]*bv[3];

  // The partial derivatives
  vec4 du = p00*dbu[0]*bv[0] + p01*dbu[0]*bv[1] + p02*dbu[0]*bv[2] +
            p03*dbu[0]*bv[3] + p10*dbu[1]*bv[0] + p11*dbu[1]*bv[1] +
            p12*dbu[1]*bv[2] + p13*dbu[1]*bv[3] + p20*dbu[2]*bv[0] +
            p21*dbu[2]*bv[1] + p22*dbu[2]*bv[2] + p23*dbu[2]*bv[3] +
            p30*dbu[3]*bv[0] + p31*dbu[3]*bv[1] + p32*dbu[3]*bv[2] +
            p33*dbu[3]*bv[3];

  vec4 dv = p00*bu[0]*dbv[0] + p01*bu[0]*dbv[1] + p02*bu[0]*dbv[2] +
            p03*bu[0]*dbv[3] + p10*bu[1]*dbv[0] + p11*bu[1]*dbv[1] +
            p12*bu[1]*dbv[2] + p13*bu[1]*dbv[3] + p20*bu[2]*dbv[0] +
            p21*bu[2]*dbv[1] + p22*bu[2]*dbv[2] + p23*bu[2]*dbv[3] +
            p30*bu[3]*dbv[0] + p31*bu[3]*dbv[1] + p32*bu[3]*dbv[2] +
            p33*bu[3]*dbv[3];

  // The normal is the cross product of the partials
  vec3 n = normalize( cross(du.xyz, dv.xyz) );
  vec4 nn = ModelViewMatrix * vec4(n,0.0);

  // Transform to clip coordinates
  gl_Position = MVP * TEPosition;

  // Convert to camera coordinates
  TEPosition = ModelViewMatrix * TEPosition;
  TENormal = normalize(vec3(nn));
}

El FragmentShader corresponde al modelo tradicional de iluminación de Phong, considerando que en este caso las posiciones y las normales se han calculado en la etapa de teselado en vez de en el vertex shader.

#version 400

in vec4 TEPosition;
in vec3 TENormal;

uniform mat4 ViewMatrix;

struct LightInfo {
	vec3 Ldir;
	vec3 La;
	vec3 Ld;
	vec3 Ls;
};
uniform LightInfo Light;

struct MaterialInfo{
	vec3 Ka;
	vec3 Kd;
	vec3 Ks;
	float Shininess;
};
uniform MaterialInfo Material;

out vec4 FragColor;

vec3 ads() 
{
  vec4 s4 = ViewMatrix*vec4(Light.Ldir, 0.0);
  vec3 n = normalize(TENormal);
  vec3 v = normalize(-vec3(TEPosition));
  vec3 s = normalize(-vec3(s4));
  vec3 r = reflect(-s, n);
  float dRate = max(dot(s, n), 0.0);
  float sRate = pow(max(dot(r, v), 0.0), Material.Shininess);
  vec3 ambient = Light.La * Material.Ka;
  vec3 difusse = Light.Ld * Material.Kd * dRate;
  vec3 specular = Light.Ls * Material.Ks * sRate;
  return ambient + difusse + specular;
}

void main()
{
  vec3 Color = ads();
  FragColor = vec4(Color,1.0);
//    FragColor = vec4(1.0,0.0,0.0,1.0);
}

 

 

La clase CGTeapot

 

Como ejemplo de un objeto descrito por medio de superficies de Bezier vamos a utilizar la tetera de Utah (este modelo fue propuesto inicialmente por Martin Newell en los años 70 como figura compleja sobre la que estudiar los diferentes modos de iluminación que se estaban desarrollando en esos momentos). La tetera se genera a partir de 32 superficies de Bezier. Los vértices utilizados para definir estas superficies son estos. El aspecto de la tetera dividida en trozos es el siguiente:

Scene

Para generar la tetera vamos a adaptar la definición de una pieza (clase CGPiece). En este caso hay que tener en cuenta que los vértices solo utilizan como atributo la posición, que es el dato que se utilizará como parámetros en los patches. Por esa razón solo es necesario definir un VBO que almacenará el atributo posición. El contenido del fichero de cabecera es el siguiente:

#pragma once

#include <GL/glew.h>
#include <glm/glm.hpp>
#include "CGMaterial.h"
#include "CGShaderProgram.h"

//
// CLASE: CGTeapot
//
// DESCRIPCIÓN: Clase describe la tetera de Utah
// 
class CGTeapot {
protected:
  GLuint numPatches; // Number of patches
  GLuint numVertices; // Number of vertices
  GLuint VBO;
  GLuint VAO;

  glm::mat4 location; // Model matrix
  CGMaterial* material;

public:
  CGTeapot();
  ~CGTeapot();
  void SetMaterial(CGMaterial* m);
  void SetLocation(glm::mat4 loc);
  glm::mat4 GetLocation();
  void Translate(glm::vec3 t);
  void Rotate(GLfloat angle, glm::vec3 axis);
  void Draw(CGShaderProgram* program, glm::mat4 projection, glm::mat4 view);
};

El constructor de la tetera define los vértices utilizados en las 32 superficies y genera el VAO con el VBO correspondiente. Para dibujar la tetera (método Draw) hay que introducir las variables uniformes utilizadas en el shader de teselado y lanzar el dibujo como un conjunto de primitivas GL_PATCH. El resto de métodos están dedicados a gestionar el material y la posición de la pieza y su contenido es igual al de prácticas anteriores. A continuación se muestra el fichero de código asociado a la clase CGTeapot.

#include "CGTeapot.h"
#include <GL/glew.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include "CGMaterial.h"

CGTeapot::CGTeapot()
{
  material = new CGMaterial();
  numPatches = 32;
  numVertices = numPatches*16;
  location = glm::mat4(1.0f);

  GLfloat teapotVertices[][3] = { ... };

  // Create the Vertex Array Object
  glGenVertexArrays(1, &VAO);
  glBindVertexArray(VAO);

  // Create the Vertex Buffer Objects
  glGenBuffers(1, &VBO);

  // Copy data to video memory
  // Vertex data
  int buffersize = sizeof(GLfloat)*numVertices * 3;
  glBindBuffer(GL_ARRAY_BUFFER, VBO);
  glBufferData(GL_ARRAY_BUFFER, buffersize, teapotVertices, GL_STATIC_DRAW);

  glEnableVertexAttribArray(0); // Vertex position
  glBindBuffer(GL_ARRAY_BUFFER, VBO);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
}

//
// FUNCIÓN: CGTeapot::~CGTeapot()
//
// PROPÓSITO: Destructor de la pieza
//
CGTeapot::~CGTeapot()
{
  glDeleteBuffers(1, &VBO);
  glDeleteVertexArrays(1, &VAO);
}

//
// FUNCIÓN: CGTeapot::SetMaterial(CGMaterial* m)
//
// PROPÓSITO: Asigna el material de la pieza
//
void CGTeapot::SetMaterial(CGMaterial* m)
{
  material = m;
}

//
// FUNCIÓN: CGTeapot::SetLocation(glm::mat4 loc)
//
// PROPÓSITO: Asigna la matriz de posición de la pieza respecto a la posición de la
// figura a la que pertenece.
//
void CGTeapot::SetLocation(glm::mat4 loc)
{
  location = loc;
}

//
// FUNCIÓN: CGTeapot::GetLocation()
//
// PROPÓSITO: Obtiene la matriz de posición de la pieza respecto a la posición de la
// figura a la que pertenece.
//
glm::mat4 CGTeapot::GetLocation()
{
  return location;
}

//
// FUNCIÓN: CGTeapot::Translate(glm::vec3 t)
//
// PROPÓSITO: Añade un desplazamiento a la matriz de posición de la pieza 
// respecto a la posición de la figura a la que pertenece.
//
void CGTeapot::Translate(glm::vec3 t)
{
  location = glm::translate(location, t);
}

//
// FUNCIÓN: CGTeapot::Rotate(GLfloat angle, glm::vec3 axis)
//
// PROPÓSITO: Añade una rotación a la matriz de posición de la pieza 
// respecto a la posición de la figura a la que pertenece.
//
void CGTeapot::Rotate(GLfloat angle, glm::vec3 axis)
{
  location = glm::rotate(location, glm::radians(angle), axis);
}

//
// FUNCIÓN: CGTeapot::Draw(CGShaderProgram * program, glm::mat4 projection, 
//                                glm::mat4 view, glm::mat4 model)
//
// PROPÓSITO: Dibuja la pieza
//
void CGTeapot::Draw(CGShaderProgram * program, glm::mat4 projection, glm::mat4 view)
{
  glm::mat4 mvp = projection*view*location;
  program->SetUniformMatrix4("MVP", mvp);
  program->SetUniformMatrix4("ViewMatrix", view);
  program->SetUniformMatrix4("ModelViewMatrix", view*location);
  program->SetUniformI("MinTessLevel", 2);
  program->SetUniformI("MaxTessLevel", 16);
  program->SetUniformF("MaxDepth", 40.0f);
  program->SetUniformF("MinDepth", 5.0f);

  material->SetUniforms(program);

  glBindVertexArray(VAO);
  glPatchParameteri(GL_PATCH_VERTICES, 16);
  glDrawArrays(GL_PATCHES, 0, numPatches*16);
}

 

 

La clase CGScene

 

La escena está formada por un objeto CGLight (con la configuración de la luz) y un objeto CGTeapot (la tetera). El material de la tetera se ha escogido con un color porcelana.

#include "CGScene.h"
#include <GL/glew.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include "CGShaderProgram.h"
#include "CGLight.h"
#include "CGTeapot.h"

//
// FUNCIÓN: CGScene::CGScene()
//
// PROPÓSITO: Construye la escena
//
CGScene::CGScene()
{
  glm::vec3 Ldir = glm::vec3(1.0f, -1.0f, -1.0f);
  Ldir = glm::normalize(Ldir); 
  light = new CGLight();
  light->SetLightDirection(Ldir);
  light->SetAmbientLight(glm::vec3(0.2f, 0.2f, 0.2f));
  light->SetDifusseLight(glm::vec3(0.8f, 0.8f, 0.8f));
  light->SetSpecularLight(glm::vec3(1.0f, 1.0f, 1.0f));

  CGMaterial* mtl = new CGMaterial();
  mtl->SetAmbientReflect(0.90f, 0.87f, 0.81f);
  mtl->SetDifusseReflect(0.90f, 0.87f, 0.81f);
  mtl->SetSpecularReflect(1.0f, 1.0f, 1.0f);
  mtl->SetShininess(32.0f);

  teapot = new Teapot();
  teapot->SetMaterial(mtl);
}

//
// FUNCIÓN: CGScene::~CGScene()
//
// PROPÓSITO: Destruye el objeto que representa la escena
//
CGScene::~CGScene()
{
  delete teapot;
  delete light;
}

//
// FUNCIÓN: CGScene::Draw()
//
// PROPÓSITO: Dibuja la escena
//
void CGScene::Draw(CGShaderProgram* program, glm::mat4 proj, glm::mat4 view)
{
  light->SetUniforms(program);
  teapot->Draw(program, proj, view);
}

Para que se aprecie mejor la tetera se ha utilizado un color de fondo celeste (asignado en la clase CGModel mediante el comando glClearColor). Para observar mejor el funcionamiento del teselado se puede dibujar la tetera en modo arista (modificando el comando glPolygonMode en la clase CGModel) y modificar el FragmentShader para que el color de los puntos de la arista sea rojo. A continuación se muestran dos imágenes con el resultado en modo relleno y en modo arista.

Scene

Scene