divendres, 28 de gener del 2011

Guia ràpida d'OpenGL (ES)

Molts bons el que sigui.

He estat llegint un llibre d'OpenGL ES 2.0, la versió per sistemes integrats d'OpenGL. La gràcia (explicat ràpidament) de la versió ES és que és essencialment el mateix que la versió "mare" però simplificada, o sigui, amb totes les funcions "redundants" eliminades. Un programa fet doncs, en OpenGL ES és pràcticament un 100% portable a OpenGL normal (excepte que les extensions són diferens, petits detallets).

Què vui fer jo ara? Escriure'm un resumet del llibre, ja que l'he de tornar a la biblioteca eventualment.

Així que, què, saveu com va OpenGL, més o menys? El de veritat, el dels Shaders, eh!

Essencialment, el que un ha de fer perque es dibuixi alguna cosa és especificar un seguit de geometria (Triangles, línees o "punts"), que recorre un seguit de transformacions fins que són pintades. Quines transformacions?

Pipeline:
  1. Vertex Shader: Agafa paràmetres que li dónes directament i les passes a la següent fase. El paràmetre important a passar són les coordenades de "clip" (coordenades de retallada), que essencialment són les coordenades dels vertexos normalitzades.
  2. Rasterització: Es divideix en diferents fases, l'objectiu és trobar els fragments que faràn el "Fragment Shader.
    1. Divisió de perspectiva: S'agafen les coordenades de clip (xc, yc, zc, wc) i es transformen a coordenades de Device (xd, yd, zd) = (xc/wc, yc/wc, zc/wc). Aquelles coordenades fóra del rang [-1, 1] són descartades.
    2. Transformació de Viewport: Transforma les anteriors coordenades a coordenades de pantalla (les x i y) i també transforma proporcionalment les z a un rang que tu mateix has especificat.
    3. Rasterització: Un cop tens cada vèrtex situat en un pixel, "s'envien" tots els pixels intermitjos al Fragment Shader. Prèviament es fa el culling (Eliminar els triangles que estiguin mirant en contra la càmera.
  3. Fragment Shader: Agafa paràmetres interpolats del Vertex Shader i els utilitza per definir el color del fragment. A més disposa de les coordenades del fragment en pantalla, les coordenades dels point-sprites, i si la cara mira o no a càmera.
  4. Operacions per fragment: També n'hi ha vàries.
    1. Scissor Testing: Fa que només es pintin pixels definits en un rectangle definit.
    2. Stencil Test: No ho he entès mai, ni mai he vist cap exemple de ningú fent-ho servir.
    3. Depth Test: Decideix si un fragment es dibuixa o no segons la seva profunditat.
    4. Blending: Barreja el color amb el color anterior.
    5. Dithering: Tampoc ho he fet servir mai.
Coses Ràpides que cal saber de shaders:

Els Shaders tenen 3 tipus de "variables globals":
  • Uniform: Són paràmetres que comparteix tota la geometria, on la geometria és allò que dibuixes d'un cop, un model, vamos. Exemples serien les matrius de transformació, les llums, etc...
  • Attribute: Són paràmetres per vèrtex. Per exemple Coordenades-Objecte, Normals, coordenades de textura i altres.
  • Varying: Aquestes s'escriuen al Vertex Shader i es llegeixen al Fragment Shader. És la manera de passar informació de l'una a l'altre.
Cada shader es defineix amb una funció "main" de tota la vida.

Com es creen els shaders? "Fàcil"!

Primer cal crear ambdós shaders:


//---------------------------------------------------------
GLenum type; //GL_VERTEX_SHADER | GL_FRAGMENT_SHADER

GLuint shader_object = glCreateShader(type);

glShaderSource( shader_object, 
                count,   // número d'strings que passes
                strings, // array de char*, amb el codi
                lenghts);// array de int, amb la llargada de cada
                         // string. Si és NULL se suposa que
                         // acaven en '\0'. Si un element és
                         // negatiu se suposa que aquell acava
                         // en '\0'.

glCompileShader( shader_object );
glGetShaderiv( shader_object,
               param,    // GL_COMPILE_STATUS
                         // GL_DELETE_STATUS
                         // GL_INFO_LOG_LENGTH
                         // GL_SHADER_SOURCE_STATUS
                         // GL_SHADER_TYPE
               &result);

//Comprovar que el compile status sigui true, sino agafar el log
if(!result) {
  int len;
  glGetShaderiv( shader_object, GL_INFO_LOG_LENGTH, len);

  char* infoLog = new char[len];
  glGetShaderInfoLog(shader_object,
                     len,
                     NULL, //llargada, com que ja la savem, null
                     infoLog);

  printf("Log: %s", infoLog);
}

//Eventualment
glDeleteShader(shader_object);
//---------------------------------------------------------

Un cop creats els shaders (Vertex i Fragment), cal lligar-los:

//---------------------------------------------------------
GLuint vertex_shader, fragment_shader;

GLuint shader_program = glCreateProgram();

glAttachShader(shader_program, vertex_shader);
glAttachShader(shader_program, fragment_shader);

//glDetachShader( ... )
glLinkProgram(shader_program); //Fer el link de les "coses"

//glValidateProgram(shader_program) serveix per validar i agafar
//info per el log, però és lent i només s'ha d'usar per "debugar"
//i buscar errors.

glProgramiv( ... ); 
//similar al de shader objects, amb paràmetres
//GL_ACTIVE_ATTRIBUTES             --nombre d'atributs
//GL_ACTIVE_ATTRIBUTES_MAX_LENGTH  --len de l'atribut més llarg

//GL_ACTIVE_UNIFORMS               -- "
//GL_ACTIVE_UNIFORMS_MAX_LENGTH    -- "
//GL_ATTACHED_SHADERS              -- nombre de shaders afegits
//GL_DELETE_STATUS                 -- si es vol eliminar
//GL_INFO_LOG_LENGHT               --
//GL_LINK_STATUS                   -- si s'ha linkad bé
//GL_VALIDATE_STATUS               -- si ha validat

glGetProgramInfoLog( ... ); // com el dels objectes.

//Eventualment
glDeleteProgram( shader_program );
//---------------------------------------------------------

Un cop tenim els shaders compilats i tot, cal agafar les localitzacions dels "uniforms". Amb les localitzacions serem capaços d'escriure aquests valors. "Passar-los" al shader.

Hi ha una funció, glGetActiveUniform, que serveix per saber quina "uniform" hi ha en cada posició i quin tipus té, però aquesta no és la manera normal de buscar-ho (ja sabem quines "uniform" hi ha en el shader, l'hem escrit nosaltres!).

GLint uniformLocation = glGetUniformLocation( shader_program, "nom_uniform");

Així tenim la localització, que podem fer servir així:

void glUniform1f(GLint location, GLfloat v0);

Coses a tenir en compte: Per fer-ho servir el shader ha d'estar "actiu" (més, després) i tmb que n'hi ha moltes, de funcions, i es troben a [1].

Els atributs s'agafen de manera similar.

GLint attributeLocation = glGetAttributeLocation( shader_program, "nom_attribute");

Com preparar la "Geometria"

No m'estaré de métodes arcaics per definir geometria i aniré directament al métode més "eficient".

Conceptualment es tracta de pujar a memória VRAM, memòria de la targeta gràfica, totes les dades necessàries per dibuixar la geometria, i posteriorment cridar-la quan calgui. L'algoritme vé a ser el següent:
  1. En una fase de "init", definir i omplir els buffers.
  2. En una fase de "render":
    1. Activar el Shader que s'utilitza ( glUseProgram(program_id); ).
    2. Definir totes les Uniforms que fa servir el Shader
    3. Fer la crida de "Pinta la geometria".
  3. En una fase de "cleanup" eliminar els buffers.
Què són i com funcionen els buffers? Imaginem-nos que volem renderitzar el següent cuadrat:

[0] ----- [1]
 |         |
 |         |
 |         |
[2] ----- [3]

Aquest té 4 vertexos, amb coordenades [0 => (0, 5, 0)], [1 => (5, 5, 0)], [2 => (0, 0, 0)], [3 => (5, 0, 0)], i amb coordenades de textura [0 => (0, 1)], [1 => (1, 1)], [2 => (0, 0)], [3 => (1, 0)]. Podríem fer un array de la següent manera:

float vertex_data[] = {0, 5, 0, 0, 1, //primer vertex
                       5, 5, 0, 1, 1,
                       0, 0, 0, 0, 0,
                       5, 0, 0, 1, 0  //últim vertex
                      };

Per indicar com s'ha de dibuixar, es pot fer indexat. Primer de tot, cal dividir la geometria en triangles, podem fer 2 triangles en aquest cas: el {0, 2, 1} i el {1, 2, 3}. Així li podem indicar a OpenGL que dibuixi la geometria {0, 2, 1, 1, 2, 3}.

uint16 index_data[] = {0, 2, 1, 1, 2, 3};

Abdós buffers es poden guardar a VRAM i posteriorment activar i utilitzar.

Creant Buffers

Aqui expliquem com crear els buffers, o sigui, la part de "init"


//----------------------------------------------------------
GLuint bufferIds[2];

glGenBufferData(2, bufferIds); //Generem 2 buffers

glBindBuffer(GL_ARRAY_BUFFER, bufferIds[0]); //activem el buffer
                                             //com a buffer de
                                             //dades
glBufferData(GL_ARRAY_BUFFER,   
             numVert * sizeVert, //Tamany que tindran les dades
             vertex_data,        //Dades
             GL_STATIC_DRAW);    //Mode. Li dóna pistes a la gràfica
             //GL_STATIC_DRAW  -- escrit 1, usat molt
             //GL_DYNAMIC_DRAW -- escrit molt, usat molt
             //GL_STREAM_DRAW  -- escrit 1, usat poc

           //GL_ELEMENT... serveix per indexos
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bufferIds[1]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, size, index_data, GL_STATIC_DRAW);

//----------------------------------------------------------
Com a nota adicional, sempre es pot re-crear les dades amb "glBufferData" o canviar una part amb "glBufferSubData" (tot i que si l'has creat amb "STATIC_DRAW" puteges al driver).

Emprant Buffers

Aqui expliquem com renderitzar usant buffers, o sigui la crida a "pintar geometria".


//-------------------------------------------------------------

  //Activem el buffer de vertexos que farem servir
glBindBuffer(GL_ARRAY_BUFFER, vertexBufferId);

  //Activem el buffer d'indexos que farem servir
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vertexBufferId);


  //activem els atributs del shader
glEnableVertexAttribArray(position_attrib); 
glEnableVertexAttribArray(texcoord_attrib); 

glVertexAttribPointer(position_attrib,  // atribut que usem
                      3,         //nº de dades a l'atribut
                      GL_FLOAT,  //tipus de dades
                      GL_FALSE,  //normalització
                      vtxStride, //distancia entre un vtx
                                 //  i el següent
                      0);        //distancia entre l'inici del
                                 //  vtx i les dades en qüestió.     

     /**********************************************************\
     *   Sobre la normalització, serveix per quan usem un tipus *
     * de dades diferent a float i volem que al passar-les a    *
     * float OpenGL les transformi al rang [-1, 1] o no.        *
     \**********************************************************/

     /**********************************************************\
     *   Sobre els tipus de dades, poden ser GL_BYTE,           *
     * GL_UNSIGNED_BYTE, GL_SHORT, GL_UNSIGNED_SHORT, GL_FIXED, *
     * GL_FLOAT, GL_HALF_FLOAT_OES/GL_HALF_FLOAT_ARB            *
     \**********************************************************/

glVertexAttribPointer(texcoord_attrib,

                      2,        
                      GL_FLOAT, 

                      GL_FALSE, 

                      vtxStride,
                      sizeof(float)*3);

glDrawElements(GL_TRIANGLES,      //Tipus de primitiva
               6,                 //nº de indexos
               GL_UNSIGNED_SHORT, //tipus de dades
               0);                //distància entre l'inici del 
                                  //  buffer i el primer index. 

// Tipus de primitives:
//GL_POINTS, GL_LINES, GL_LINE_STRIP, GL_LINE_LOOP,
//GL_TRIANGLES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN

// Tipus de dades als indexos
//GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT, GL_UNSIGNED_INT
//La darrera necessita de l'extensió OES_element_index_uint
       
glDisableVertexAttribArray(position_attrib); 
glDisableVertexAttribArray(texcoord_attrib); 
//-------------------------------------------------------------

Textures:

Les textures en OpenGL sempre han estat un petit percal. No m'extendré massa, però coses "importants":


//----------------------------------------------
glGenTextures(num, &id); //genera textures
glDeleteTextures(num, &id); //ejem

  //Activar textura
glBindTexture(tipus, //GL_TEXTURE_2D, GL_TEXTURE_CUBE_MAP
              id);

  //Definir la informació
glTexImage(capa,
           nivell,         //Nivell de mipmap. Deixeu-lo a 0
           internalFormat, //format intern
           width,         
           height,
           border,         //en OpenGL ES, 0.
           externalFormat, //cal que sigui el mateix que l'intern.
           dataType,       //tipus de dades
           data);          //dades, per "files"

// Capa:
// Per textures 2D, GL_TEXTURE_2D
// Per Cubemap s'han de definir totes 6 cares:
// GL_TEXTURE_CUBE_MAP_POSITIVE_X, ...NEGATIVE_X, ...

// Formats:
//GL_RGBA, GL_RGB, GL_LUMINANCE_ALPHA, GL_LUMINANCE, GL_ALPHA
//Pensar en la luminance com a "escala de grisos"

// Tipus de dades:
//GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT_4_4_4_4,
//GL_UNSIGNED_SHORT_5_5_5_1, GL_UNSIGNED_SHORT_5_6_5

  //Definir com es filtra la imatge quan es fa més gran.
glTexParameteri( tipus,
                 GL_TEXTURE_MAG_FILTER,
                 param); //GL_NEAREST / GL_LINEAR

  //Definir com es filtra la imatge quan es fa més petita.
glTexParameteri( tipus,
                 GL_TEXTURE_MIN_FILTER,
                 param); 
              //GL_NEAREST, GL_LINEAR
              //GL_NEAREST_MIPMAP_NEAREST x4 combinacions
//Definir què fa una textura quan té una coordenada fora de [0,1]
glTexParameteri( tipus,
                 GL_TEXTURE_WRAP_S, //O t
                 GL_REPEAT); //GL_CLAMP_TO_EDGE, GL_MIRRORED_REPEAT



glGenerateMipmap(tipus); //Genera automàticament un mipmap per la 
                         //textura activa

//Finalment parlem de com agafar una textura des del shader.
//Per tal de fer-ho, necessitem una "uniform" especial, anomenada
//"sampler" ("sampler2D" o "samplerCube")

//Activem la textura en un la unitat 0:
glActiveTexture(GL_TEXTURE0); //GL_TEXTURE1, ...
glBindTexture(tipus, textureId);

//Fem que el sampler apunti a la unitat 0
glUniform1i(sampler_uniform, 0);

//----------------------------------------------
També hi ha un altre tipus de textura, la textura3D, que és estandard a OpenGL i a OpenGL ES necessita de l'extenció "GL_OES_texture_3D". Funciona pràcticament igual amb una funció extra per carregar-la "glTexImage3D"[OES] amb un paràmetre adicional indicant la profunditat.

Adicionalment podem crear textures de "profunditat" si l'extenció "GL_OES_depth_texture" està disponible. Això serveix essencialment amb el tema dels pBuffers (vist més endavant) i es creen amb un format "GL_DEPTH_COMPONENT" i tipus "GL_UNSIGNED_SHORT" o "GL_UNSIGNED_INT" si estàn disponibles.


Continuaré en un altre moment. Falta per fer:

  • pBuffers
  • Vertex Shaders
  • Fragment Shaders
  • Operacions post-shader
  • Exemples

[1] http://www.opengl.org/sdk/docs/man/xhtml/glUniform.xml