Esta técnica nos ayuda a dibujar muchas veces la misma pieza geométrica variando algunas cosas de ella como color, tamaño, posición... Se puede variar prácticamente todo pero, cuantas más cosas varíes, menos eficiente se vuelve.
Lo de dibujar muchas veces la misma pieza no es cuestión de ahorrarnos lineas de código (que ahora veréis que tampoco es un gran avance). El verdadero avance está en tiempo de ejecución, si nosotros ejecutamos un código algo así (pseudo-código):
for(int i = 0; i<1000; i++) DrawGeometry(geometry, position);
Esta haciendo 1000 peticiones a OpenGL de dibujar una geometría. En cambio, con esta técnica, hace únicamente una petición, y la gráfica (si lo soporta) dibuja las 1000 geometrías. El tiempo de hacer una petición es relativamente lento. Si pretendes ejecutar el código 60 veces por segundo y dibujar muchas veces la misma geometría, debes utilizar Geometry Instancing. Además el procesador de la tarjeta está especializado en cálculo en coma flotante y vectorial, por lo que las operaciones las hará más rápidamente que nuestro procesador (por muy i7-muchicore que sea y si nuestra gráfica es decente claro).
Vamos a poner unos ejemplos para ver que esto es útil y hay que utilizarlo.
- El ejemplo más claro es: Minecraft. Intenté, con un motor gráfico que no soportaba Geometry Instancing, montar un Minecraft sencillo. Hice un "supercubo" a base de cubos de superficie 10x10x10 (1000) cubos y me iba a 15 fps. Con el mismo ordenador utilizando esta técnica y 50x50x50 (125000) cubos me iba a 70-80 fps. Se puede apreciar la sustancial mejora. Ademas se pueden aplicar otras técnicas como "occlusion" que iremos viendo.
- Otro momento en el que se usa mucho esta técnica, es para dibujar elementos naturales: árboles, hierba, piedras... La hierba por ejemplo se dibuja igual en todos los sitios y se le aplica una modificación en función de una variable que pueda ser el aire, la gravedad...
- Soldados en una batalla: Si queremos montar una batalla con cientos de soldados, lo normal es que todos sean instancias del mismo soldado (o de 3 o 4) cambiando texturas o posturas.
Hay muchos más ejemplos pero estos son los más representativos que se me han ocurrido.
Vamos a dibujar cubos en cuadricula 5x5. Como siempre: .h y .cpp
Demo_OGL_3a.h:
/* * Demo_3.h * * Created on: 01/07/2013 * Author: Korgan */ #ifndef DEMO_OGL_IIIA_H_ #define DEMO_OGL_IIIA_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_3A { private: bool running; SDL_Window* window; SDL_GLContext ctxt; static const uint32_t WIN_HEIGHT = 600; //px static const uint32_t WIN_WIDTH = 1000; //px static const char* WIN_TITLE; //px static const int32_t INST_LENGTH = 5; static const int32_t INSTANCES = INST_LENGTH*INST_LENGTH; Info_Manager info; /***************************************************/ /***************************************************/ /***************************************************/ float aspect; static const GLfloat cube_positions[]; static const GLfloat cube_colors[]; static const GLfloat cube_rel_pos[INSTANCES*4]; 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[2]; GLuint render_prog; GLuint render_model_matrix_loc; GLuint render_projection_matrix_loc; GLfloat posX; GLfloat posY; GLfloat posZ; public: Demo_OGL_3A(); /* * 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(); void CheckErr(); }; #endif /* DEMO_OGL_I_H_ */
Dos nuevas constantes para especificar la cuadricula, como son 5 filas y 5 columnas, INSTANCES = 25 (5x5)
Siguiente constante nueva: cube_rel_pos. Un array que contiene las posiciones en las que vamos a ubicar los cubos.
Modificacion: vbo[2] Un nuevo vertex buffer para guardar cube_rel_pos en la memoria de ogl.
Las tres siguientes variables nuevas: posX, posY, posZ. Guardan la posición del observador. Esto sobretodo esta pensado para la parte B.
Por último CheckErr(), es una función sobretodo para debug. Si ocurre un error, lo suelta por la salida estándar de errores.
No me he querido entretener mucho en el .h porque no tiene ninguna complicación.
Vamos por partes como siempre, doy por hecho todo lo del anterior tutorial:Siguiente constante nueva: cube_rel_pos. Un array que contiene las posiciones en las que vamos a ubicar los cubos.
Modificacion: vbo[2] Un nuevo vertex buffer para guardar cube_rel_pos en la memoria de ogl.
Las tres siguientes variables nuevas: posX, posY, posZ. Guardan la posición del observador. Esto sobretodo esta pensado para la parte B.
Por último CheckErr(), es una función sobretodo para debug. Si ocurre un error, lo suelta por la salida estándar de errores.
No me he querido entretener mucho en el .h porque no tiene ninguna complicación.
/* * Demo_3.cpp * * Created on: 01/07/2013 * Author: Korgan * * mingw32 * glew32 * opengl32 * SDL2main * SDL2 * */ #include <iostream> #include <string> #include <sstream> #include "Demo_OGL_3a.h" #include "../../LibsNUtils/vmath.h" #include <GL/gl3w.h> #include <GL/gl.h> #include "LoadShaders.h" const char* Demo_OGL_3A::WIN_TITLE = "Titulo de la Ventana"; const GLfloat Demo_OGL_3A::cube_positions[] = { -0.3f, -0.3f, -0.3f, 1.0f, -0.3f, -0.3f, 0.3f, 1.0f, -0.3f, 0.3f, -0.3f, 1.0f, -0.3f, 0.3f, 0.3f, 1.0f, 0.3f, -0.3f, -0.3f, 1.0f, 0.3f, -0.3f, 0.3f, 1.0f, 0.3f, 0.3f, -0.3f, 1.0f, 0.3f, 0.3f, 0.3f, 1.0f }; const GLfloat Demo_OGL_3A::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 GLfloat Demo_OGL_3A::cube_rel_pos[] = { -2.0f, -2.0f, 0.0f, 1.0f, -1.0f, -2.0f, 0.0f, 1.0f, 0.0f, -2.0f, 0.0f, 1.0f, 1.0f, -2.0f, 0.0f, 1.0f, 2.0f, -2.0f, 0.0f, 1.0f, -2.0f, -1.0f, 0.0f, 1.0f, -1.0f, -1.0f, 0.0f, 1.0f, 0.0f, -1.0f, 0.0f, 1.0f, 1.0f, -1.0f, 0.0f, 1.0f, 2.0f, -1.0f, 0.0f, 1.0f, -2.0f, 0.0f, 0.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 2.0f, 0.0f, 0.0f, 1.0f, -2.0f, 1.0f, 0.0f, 1.0f, -1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 2.0f, 1.0f, 0.0f, 1.0f, -2.0f, 2.0f, 0.0f, 1.0f, -1.0f, 2.0f, 0.0f, 1.0f, 0.0f, 2.0f, 0.0f, 1.0f, 1.0f, 2.0f, 0.0f, 1.0f, 2.0f, 2.0f, 0.0f, 1.0f, }; const GLushort Demo_OGL_3A::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_3A::X(1.0f, 0.0f, 0.0f); const vmath::vec3 Demo_OGL_3A::Y(0.0f, 1.0f, 0.0f); const vmath::vec3 Demo_OGL_3A::Z(0.0f, 0.0f, 1.0f); Demo_OGL_3A::Demo_OGL_3A() : 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), posX(0.0f), posY(0.0f), posZ(-5.0f){} void Demo_OGL_3A::OnEvent(SDL_Event* event) { switch (event->type) { case SDL_KEYUP: switch(event->key.keysym.sym){ case SDLK_v: std::cout << std::endl; std::cout << info.client_info() << std::endl; break; case SDLK_ESCAPE: running = false; break; default: break; } break; case SDL_QUIT: running = false; break; default: break; } } void Demo_OGL_3A::OnLoop() {} void Demo_OGL_3A::OnCleanup() { glUseProgram(0); glDeleteProgram(render_prog); glDeleteVertexArrays(1, vao); glDeleteBuffers(2, vbo); SDL_GL_DeleteContext(ctxt); SDL_DestroyWindow(window); SDL_Quit(); } bool Demo_OGL_3A::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_3A::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_3A::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_3A::InitData(){ ShaderInfo shaders[] = { { GL_VERTEX_SHADER, "Resources/Demo_OGL_III/geometry_instancingA.vs.glsl" }, { GL_FRAGMENT_SHADER, "Resources/Demo_OGL_III/simple.fs.glsl" }, { GL_NONE, NULL } }; render_prog = LoadShaders( shaders ); glUseProgram( render_prog ); render_model_matrix_loc = glGetUniformLocation(render_prog, "model_matrix"); render_projection_matrix_loc = glGetUniformLocation(render_prog, "projection_matrix"); glGenBuffers(1, ebo); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo[0]); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(cube_indices), cube_indices, GL_STATIC_DRAW); glGenVertexArrays(1, vao); glBindVertexArray(vao[0]); glGenBuffers(2, vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo[0]); glBufferData(GL_ARRAY_BUFFER, sizeof(cube_positions) + sizeof(cube_colors), NULL, GL_STATIC_DRAW); glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(cube_positions), cube_positions ); glBufferSubData(GL_ARRAY_BUFFER, sizeof(cube_positions), sizeof(cube_colors), cube_colors); glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, NULL); glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, (const GLvoid*)sizeof(cube_positions)); glEnableVertexAttribArray(0); glEnableVertexAttribArray(1); /************************************** * GEOMETRY INSTANCING **************************************/ glBindBuffer(GL_ARRAY_BUFFER, vbo[1]); glBufferData(GL_ARRAY_BUFFER, sizeof(cube_rel_pos), cube_rel_pos, GL_STATIC_DRAW); glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, 0, 0); glEnableVertexAttribArray(2); glVertexAttribDivisor(2, 1); /************************************** * END GEOMETRY INSTANCING **************************************/ glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glEnable(GL_CULL_FACE); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); glViewport(0, 0, WIN_WIDTH, WIN_HEIGHT); aspect = float(WIN_HEIGHT) / float(WIN_WIDTH); CheckErr(); } void Demo_OGL_3A::OnRender() { float t = float(GetTickCount() & 0x1FFF) / float(0x1FFF); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glUseProgram(render_prog); // Calculamos la matriz modelo vmath::mat4 model_matrix(vmath::translate(posX, posY, posZ) * vmath::rotate(t * 360.0f, Y) * vmath::rotate(t * 720.0f, Z)); vmath::mat4 projection_matrix(vmath::frustum(-1.0f, 1.0f, -aspect, aspect, 1.0f, 500.0f)); glUniformMatrix4fv(render_model_matrix_loc, 1, GL_FALSE, model_matrix); glUniformMatrix4fv(render_projection_matrix_loc, 1, GL_FALSE, projection_matrix); glBindVertexArray( vao[0] ); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo[0]); #define USE_PRIMITIVE_RESTART 1 #if USE_PRIMITIVE_RESTART glEnable(GL_PRIMITIVE_RESTART); 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) // dibujamos tantos cubos como ponga en INSTANCES glDrawElementsInstanced(GL_TRIANGLE_STRIP, 17, GL_UNSIGNED_SHORT, NULL, INSTANCES); #else //Dibujamos un strip y otro. // Without primitive restart, we need to call two draw commands glDrawElementsInstanced(GL_TRIANGLE_STRIP, 8, GL_UNSIGNED_SHORT, NULL, INSTANCES); glDrawElementsInstanced(GL_TRIANGLE_STRIP, 8, GL_UNSIGNED_SHORT, (const GLvoid *)(9 * sizeof(GLushort)), INSTANCES); #endif // Buffer swap SDL_GL_SwapWindow(window); } void Demo_OGL_3A::CheckErr() { GLenum err = glGetError(); if ( err != GL_NO_ERROR ) std::cerr << "Error: " <<err; }
- cube_positions, cube_colors, cube_indices, X, Y y Z ya están explicados del anterior tutorial. cube_positions x, y & z se han ajustado a 0.3 en lugar de 1.
- cube_rel_pos guarda las posiciones de cada cubo con respecto al cubo inicial. El cubo inicial esta dibujado en el centro del eje de coordenadas. Echándole un poco de imaginación con las coordenadas podemos imaginarnos como van a estar situados: 5 filas y 5 columnas empezando por la posición -2 hasta la 2 en las coordenadas X e Y. La Z está a 0 porque vamos a dibujarlas sólo en el plano XY.
- Constructor: lo único que considero destacable es la inicialización de Z a -5. Movemos la posición del observador/cámara, alejándonos de los cubos para poder verlos. (Si a alguien se le olvida esto que no diga que no ve nada xD)
- Hasta InitData las funciones no han sufrido cambios salvo OnCleanup que borramos (lógicamente) 2 vbo en lugar de 1.
- InitData: Configuramos los shaders, configuramos el ebo, el vao, y a la hora de configurar el vbo pedimos 2 buffers en lugar de 1: glGenBuffers(2, vbo);
Configuramos el primero de los buffers igual que lo hicimos en el anterior con el color y las posiciones de los vertices y a partir del comentario empieza la configuración de la parte de Instancing.
Como veis las primeras 4 lineas son IGUALES que siempre, utilizando el vbo[1] (que es nuestro nuevo buffer) y habilitando con el location adecuado, como siempre. Lo nuevo es añadir la 5º linea: glVertexAttribDivisor(2, 1);
El primer parámetro es location y el segundo el número de instancias en las que se va a repetir el valor. Por lo general voy a usar 1, pero podría usarse cualquier número mayor que 0. En el caso de 0 se utiliza un valor por vértice (como se usa habitualmente).
Ésta función indica cada cuantas instancias tiene que cambiar el valor. Es decir, nosotros hemos dicho que dibuje 25 instancias del cubo, y tenemos 25 posiciones, pero va a dibujar 8 vértices por cubo. Necesitamos que cada 8 vértices, cambie la posición. Si la cambiase en cada vértice (como se hace normalmente), nos saldría una masa amorfa, cosa que no queremos. Eso es lo que hace glVertexAttribDivisor, decirle que cada 8 vértices cambie la posición del cubo.
Esta foto, aunque hay un poco de lío de líneas, pretende ilustrar cómo todos los cubos usan los 8 valores de los dos buffers azules y usan únicamente un valor del buffer rojo cada uno.
El resto de líneas de la función las conocemos, por lo tanto, las paso por alto. - OnRender(): no ha cambiado demasiado, voy a las lineas que han cambiado:
- model_matrix ahora la calculamos a partir de posX posY y posZ. Nada especial, igual que si pusieramos (0.0f, 0.0f, -5.0f)
- glDrawElementsInstanced(GL_TRIANGLE_STRIP, 17, GL_UNSIGNED_SHORT, NULL, INSTANCES);
Esta función es clave. Es igual que la anterior añadiendo el número de instancias que hay que dibujar. - Las llamadas con USE_PRIMITIVE_RESTART "off" son iguales tambien pero con INSTANCES.
Ahora los Shaders. Sólo os voy a hablar del vertex shader, el fragment shader es igual que el anterior.
geometry_instancingA.vs.glsl:
#version 330 uniform mat4 model_matrix; uniform mat4 projection_matrix; layout (location = 0) in vec4 position; layout (location = 1) in vec4 color; // per instance attribute layout (location = 2) in vec4 translation; out vec4 vs_fs_color; void main(void) { mat4 trans = mat4( 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, translation); vs_fs_color = color; gl_Position = projection_matrix * ( (trans * model_matrix) * position) ; }
Tenemos un atributo nuevo, al que he llamado translation, es al que asignamos cube_rel_pos. No tiene nada especial, es un atributo igual que position y color.
En el main tenemos la matriz de traslación en la cual "cargamos" la matriz identidad y añadimos nuestro vector de traslación en la última linea.
La posición se calcula igual que se calculaba en el anterior ejemplo multiplicando la matriz modelo por la matriz de traslación.
Como veis no hay nada de especial en Geometry Instancing dentro de los shaders. Es OpenGL el que se encarga de hacer el "trabajo sucio".
Debería de saliros algo como esto:
Esto es todo, como veis hay más de concepto que de código. Con respecto a lo anterior son "un par de cambios".
Ésta es la primera parte del ejemplo 3. En cuanto tenga tiempo cuelgo la siguiente parte. Aprendemos a movernos con WASD y probaremos cuantos cubos soporta nuestro ordenador con Geometry Instancing.
No hay comentarios:
Publicar un comentario