|
Grado en Ingeniería Informática
Realidad Virtual
Curso 2023/2024
|
Práctica 2
|
|
Programas
gráficos
|
|
Objetivos
|
|
El objetivo de esta práctica es desarrollar una aplicación gráfica
sencilla formada por un único triángulo en movimiento. Para ello es
necesario definir el contenido de las etapas programables del
proceso de renderizado (vertex shader y fragment shader),
generar los atributos de los vértices del triángulo y desarrollar la
interfaz del modelo para responder a los distintos eventos recibidos
en la ventana de la aplicación.
|
|
Un modelo 3D
sencillo
|
|
El código descrito en la práctica anterior permite generar una plantilla para
crear aplicaciones gráficas con OpenGL. Para completar las aplicaciones debemos
definir la clase CGModel dando contenido a las funciones que hemos utilizado
en la plantilla.
A continuación vamos a presentar un ejemplo de definición de la clase
CGModel
para desarrollar una aplicación sencilla. Se trata de mostrar un triángulo rojo que
se desplaza por una pantalla azul, rebotando en los bordes. El tamaño del triángulo
puede aumentarse y disminuirse pulsando sobre las teclas UP y DOWN.
El funcionamiento se puede resetear pulsando la tecla R.
El primer paso para desarrollar la aplicación consiste en crear el fichero de cabecera
CGModel.h, que contiene la definición de la clase. Además de predefinir los
métodos utilizados en la plantilla, la clase incluye una serie de campos que almacenarán
la información necesaria para desarrollar el modelo. Así, los campos
posX y posY contendrán la posición del centro del triángulo, el campo
size
indicará el tamaño del triángulo y los campos stepX y stepY
almacenarán la velocidad de movimiento del triángulo sobre los ejes
X e Y.
Para dibujar el triángulo se va a crear una clase específica
denominada CGTriangle que describiremos más adelante. Para
poder desarrollar el proceso de renderizado se utilizará una clase
denominada CGShaderProgram que describe un programa gráfico
basado en shaders.
#pragma once
#include <GL/glew.h>
#include "CGShaderProgram.h"
#include "CGTriangle.h"
class CGModel
{
public:
void initialize(int w, int h);
void finalize();
void render();
void update();
void key_pressed(int key);
void mouse_button(int button, int action);
void mouse_move(double xpos, double ypos);
void resize(int w, int h);
private:
GLfloat posX;
GLfloat posY;
GLfloat size;
GLfloat stepX;
GLfloat stepY;
CGTriangle* triangle;
CGShaderProgram* program;
};
|
Para definir el código de las funciones de la clase
CGModel se crea el fichero
CGModel.cpp. En primer lugar se define la función initialize(). Los argumentos de
esta función corresponden al tamaño de la ventana principal de la
aplicación, expesados en píxeles. Este método es el encargado de
crear y compilar el programa gráfico, inicializar los campos de la
clase y configurar las opciones generales de OpenGL. En este punto
se suelen cargar también las texturas que pueda necesitar el modelo,
pero en esta sencilla aplicación no se utilizarán texturas. Para
crear el programa gráfico se ha utilizado un constructor basado en
recursos pero se ha dejado comentado una versión basada en el nombre
de los ficheros fuente de los shaders.
El siguiente método a desarrollar es finalize(). Este
método se encarga de liberar los recursos reservados por el modelo.
En este caso se eliminan el objeto que describe el triángulo y el
programa gráfico.
El método resize() se ejecuta cuando se producen
cambios en el tamaño de la ventana. En este caso se limita a
modificar el viewport, aunque normalmente se modifica
también la matriz de proyección entre el sistema de coordenadas del
modelo y el sistema de coordenadas clip.
El método
render() es el encargado de dibujar el modelo. En
primer lugar "limpia" la imagen a mostrar llamando al comando
glClear(). El color de fondo con el que se limpia se asigna con
el comando glClearColor() al inicializar el modelo. A
continuación dibuja el triángulo llamando al método Draw()
de ese objeto.
El método
update() se ejecuta para actualizar el estado del
modelo siguiendo la frecuencia de refresco utilizada en la
aplicación (60 fps). Para generar la
animación se incrementan los valores de posX y posY con los
valores de velocidad stepX y stepY. Cuando la posición
del triángulo alcanza los límites del Clip se
cambia el signo de la velocidad para generar el efecto de rebote.
El método key_pressed() es la respuesta a eventos de
teclado. En este caso se aumenta el tamaño del triángulo al pulsar
sobre la tecla UP y se disminuye al pulsar la tecla DOWN. La tecla R
se usará para reinicializar la posición y el tamaño del triángulo.
Para finalizar, los métodos mouse_button() y
mouse_move() son la respuesta a los eventos de ratón.
En esta aplicación no se va a programar ninguna respuesta a estos eventos, por
lo que estos métodos se dejan en blanco.
#include "CGModel.h"
#include <GLFW\glfw3.h>
#include <iostream>
//
// FUNCIÓN: CGModel::initialize(int, int)
//
// PROPÓSITO: Initializa el modelo 3D
//
void CGModel::initialize(int w, int h)
{
// Crea el programa
program = new CGShaderProgram(IDR_SHADER1, IDR_SHADER2, -1, -1, -1);
//program = new CGShaderProgram("shaders/VertexShader.glsl",
"shaders/FragmentShader.glsl", NULL, NULL, NULL);
if (program->IsLinked() == GL_TRUE) program->Use();
else std::cout << program->GetLog() << std::endl;
// Crea la escena
triangle = new CGTriangle();
posX = 0.0f;
posY = 0.0f;
size = 5.0f;
stepX = 0.02f;
stepY = 0.01f;
// Asigna el viewport
resize(w, h);
// Opciones de dibujo
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
glPolygonMode(GL_FRONT, GL_FILL);
}
//
// FUNCIÓN: CGModel::finalize()
//
// PROPÓSITO: Libera los recursos del modelo 3D
//
void CGModel::finalize()
{
delete triangle;
delete program;
}
//
// FUNCIÓN: CGModel::resize(int w, int h)
//
// PROPÓSITO: Asigna el viewport y el clipping volume
//
void CGModel::resize(int w, int h)
{
glViewport(0, 0, w, h);
}
//
// FUNCIÓN: CGModel::render()
//
// PROPÓSITO: Genera la imagen
//
void CGModel::render()
{
// Limpia el framebuffer
glClear(GL_COLOR_BUFFER_BIT);
// Dibuja la escena
triangle->Draw(program, posX, posY, size);
}
//
// FUNCIÓN: CGModel::update()
//
// PROPÓSITO: Anima la escena
//
void CGModel::update()
{
GLfloat length = 0.01f * size;
posX += stepX;
if (posX - length < -1.0f) { posX -= stepX; stepX = 0.02f; }
if (posX + length > 1.0f) { posX -= stepX; stepX = -0.02f; }
posY += stepY;
if (posY - length < -1.0f) { posY -= stepY; stepY = 0.01f; }
if (posY + length > 1.0f) { posY -= stepY; stepY = -0.01f; }
}
//
// FUNCIÓN: CGModel::key_pressed(int)
//
// PROPÓSITO: Respuesta a acciones de teclado
//
void CGModel::key_pressed(int key)
{
switch (key)
{
case GLFW_KEY_R:
posX = 0.0f;
posY = 0.0f;
size = 5.0f;
break;
case GLFW_KEY_UP:
size += 0.5;
if (size >= 50.0f) size = 50.0f;
break;
case GLFW_KEY_DOWN:
size -= 0.5;
if (size <= 1.0f) size = 1.0f;
break;
}
}
//
// FUNCIÓN: CGModel:::mouse_button(int button, int action)
//
// PROPÓSITO: Respuesta del modelo a un click del ratón.
//
void CGModel::mouse_button(int button, int action)
{
}
//
// FUNCIÓN: CGModel::mouse_move(double xpos, double ypos)
//
// PROPÓSITO: Respuesta del modelo a un movimiento del ratón.
//
void CGModel::mouse_move(double xpos, double ypos)
{
}
|
|
|
Programa gráfico
|
|
Para realizar el programa gráfico es necesario crear el código del
Vertex Shader y del Fragment Shader e incluirlos en el programa.
Una forma de definir el código de estos shaders es incluirlo
directamente en el código C++ en forma de string. Sin embargo,
cuando el código del shader es largo, esta forma de definirlo
resulta muy incómoda. Una forma alternativa consiste en incluir el
código de los shaders en ficheros externos y cargarlos en tiempo de
ejecución. Esto obliga a distribuir los ficheros junto con el
ejecutable de la aplicación y se corre el riesgo de que pueden ser
manipulados externamente. La solución utilizada en estas prácticas
consiste en definir el código de los shaders en ficheros externos,
pero incluir estos ficheros como recursos de la aplicación. Los
recursos de una aplicación (por ejemplo, los iconos) se incrustan en
el fichero ejecutable. De esta forma se evita tener que
distribuirlos como ficheros asociados.
Para crear un recurso en VisualStudio hay que abrir la
ventana de "Agregar recurso". Se puede alcanzar esta
ventana desde el explorador de soluciones (a la derecha de la
ventana principal de VisualStudio) pulsando el botón derecho
sobre el directorio "Archivos de recursos", seleccionando
la opción de "Agregar" de este menú. En esta ventana hay que crear
un nuevo tipo de recurso pulsando sobre la opción "Personalizar".
La ventana de personalización permite crear un nuevo tipo de recurso
que llamaremos "SHADER".
Una vez creado el recurso es posible editar sus propiedades utilizando la "Vista de recursos".
Esta vista se puede obtener con el acelerador Ctrl+Mayús.+E. En la propiedad de nombre de
fichero vamos a introducir el nombre "shaders\VertexShader.glsl" para el archivo de código del
Vertex Shader y "shaders\FragmentShader.glsl" para el archivo de código del
Fragment Shader.
Una vez creado el archivo "shaders\VertexShader.glsl" podemos introducir el código de esta etapa.
En este caso se va a considerar un único atributo de entrada llamado VertexPosition
(que contendrá la posición de los vértices del triángulo respecto del centro del triángulo).
Para poder modificar la posición y el tamaño del triángulo se van a considerar tres variables
uniformes: posX y posY, que describen la posición del centro del triángulo en
coordenadas Clip, y size, que describe el tamaño del triángulo. La posición
final se calcula multiplicando los atributos por el tamaño y desplazando a la posición del
centro del triángulo.
#version 400
in vec3 VertexPosition;
uniform float posX;
uniform float posY;
uniform float size;
void main()
{
gl_Position = vec4( size*VertexPosition.x + posX
, size*VertexPosition.y + posY
, VertexPosition.z, 1.0);
}
|
Por su parte, el Fragment Shader utilizado es muy sencillo. La salida del Fragment Shader
debe ser el color de cada pixel del fragmento. En este caso todos los píxeles del interior
del triángulo se dibujarán de rojo.
#version 400
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0,0.0,0.0,1.0);
}
|
Para describir un programa gráfico se va a definir la clase
CGShaderProgram. El archivo
"CGShaderProgram.h" contiene la cabecera de esta clase. Los
miembros privados de la clase incluyen los identificadores de los
shader y del programa, así como el campo linked
para indicar si el programa se ha compilado y enlazado
correctamente.
#pragma once
#include <GL/glew.h>
#include <glm/glm.hpp>
#define NO_SHADER 0xffff
//
// CLASE: CGShaderProgram
//
// DESCRIPCIÓN: Clase que desarrolla un programa GLSL
//
class CGShaderProgram {
private:
GLuint program;
GLuint vertexShader;
GLuint fragmentShader;
GLuint geometryShader;
GLuint tessControlShader;
GLuint tessEvaluationShader;
GLboolean linked;
char* GetShaderCodeFromResource(int idr);
char* GetShaderCodeFromFile(const char* filename);
GLuint CreateShader(int mode, int idr);
GLuint CreateShader(int mode, const char* filename);
public:
CGShaderProgram(int vs, int fs, int gs, int tcs, int tes);
CGShaderProgram(const char* vs, const char* fs, const char* gs,
const char* tcs, const char* tes);
~CGShaderProgram();
GLboolean IsLinked();
GLvoid Use();
GLvoid SetUniformF(const char* name, GLfloat f);
GLvoid SetUniformVec4(const char* name, glm::vec4 m);
GLvoid SetUniformVec3(const char* name, glm::vec3 m);
GLvoid SetUniformMatrix4(const char* name, glm::mat4 m);
GLvoid SetUniformI(const char* name, GLint i);
};
|
El desarrollo de la clase CGShaderProgram se encuentra en el archivo "CGShaderProgram.cpp".
El constructor de la clase se encarga de crear los shaders indicados
en los argumentos. A continuación se crea el programa y se enlazan
los shaders, estudiando si se producen errores al enlazar. El destructor de la clase elimina los
shaders y el programa cargados en la tarjeta gráfica. El método GetShaderCodeFromResource() se encarga de acceder a los ficheros de recursos y cargar
el contenido en forma de string. Por su parte, el método
GetShaderCodeFromFile() permite cargar el código directamente de
un fichero externo no insertado como recurso. Los métodos
CreateShader() crean un shader, cargan su código y lo compilan,
especificando el tipo de shader y el recurso o nombre del fichero en
el que se encuentra el código GLSL. El método IsLinked() informa
del resultado del proceso de creación del programa. El método Use() activa el uso del programa en la tarjeta
gráfica. Los métodos SetUniform...() permiten asignar los valores a las variables uniformes.
#include <GL/glew.h>
#include <iostream>
#include <SDKDDKVer.h>
#include <Windows.h>
#include "CGShaderProgram.h"
#include "resource.h"
CGShaderProgram::CGShaderProgram(int vs, int fs, int gs, int tcs, int tes)
{
vertexShader = NO_SHADER;
fragmentShader = NO_SHADER;
geometryShader = NO_SHADER;
tessControlShader = NO_SHADER;
tessEvaluationShader = NO_SHADER;
linked = GL_FALSE;
// Crea y compila los shaders
if (vs != -1) vertexShader = CreateShader(GL_VERTEX_SHADER, vs);
if (fs != -1) fragmentShader = CreateShader(GL_FRAGMENT_SHADER, fs);
if (gs != -1) geometryShader = CreateShader(GL_GEOMETRY_SHADER, gs);
if (tcs != -1) tessControlShader = CreateShader(GL_TESS_CONTROL_SHADER, tcs);
if (tes != -1) tessEvaluationShader = CreateShader(GL_TESS_EVALUATION_SHADER, tes);
//Crea el programa y carga los shaders
program = glCreateProgram();
if (vertexShader != NO_SHADER) glAttachShader(program, vertexShader);
if (fragmentShader != NO_SHADER) glAttachShader(program, fragmentShader);
if (geometryShader != NO_SHADER) glAttachShader(program, geometryShader);
if (tessControlShader != NO_SHADER) glAttachShader(program, tessControlShader);
if (tessEvaluationShader != NO_SHADER) glAttachShader(program, tessEvaluationShader);
glLinkProgram(program);
GLint status;
glGetProgramiv(program, GL_LINK_STATUS, &status);
if (status == GL_FALSE)
{
linked = GL_FALSE;
GLint logLength;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &logLength);
char* logInfo = (char*)malloc(sizeof(char) * logLength);
GLsizei written;
glGetProgramInfoLog(program, logLength, &written, logInfo);
std::cout << logInfo << std::endl;
return;
}
linked = GL_TRUE;
}
CGShaderProgram::CGShaderProgram(const char* vs, const char* fs, const char* gs,
const char* tcs, const char* tes)
{
vertexShader = NO_SHADER;
fragmentShader = NO_SHADER;
geometryShader = NO_SHADER;
tessControlShader = NO_SHADER;
tessEvaluationShader = NO_SHADER;
linked = GL_FALSE;
// Crea y compila los shaders
if (vs != NULL) vertexShader = CreateShader(GL_VERTEX_SHADER, vs);
if (fs != NULL) fragmentShader = CreateShader(GL_FRAGMENT_SHADER, fs);
if (gs != NULL) geometryShader = CreateShader(GL_GEOMETRY_SHADER, gs);
if (tcs != NULL) tessControlShader = CreateShader(GL_TESS_CONTROL_SHADER, tcs);
if (tes != NULL) tessEvaluationShader = CreateShader(GL_TESS_EVALUATION_SHADER, tes);
//Crea el programa y carga los shaders
program = glCreateProgram();
if (vertexShader != NO_SHADER) glAttachShader(program, vertexShader);
if (fragmentShader != NO_SHADER) glAttachShader(program, fragmentShader);
if (geometryShader != NO_SHADER) glAttachShader(program, geometryShader);
if (tessControlShader != NO_SHADER) glAttachShader(program, tessControlShader);
if (tessEvaluationShader != NO_SHADER) glAttachShader(program, tessEvaluationShader);
glLinkProgram(program);
GLint status;
glGetProgramiv(program, GL_LINK_STATUS, &status);
if (status == GL_FALSE)
{
linked = GL_FALSE;
GLint logLength;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &logLength);
char* logInfo = (char*)malloc(sizeof(char) * logLength);
GLsizei written;
glGetProgramInfoLog(program, logLength, &written, logInfo);
std::cout << logInfo << std::endl;
return;
}
linked = GL_TRUE;
}
GLuint CGShaderProgram::CreateShader(int mode, int idr)
{
GLint status;
GLuint shader = glCreateShader(mode);
char* code = GetShaderCodeFromResource(idr);
glShaderSource(shader, 1, &code, NULL);
glCompileShader(shader);
free(code);
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (status == GL_FALSE)
{
GLint logLength;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &logLength);
char* logInfo = (char*)malloc(sizeof(char) * logLength);
GLsizei written;
glGetShaderInfoLog(shader, logLength, &written, logInfo);
std::cout << logInfo << std::endl;
return NO_SHADER;
}
return shader;
}
GLuint CGShaderProgram::CreateShader(int mode, const char* filename)
{
GLint status;
GLuint shader = glCreateShader(mode);
char* code = GetShaderCodeFromFile(filename);
glShaderSource(shader, 1, &code, NULL);
glCompileShader(shader);
free(code);
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (status == GL_FALSE)
{
GLint logLength;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &logLength);
char* logInfo = (char*)malloc(sizeof(char) * logLength);
GLsizei written;
glGetShaderInfoLog(shader, logLength, &written, logInfo);
std::cout << logInfo << std::endl;
return NO_SHADER;
}
return shader;
}
char* CGShaderProgram::GetShaderCodeFromResource(int idr)
{
HRSRC shaderHandle = FindResource(NULL, MAKEINTRESOURCE(idr), L"SHADER");
HGLOBAL shaderGlobal = LoadResource(NULL, shaderHandle);
LPCTSTR shaderPtr = static_cast<LPCTSTR>(LockResource(shaderGlobal));
DWORD shaderSize = SizeofResource(NULL, shaderHandle);
char* shaderCodeLine = (char*)malloc((shaderSize + 1) * sizeof(char));
memcpy(shaderCodeLine, shaderPtr, shaderSize);
shaderCodeLine[shaderSize] = '\0';
FreeResource(shaderGlobal);
return shaderCodeLine;
}
char* CGShaderProgram::GetShaderCodeFromFile(const char* filename)
{
FILE* file = NULL;
fopen_s(&file,filename, "r");
fseek(file, 0L, SEEK_END);
int size = ftell(file);
fseek(file, 0L, SEEK_SET);
char* code = (char*)malloc(sizeof(char) * (size + 1));
int read = fread(code, sizeof(char), size, file);
code[read] = '\0';
fclose(file);
return code;
}
CGShaderProgram::~CGShaderProgram()
{
if (vertexShader != NO_SHADER) glDeleteShader(vertexShader);
if (fragmentShader != NO_SHADER) glDeleteShader(fragmentShader);
if (geometryShader != NO_SHADER) glDeleteShader(geometryShader);
if (tessControlShader != NO_SHADER) glDeleteShader(tessControlShader);
if (tessEvaluationShader != NO_SHADER) glDeleteShader(tessEvaluationShader);
glDeleteProgram(program);
}
GLboolean CGShaderProgram::IsLinked()
{
return linked;
}
GLvoid CGShaderProgram::Use()
{
glUseProgram(program);
}
void CGShaderProgram::SetUniformF(const char * name, GLfloat f)
{
GLuint location = glGetUniformLocation(program, name);
if (location >= 0) glUniform1f(location, f);
}
GLvoid CGShaderProgram::SetUniformMatrix4(const char *name, glm::mat4 m)
{
GLuint location = glGetUniformLocation(program, name);
if (location >= 0) glUniformMatrix4fv(location, 1, GL_FALSE, &m[0][0]);
}
void CGShaderProgram::SetUniformVec4(const char * name, glm::vec4 v)
{
GLuint location = glGetUniformLocation(program, name);
if (location >= 0) glUniform4fv(location, 1, &v[0]);
}
void CGShaderProgram::SetUniformVec3(const char * name, glm::vec3 v)
{
GLuint location = glGetUniformLocation(program, name);
if (location >= 0) glUniform3fv(location, 1, &v[0]);
}
void CGShaderProgram::SetUniformI(const char * name, GLint i)
{
GLuint location = glGetUniformLocation(program, name);
if (location >= 0) glUniform1i(location, i);
}
|
|
|
Creación de un triángulo
|
|
Para completar el código de la aplicación queda desarrollar la clase que describa el triángulo.
La estructura de la clase se va a definir en un fichero de cabecera llamado "CGTriangle.h".
Este fichero define la cabecera de una clase formada por dos campos (el identificador del
VertexArrayObject del objeto y el identificador del VertexBufferObject que almacenará los
valores del atributo VertexPosition). La clase incluye el método Draw() que se
encargará de activar el VAO y lanzar la primitiva que dibuje el triángulo.
#pragma once
#include <GL/glew.h>
#include "CGShaderProgram.h"
//
// CLASE: CGTriangle
//
// DESCRIPCIÓN: Clase que representa un triángulo descrito mediante
// VAO para su renderizado mediante shaders
//
class CGTriangle {
private:
GLuint VBO;
GLuint VAO;
public:
CGTriangle();
~CGTriangle();
void Draw(CGShaderProgram* program, GLfloat posX, GLfloat posY, GLfloat size);
};
|
El desarrollo de la clase se va a definir en el fichero "CGTriangle.cpp".
Para construir el objeto se crea un array de valores con las coordenadas de los
vértices que formarán el triángulo. A partir de ahí se crea el VAO y el VBO y
se introduce el contenido. El destructor de la clase se encarga de eliminar el
VAO y el VBO, liberando de esta forma la memoria que ocupan en la tarjeta gráfica.
El método Draw() asigna los valores de las variables uniformes y a
continuación activa el VAO y lanza una primitiva GL_TRIANGLES indicando que
se tomen 3 vértices, por lo que se dibujará un único triángulo.
#include "CGTriangle.h"
#include <GL/glew.h>
//
// FUNCIÓN: CGTriangle::CGTriangle()
//
// PROPÓSITO: Crea un triángulo formado por tres vértices
//
CGTriangle::CGTriangle()
{
int numVertices = 3;
int numFaces = 1;
GLfloat vertices[9] = {
-0.01f, -0.01f, 0.5f,
0.01f, -0.01f, 0.5f,
0.0f, 0.01f, 0.5f
};
// Create the Vertex Array Object
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
// Create the Vertex Buffer Objects
glGenBuffers(1, &VBO);
// Vertex data
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * numVertices * 3, vertices,
GL_STATIC_DRAW);
glEnableVertexAttribArray(0); // Vertex position
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
}
//
// FUNCIÓN: CGTriangle::~CGTriangle()
//
// PROPÓSITO: Destruye el triángulo
//
CGTriangle::~CGTriangle()
{
// Delete vertex buffer objects
glDeleteBuffers(1, &VBO);
// Delete vetex array object
glDeleteVertexArrays(1, &VAO);
}
//
// FUNCIÓN: CGTriangle::Draw(CGShaderProgram*, GLfloat, GLfloat, GLfloat)
//
// PROPÓSITO: Dibuja el triángulo
//
void CGTriangle::Draw(CGShaderProgram* program, GLfloat posX, GLfloat posY,
GLfloat size)
{
program->SetUniformF("posX", posX);
program->SetUniformF("posY", posY);
program->SetUniformF("size", size);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
}
|
|
|
Aspecto final
|
|
Una vez generado los ficheros anteriores la aplicación puede ser
compilada y ejecutada. A continuación se muestra el
aspecto de la ventana principal de la aplicación en un instante.
|
|