|
Grado en Ingeniería Informática
Realidad Virtual
Curso 2023/2024
|
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.
|
|
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:

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:

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

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

|
|
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:

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.
|
|