Vamos a dibujar un cubo dando vueltas sobre sí mismo a una velocidad constante con respecto al tiempo y no a la velocidad de procesamiento. Esto es muy importante tenerlo en cuenta, iremos viendo las diferencias que puede suponer.
Lo vamos a dibujar de una manera un poco especial, utilizando la función glDrawElements, que explicaremos más adelante. En otro post explicaré las diferentes maneras que tenemos de dibujar una misma cosa en OpenGL.
Lo primero decir que vamos a usar la librería vmath, como habíamos comentado en los post de introducción. Si no lo has visitado éste es el enlace. Además vamos a utilizar otra librería que he hecho yo mismo, se trata de unas pocas lineas que nos van a permitir mostrar en el titulo de la ventana los FPS (Frames Per Second - Imágenes por segundo) del entorno OpenGL. Esto nos va a permitir hacer mediciones de eficiencia más adelante. (No es una librería para ser reutilizada con otros entornos, sólo asegura funcionar en SDL 2.0. Es una librería de andar por casa para hacer pruebas.)
Info_Manager.h:
Tenemos 4 funciones:
- frame: sirve para indicar que se ha dibujado un frame, devuelve true si es un fotograma clave (acaba de pasar un segundo desde el ultimo fotograma clave) o false si no lo es.
El primer if es una precaución para el caso en que SDL_GetTicks llegue a MAX_INT y vuelva a 0. En ese caso se resetean los frames y se vuelve a empezar. - fps: es un getter o accesor
- fps_ogl_ver_on_window: pretende tener un nombre intuitivo. Establece el nombre del titulo de la ventana poniendo primero el título como tal y concatenandolo con los FPS y la versión de OpenGL actual del componente.
- client_info: Devuelve un String tipo C con diversas informaciones sobre el componente gráfico que se esta utilizando.
#ifndef FPS_CTRLR_H_ #define FPS_CTRLR_H_ #include <iostream> #include <string> #include <sstream> #include <GL/gl3w.h> #include <GL/gl3.h> #include "SDL2/SDL.h" #include "LoadShaders.h" class Info_Manager { private: Uint32 _last_time_FPS; Uint32 _frames; Uint32 _fps; public: Info_Manager() : _last_time_FPS(SDL_GetTicks()), _frames(0), _fps(0){} bool frame(){ _frames++; //Por si llega al limite de los int if (_last_time_FPS - 1000 > SDL_GetTicks()){ _last_time_FPS = SDL_GetTicks(); _frames = 0; } if ((_last_time_FPS + 1000) <= SDL_GetTicks()){ _last_time_FPS += 1000; _fps = _frames; _frames = 0; return true; } return false; } Uint32 fps(){ return _fps; } void fps_ogl_ver_on_window(SDL_Window* window, const char* win_title){ std::stringstream sstring; sstring << win_title << " [FPS: " << _fps << ", OGL-version: " << glGetString(GL_VERSION) << "]"; std::string s = sstring.str(); SDL_SetWindowTitle(window, s.c_str()); } const char* client_info(){ std::stringstream sstring; sstring << "--------------------------------------------------------" << std::endl << " + OGL-version: " << glGetString(GL_VERSION) << std::endl << " + GLSL-version: " << glGetString(GL_SHADING_LANGUAGE_VERSION) << std::endl << " + Vendor: " << glGetString(GL_VENDOR) << std::endl << " + Renderer: " << glGetString(GL_RENDERER) << std::endl << "--------------------------------------------------------" << std::endl; std::string s = sstring.str(); return s.c_str(); } }; #endif /* FPS_CTRLR_H_ */
En el ejemplo anterior. El array de vértices y de colores del triángulo los escribíamos dentro de la función, esta vez vamos a sacarlos para dejar un código mas claro. Veamos las principales diferencias en el ".h":
Lo primero a resaltar el Info_Manager, nuestra clase de utilidades.
Después nos encontramos con una variable aspect que guardará la relación de aspecto ancho-alto para la matriz de projección.
3 arrays para la información del cubo, tres constantes para facilitar la escritura de código, y 3 arrays, 2 de ellos buffer objects y uno de ellos vertex array object.
Las 3 últimas son información sobre el shader: nombre del programa y la localización de dos de sus variables.
Las funciones siguen siendo las mismas.
Pues sólo queda explicar el .cpp que como siempre es donde está lo realmente interesante.
Lo primero a resaltar el Info_Manager, nuestra clase de utilidades.
Después nos encontramos con una variable aspect que guardará la relación de aspecto ancho-alto para la matriz de projección.
3 arrays para la información del cubo, tres constantes para facilitar la escritura de código, y 3 arrays, 2 de ellos buffer objects y uno de ellos vertex array object.
Las 3 últimas son información sobre el shader: nombre del programa y la localización de dos de sus variables.
Las funciones siguen siendo las mismas.
#ifndef DEMO_OGL_II_H_ #define DEMO_OGL_II_H_ #include <iostream> #include <GL/gl3w.h> #include <GL/gl3.h> #include "SDL2/SDL.h" #include "../../LibsNUtils/vmath.h" #include "LoadShaders.h" #include "../Utils/Info_Manager.h" class Demo_OGL_2 { private: bool running; SDL_Window* window; SDL_GLContext ctxt; static const uint32_t WIN_HEIGHT = 768; //px static const uint32_t WIN_WIDTH = 1024; //px static const char* WIN_TITLE; //px Info_Manager info; /***************************************************/ /***************************************************/ /***************************************************/ float aspect; static const GLfloat cube_positions[]; static const GLfloat cube_colors[]; static const GLushort cube_indices[]; static const vmath::vec3 X; static const vmath::vec3 Y; static const vmath::vec3 Z; GLuint ebo[1]; GLuint vao[1]; GLuint vbo[1]; GLuint render_prog; GLuint render_model_matrix_loc; GLuint render_projection_matrix_loc; public: Demo_OGL_2(); /* * GAME LOOP FUNCTIONS */ int Execute(){ return OnExecute(); } bool Init(){ return OnInit(); } void Loop(){ return OnLoop(); } void Render(){ return OnRender(); } void Cleanup(){ return OnCleanup(); } void Event(SDL_Event* Event){ OnEvent(Event); } int OnExecute(); bool OnInit(); void OnEvent(SDL_Event* Event); void OnLoop(); void OnRender(); void OnCleanup(); /***************************************************/ /***************************************************/ /***************************************************/ void SetupOpenGL(); void InitData(); }; #endif /* DEMO_OGL_I_H_ */
#include <iostream> #include <string> #include <sstream> #include "Demo_OGL_2.h" #include "../../LibsNUtils/vmath.h" #include <GL/gl3w.h> #include <GL/gl.h> #include "LoadShaders.h" const char* Demo_OGL_2::WIN_TITLE = "Titulo de la Ventana"; const GLfloat Demo_OGL_2::cube_positions[] = { -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f }; const GLfloat Demo_OGL_2::cube_colors[] = { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.5f, 0.5f, 0.5f, 1.0f }; const GLushort Demo_OGL_2::cube_indices[] = { 0, 1, 2, 3, 6, 7, 4, 5, // First strip 0xFFFF, // <<-- This is the restart index 2, 6, 0, 4, 1, 5, 3, 7 // Second strip*/ }; const vmath::vec3 Demo_OGL_2::X(1.0f, 0.0f, 0.0f); const vmath::vec3 Demo_OGL_2::Y(0.0f, 1.0f, 0.0f); const vmath::vec3 Demo_OGL_2::Z(0.0f, 0.0f, 1.0f); Demo_OGL_2::Demo_OGL_2() : running(false), window(NULL), ctxt(NULL), info(), aspect(0), ebo(), vao(), vbo(), render_prog(0), render_model_matrix_loc(0), render_projection_matrix_loc(0){} void Demo_OGL_2::OnEvent(SDL_Event* event) { switch (event->type) { case SDL_KEYUP: switch(event->key.keysym.sym){ case SDLK_v: std::cout << info.client_info() << std::endl << std::endl; break; case SDLK_ESCAPE: running = false; break; default: break; } break; case SDL_QUIT: running = false; break; default: break; } } void Demo_OGL_2::OnLoop() {} void Demo_OGL_2::OnCleanup() { glUseProgram(0); glDeleteProgram(render_prog); glDeleteVertexArrays(1, vao); glDeleteBuffers(1, vbo); SDL_GL_DeleteContext(ctxt); SDL_DestroyWindow(window); SDL_Quit(); } bool Demo_OGL_2::OnInit() { if(SDL_Init(SDL_INIT_EVERYTHING) < 0) return false; window = SDL_CreateWindow(WIN_TITLE, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, WIN_WIDTH, WIN_HEIGHT, SDL_WINDOW_SHOWN | SDL_WINDOW_OPENGL); if(!window) return false; SetupOpenGL(); InitData(); running = true; return true; } void Demo_OGL_2::SetupOpenGL(){ SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 32); ctxt = SDL_GL_CreateContext(window); SDL_GL_SetSwapInterval(1); if (gl3wInit()) { std::cout << "Error al Inicializar GL3W" << std::endl; } } int Demo_OGL_2::OnExecute() { if (!Init()) return -1; SDL_Event event; while (running) { while (SDL_PollEvent(&event)) Event(&event); Loop(); Render(); if(info.frame()){ info.fps_ogl_ver_on_window(window, WIN_TITLE); } } Cleanup(); return 0; } void Demo_OGL_2::InitData(){ // Seleccionamos los shaders que queremos cargar ShaderInfo shaders[] = { { GL_VERTEX_SHADER, "Resources/Demo_OGL_II/primitive_restart.vs.glsl" }, { GL_FRAGMENT_SHADER, "Resources/Demo_OGL_II/primitive_restart.fs.glsl" }, { GL_NONE, NULL } }; // Cargamos los shaders render_prog = LoadShaders( shaders ); // Decimos a opengl que los utilice glUseProgram( render_prog ); // Seleccionamos la posicion dentro del shader de la matriz de modelado y la de proyeccion // Mas tarde vamos a usar esas posiciones para cambiar datos del cubo render_model_matrix_loc = glGetUniformLocation(render_prog, "model_matrix"); render_projection_matrix_loc = glGetUniformLocation(render_prog, "projection_matrix"); // Pedimos un buffer para el element buffer object glGenBuffers(1, ebo); // Le hacemos hueco diciendole el tipo de buffer glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo[0]); // Lo rellenamos con los indices de los cubos glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(cube_indices), cube_indices, GL_STATIC_DRAW); // Pedimos un array de vertices glGenVertexArrays(1, vao); // Le hacemos hueco glBindVertexArray(vao[0]); // Pedimos un buffer para el vertex buffer object glGenBuffers(1, vbo); // Le hacemos hueco glBindBuffer(GL_ARRAY_BUFFER, vbo[0]); // Le decimos que el hueco tiene que ser de tamaño "tamaño de cube positions"+"tamaño de cube colors" glBufferData(GL_ARRAY_BUFFER, sizeof(cube_positions) + sizeof(cube_colors), NULL, GL_STATIC_DRAW); // Y como lo tenemos en dos arrays diferentes lo guardamos poco a poco glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(cube_positions), cube_positions ); glBufferSubData(GL_ARRAY_BUFFER, sizeof(cube_positions), sizeof(cube_colors), cube_colors); // localizacion del atributo, numero de valores, tipo de valores, // normalizarlos, espacio entre valores, puntero al primer valor. // Por lo tanto decimos que nos pille los 4 primeros valores y // nos los meta en "posicion", glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, NULL); // Y apartir de cube positions que pille los colores. glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, (const GLvoid*)sizeof(cube_positions)); // Activamos los dos arrays de atributos glEnableVertexAttribArray(0); glEnableVertexAttribArray(1); //Seleccionamos el color de fondo glClearColor(0.0f, 0.0f, 0.0f, 1.0f); //Los triangulos que no se ven, ni los pinta (los triangulos que estan de espaldas) glEnable(GL_CULL_FACE); //Utiliza el z buffer glEnable(GL_DEPTH_TEST); //Funcion para el z-buffer glDepthFunc(GL_LESS); //lo que ocupa la parte en la que se dibuja. glViewport(0, 0, WIN_WIDTH, WIN_HEIGHT); aspect = float(WIN_HEIGHT) / float(WIN_WIDTH); } void Demo_OGL_2::OnRender() { // Un numero incremental en funcion del tiempo real float t = float(GetTickCount() & 0x1FFF) / float(0x1FFF); // Limpiamos el buffer de profundidad (se pone al valor por defecto) // y el de color (se pone al valor de glClearColor glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Decimos que Shader usar glUseProgram(render_prog); // Calculamos la matriz modelo vmath::mat4 model_matrix(vmath::translate(0.0f, 0.0f, -5.0f) * vmath::rotate(t * 360.0f, Y) * vmath::rotate(t * 720.0f, Z)); // Calculamos la matriz de proyección mediante un frustum vmath::mat4 projection_matrix(vmath::frustum(-1.0f, 1.0f, -aspect, aspect, 1.0f, 500.0f)); // Posicion en el shader, cantidad de matrices, es traspuesta? , matriz a setear. // Guardamos las dos matrices en el shader para que dibuje. // Al ser uniform el valor, se aprovecha entre los shaders glUniformMatrix4fv(render_model_matrix_loc, 1, GL_FALSE, model_matrix); glUniformMatrix4fv(render_projection_matrix_loc, 1, GL_FALSE, projection_matrix); // Activamos el vertex array Object glBindVertexArray( vao[0] ); // Activamos el buffer de indices glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo[0]); #define USE_PRIMITIVE_RESTART 1 #if USE_PRIMITIVE_RESTART // When primitive restart is on, we can call one draw command // Le decimos que active el reinicio de primitivas, glEnable(GL_PRIMITIVE_RESTART); // Este es el valor del indice que se toma para el reinicio glPrimitiveRestartIndex(0xFFFF); // Dibujamos como triangle strip (aprovechamos los dos vertices // anteriores para dibujar el siguiente triangulo) // un total de 17 indices y de tipo unsigned short sin offset (NULL) glDrawElements(GL_TRIANGLE_STRIP, 17, GL_UNSIGNED_SHORT, NULL); #else //Dibujamos un strip y otro. // Without primitive restart, we need to call two draw commands glDrawElements(GL_TRIANGLE_STRIP, 8, GL_UNSIGNED_SHORT, NULL); glDrawElements(GL_TRIANGLE_STRIP, 8, GL_UNSIGNED_SHORT, (const GLvoid *)(9 * sizeof(GLushort))); #endif // Buffer swap SDL_GL_SwapWindow(window); }
Edito: El eje Y (el vertical) crece hacia abajo, lo que llevaría los colores de la parte superior a la inferior y viceversa. No considero necesario cambiar el dibujo.
Lo siguiente es una lista de vértices para construir la forma, cuando veamos el glDrawElements explicaré porque es así.
Después viene la inicialización de tres constantes que básicamente representan los 3 vectores básicos (x y z)
OnEvent no se diferencia mucho del anterior, ahora cuando le damos a la v, sale información más detallada sobre el cliente.
OnLoop sigue vacío.
OnCleanUp sigue igual, borramos todo lo que usamos.
OnInit y SetupOpenGL siguen prácticamente igual, glClearColor lo hemos movido a InitData.
OnExecute ha sido ligeramente modificado, hemos añadido una comprobación, recordemos: con frame() informamos que se ha dibujado un frame y si es un frame clave nos dice "true". En ese caso cambiamos el título de la ventana. En nuestro caso debería quedar algo así:
Bueno, hasta aquí lo sencillo, casi copia y pega del anterior. Ahora viene lo "gracioso" :D.
InitData
- Los Shaders se cargan igual, los hemos puesto al principio para preparar la siguiente instrucción.
- Primera cosa interesante de este ejemplo: glGetUniformLocation, ésta función nos sirve para buscar la localización de una variable uniforme en nuestro programa de shaders. Y ahora... ¿Qué es una variable uniforme? Se podría decir que es el equivalente a una variable static de Java o C/C++ pero en GLSL. La modificas una sola vez y tiene el mismo valor en todas las ejecuciones hasta que se vuelva a modificar.Por eso, utilizamos como variables uniformes la matriz de proyección y la matriz del modelo. Las dos matrices se van a encargar de alterar lo que estamos viendo. Esta modificación se produce en todos los vértices y objetos, y les va a afectar por igual. Después con el ejemplo se verá mejor esto de la matriz modelo. En el caso de la matriz de proyección es muy claro: <<Imagínate cualquier escenario, tu habitación por ejemplo, y a ti con una cámara de fotos. Todos los elementos se encuentran en el mismo punto del escenario, sin embargo si te mueves en la habitación los elementos salen en distinto sitio en tu foto o distinta rotación (normalmente, no seáis trollers girando alrededor de una botella perfecta :P) la matriz de proyección capta una pirámide delante de la cámara y la usa para dibujar la escena, pero obviamente esta pirámide es igual para cualquier objeto, lo importante es la posición de ese objeto dentro o fuera de la piramide.>>En el renderizado veremos como se inicializan estos elementos.
- El siguiente paso se va a convertir en un clásico: generamos buffer, lo ligamos y lo rellenamos con información. ¿Qué tiene de especial? GL_ELEMENT_ARRAY_BUFFER que le informa del tipo de buffer que queremos ocupar y lo rellenamos con el array de índices.
- Lo siguiente es otro clásico, generar un array de vértices, ligarlo, pedir un buffer para los vértices, ligarlo y... ¡OJO! hacer hueco para la información. Si os fijáis donde debería ir el puntero al array con los datos hay NULL y reservamos espacio para las posiciones y para los colores.
¿Por qué? Hay dos maneras de guardar en el mismo buffer la información conjunta: - Creamos un sólo array y cuando vayamos a darle las instrucciones de como leer al shader, le decimos donde tiene que empezar y listo. Esto tiene un problema cuando quieres añadir nueva información, hay que modificar todo lo implicado.
- La solución correcta: por un lado haces hueco y por otro rellenas el hueco con la función glBufferSubData que nos permite rellenar por partes el buffer. La estructura de la función es casi la misma que la de glBufferData, con la diferencia de que hay que decirle dónde empieza la información.
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(cube_positions), cube_positions );
glBufferSubData(GL_ARRAY_BUFFER, sizeof(cube_positions), sizeof(cube_colors), cube_colors); - Lo siguiente son los punteros para asociar los valores de los buffer al programa GLSL. Esto ya esta explicado en el anterior tutorial si no os quedo claro, podéis volver a ello.
- glClearColor ya explicado también.
- glEnable(GL_CULL_FACE) es una optimización para evitar cálculos innecesarios.
- glEnable(GL_DEPTH_TEST) le decimos que use el test de profundidad, si desactivais esto, pueden salir cosas muy raras.
- Lo siguiente es la función que se utiliza para el z-buffer, si el valor de z es menor, se guarda en el buffer. Es decir dibujamos lo mas cercano como es lógico. Aquí tenéis una referencia al resto de posibles funciones.
- Establecemos el ancho y el alto del contexto y calculamos el aspect ratio que es una relación ancho alto para calcular la matriz de proyección.
Hasta aquí la inicialización "global" de la información. Ahora llega el momento del dibujado.
OnRender
- Lo primero es establecer una medida de tiempo. En primera instancia se nos podría ocurrir que en el ejemplo de mover un objeto desde A hasta B en el eje X, Aumentamos o disminuimos X de manera constante hasta llegar a B.
El problema a este planteamiento es el ordenador: Supongamos que el ordenador funciona perfectamente y va a 60 fps (tenemos la sincro vertical activada), eso quiere decir que aumentaríamos X 60 puntos. Hasta aquí bien, el problema llega cuando desactivamos la sincro vertical o el ordenador no es capaz de llegar a los 60 fps. Supongamos que otro ordenador paupérrimo intenta tirar nuestro juego o animación y solo es capaz de llegar a los 20 fps. Todo el juego le iría a un tercio de la velocidad a la que le iba al de 60 fps, esto aparte de un aburrimiento, podría suponer una ventaja o un inconveniente para el jugador. Supongamos un buen ordenador con la sincro vertical desactivada, podría llegar a correr el juego 10 o incluso 100 veces más rápido, en la mayoría de juegos, no se podría jugar. ¿Os imagináis jugar al plantas vs zombis 100 veces mas rápido? desaparecen los soles y los zombis entran hasta la cocina antes de que de tiempo de poner las plantas.
Por ello necesitamos una medida de tiempo: ¿Cuánto tiempo ha pasado desde el último fps? Eso es lo que hace la primera linea, como vamos a poner a dar vueltas el cubo, nos viene bien una variable "circular": cuando llega a un valor se reinicia. Esto lo conseguimos usando un and lógico y una división con respecto al tiempo (GetTickCount()). - glClear limpia los dos buffer de dibujado básicos.
- Informamos del shader que hay que usar para dibujar.
- Calculamos la matriz modelo: Básicamente hacemos la multiplicación de 3 matrices. vmath nos facilita estas operaciones (si alguien tiene curiosidad por como se hace, investigad que hay mucha teoría por internet, aquí no me voy a parar en eso). La matriz de traslación traslada el modelo (es decir, el cubo) 5 puntos hacia el fondo (Z) y después hacemos las rotaciones del cubo en el eje Z e Y. Es importantísimo el orden de multiplicación. La rotación modifica el eje del objeto. Si primero hiciésemos la rotación y después la traslación se trasladaría en otro eje diferente de Z con respecto al eje "global". Tendríamos un cubo haciendo una órbita en lugar de una rotación en el sitio.
- Calculamos la matriz de proyección: utilizamos el frustum [wikipedia en ingles]. Un frustum en informática gráfica 3D es una pirámide de base cuadrada que define el campo visual en cuyo vértice superior se situaría la cámara mirando en dirección y sentido a la base.
En un frustum hay que definir (con respecto al origen 2D de coordenadas de la ventana) las proporciones. En este caso hemos elegido mantener los margenes izquierdo y derecho constantes (1 y -1) y utilizamos la variable aspect que hemos calculado antes para que nos de la proporción correcta de altura.
Ademas de estos 4 valores hay que darle dos más: near y far.- Un frustum en realidad es una pirámide truncada, el punto de truncamiento lo define near. Es el punto mínimo para empezar "recolectar" objetos que salen en imagen. Lo que este antes de near, ni se calcula.
- far define el punto mas lejano que se puede avistar.
- Ahora guardamos los valores que hemos calculado en las localizaciones que hemos calculado en InitData.
- Activamos el array de vertices, activamos el buffer de elementos
- Procedemos a dibujar como tal, para ello quiero pararme en un par de conceptos:
- Hay varias maneras de dibujar en opengl. vimos en el primer ejemplo glDrawArrays que simplemente dibujaba lo que se encuentra en el buffer de vértices. Ahora vamos a utilizar glDrawElements que utiliza una lista de indices de vértices para dibujar. Es decir, si tenemos 10 vértices numerados del 0 al 9, glDrawArrays dibujaría del 0 al 9, en cambio, con glDrawElements podríamos dibujarlos así: 0, 5, 3, 2...
- Esto nos va a traer unas ventajas porque ademas vamos a utilizar el GL_TRIANGLE_STRIP para dibujar. ¿Qué significa eso? que dibuja dos vértices y el siguiente vértice conformara el primer triángulo como viene siendo lógico, pero el cuarto vértice creará el segundo triángulo con los dos últimos vértices utilizados. Ejemplo: tenemos 4 vértices dispuestos en forma de cuadrado. Suponiéndolos numerados en sentido de las agujas del reloj el orden de los vértices en nuestro array sería: 0, 1, 3, 2. Lo que se transformaría en dos triangulos: 0, 1, 3 y 1, 3, 2. Cosa que no podríamos conseguir con glDrawArrays si no cambiamos el orden de los vértices en nuestro array.
En 3 dimensiones tiene más gracia porque vamos a reutilizar todos los vértices para hacer el mínimo número de llamadas a funciones teniendo el mínimo número de vértices.
Ademas nos trae otra ventaja, cuando dibujamos dos triángulos contiguos, el render puede dejar en medio una linea sin colorear, es un bug gráfico común que mas de uno habrá apreciado en juegos. GL_TRIANGLE_STRIP evade esos huecos, la gráfica entiende que están unidos y no juntos y rellena el hueco. - Por último la activación de GL_PRIMITIVE_RESTART. Es una especie de "marca" que indica cuando tiene que volver a empezar a dibujar GL_TRIANGLE_STRIP. Hay que activarlo y decirle que marca es. Lo que hacemos es crear un "puzzle" de dos piezas que encajan a la perfección porque los vértices son los mismos.
La mejor manera de captarlo es visualmente: Los vértices los he enumerado entre corchetes y las lineas punteadas representan los triángulos que no forman parte de las aristas del cubo. - Bueno pues entendidos los conceptos tenemos los dos ejemplos, con reinicio y sin reinicio de primitivas:
- Con reinicio: 1º habilitamos el reinicio, 2º decimos que el indice de reinicio (nuestra marca) es 0xFFFF (un vértice es improbable que se use) y 3º dibujamos, le decimos el tipo de primitiva, el número de índices que tiene que leer (8+8+1 marca), el tipo de dato y un puntero al primer valor (NULL = primero).
- Sin reinicio: dibujamos 8 vértices y después los otros 8 empezando por el 9º.
- Por último cambiamos el buffer.
Shaders
Por otro lado tenemos los Shaders:
- Fragment Shader: simple como el solo, una variable de entrada y otra de salida que se comunican como si el shader fuera una simple tubería.
- Vertex Shader: Tiene la definición de las dos variables uniformes de las que hemos hablado largo y tendido. Las dos variables de entrada, y la variable de salida para comunicarse con el fragment shader.
En el main pasamos el color directamente y la posición de cada vértice la calculamos multiplicando las matrices que hemos calculado previamente. El orden de multiplicación también es importante, a si que os invito a probar calculadoras online de matrices que os dan una idea de como funciona el asunto.
Debería de quedar algo así:
Cosas que podéis probar como curiosidad
Os dejo cosas que podéis probar si os sentís curiosos antes de pasar al siguiente ejemplo, si os equivocáis con dejar las cosas como estaban funciona.
- Cambiar el frustum.
- Desactivar la sincro vertical y ver como se disparan los fps (SDL_GL_SetSwapInterval( 0 )); a mi se me pone en una media de 4500fps.
- Incrementar t sin que esté en función del tiempo para ver la diferencia con la sincro vertical activada y desactivada.
- Cambiar el orden de las multiplicaciones de matrices y los valores de posicionamiento para entender bien el sistema de traslación y rotación.
- EPIC: Intentar hacer un sistema de movimiento con WASD.
(Spoiler: en el siguiente ejemplo está hecho, si lo quieres intentar no avances) - ¡¡¡EPIC!!!: Intentar hacer niebla con el fragment shader.
PISTA: La clave está en Z (del Z-buffer), buscad en intenet las built-in variables de GLSL.
(Spoiler: en el siguiente ejemplo está hecho, si lo quieres intentar no avances)
Espero que os haya gustado, cuando pueda cuelgo el siguiente que ya tengo en github, aunque puede que lo divida en dos partes.
No hay comentarios:
Publicar un comentario