dissabte, 5 de febrer de 2011

Suficiència d'OpenGL ES 2.0 Part II (Framebuffers)

Continuant el post anterior, i una mica en la mateixa línea, avordem un tema "important", sobretot si es tracta de fer efectes abançats: Els Framebuffers.

D'acord, primer de tot, necessitem entendre una mica de teoria sobre on dibuixem les coses i quins espais de memòria ens són disponibles.

Aquests espais de memòria als que em refereixo són els buffers de renderització; allà on guardem informació de color, profunditat, etc... En l'exemple més senzill de OpenGL, disposem de 2 buffers: el front buffer i el back buffer. Aquests buffers guarden informació de color (RGB i a vegades un Alpha que indica "quantitat d'opacitat"), el front buffer es mostra per pantalla mentre que el backbuffer és on dibuixem. Un cop acavem de dibuixar un frame, intercanviem el front i el back, així no es veu mai "mentre dibuixem" sinó que veiem sempre dibuixos sencers.

Addicionalment se li poden afegir 2 buffers: el depth buffer i el stencil buffer. Sobre l'stencil no en parlarè gaire, per no dir gens; mai l'he fet servir i no sé ben bé per a què serveix, però el depth buffer és molt útil. Aquest búffer de profunditat (depth) guarda la profunditat on se suposa que cada pixel està pintat. Això ens permet un mètode senzill per renderitzar les coses que estàn més a prop: si anem a dibuixar una cosa més llunyana que la que hi ha dibuixada, no la dibuixem. Si és més propera, la dibuixem i actualitzem el depth.

Molt bé, ara veiem que per renderitzar necessitem un buffer de color i, opcionalment, un de profunditat i/o un de stencil. Per defecte normalment en tenim un de cada (més el front buffer), però a vegades ens interessa dibuixar coses "fora de la pantalla". Per exemple: Imaginem-nos que estem en un joc, en una sala amb càmeres de videovigilància. Aquestes càmeres mostren, en temps real, altres habitacions. Per a acomplir aquest efecte, podem renderitzar aquestes altres habitacions en una textura cada una i, posteriorment, renderitzar la textura a la càmera.

Això és el que es coneix com a off-screen rendering, i consisteix en crear un "Framebuffer object", que és una col·lecció de buffers de distínta índole, i dir-li a l'openGL que renderitzi sobre aquest. Els buffer poden ser de 2 maneres: o bé una textura, o bé un Renderbuffer. La diferència essencial és que no tenim fàcilment accessible la informació dels Renderbuffers, mentre que una textura la tenim sempre a mà.

Codi:

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

//Primer creem els Renderbuffers, que poden servir com a color,
//profunditat o stencil
GLuint renderbuffers[2];
glGenRenderbuffers(2, &(renderbuffers[0]));

//activem el renderbuffer
glBindRenderbuffer(GL_RENDERBUFFER, renderbuffers[0]);

//definim com és el renderbuffer
glRenderbufferStorage(GL_RENDERBUFFER,
                      format,         //Format intern
                      width, height); //dimensions

// Formats estàndard:
// GL_RGB565, GL_RGBA4, GL_RGB5_A1,         -- Color
// GL_DEPTH_COMPONENT16, GL_STENCIL_INDEX8
//
// Formats amb l'extensió GL_OES_rgb8_rgba8
// GL_RGB8_OES, GL_RGBA8_OES
//
// Formats amb l'extensió GL_OES_depth24 / GL_OES_depth32
// GL_DEPTH_COMPONENT24_OES, GL_DEPTH_COMPONENT32_OES
//
// Formats amb l'extensió GL_OES_stencil1 / GL_OES_stencil4
// GL_STENCIL_INDEX1, GL_STENCIL_INDEX4
//
// Formats amb l'extensió GL_OES_packed_depth_stencil
// GL_DEPTH24_STENCIL8_OES

// Les dimensions cal que siguin iguals o menors a
// GL_MAX_RENDERBUFFER_SIZE
 
glBindRenderbuffer(GL_RENDERBUFFER, renderbuffers[0]);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA4, 800, 600);
glBindRenderbuffer(GL_RENDERBUFFER, renderbuffers[1]);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, 800, 600);



//Crear identificadors de frambuffer,
// de forma similar a les textures:
GLuint framebufferID;
glGenFramebuffers(1,                //nombre d'id a generar
                  &framebufferID);

// Activar el framebuffer
glBindFramebuffer(GL_FRAMEBUFFER, framebufferID);

// Aquesta funció ens serveix tant per especificar les propietats
//del framebuffer (punts d'anclatge) com per indicar que volem
//que es renderitzi a aquest. Per tornar a renderitzar als buffer
//"normals" hem de fer:
glBindFramebuffer(GL_FRAMEBUFFER, 0);

// Lligar un renderbuffer al framebuffer
gl_FramebufferRenderbuffer(GL_FRAMEBUFFER,
                           punt_d_anclatge,
                           GL_RENDERBUFFER,
                           renderbufferID); //0 per no anclar-hi res

// Punts d'anclatge:
// GL_COLOR_ATTACHMENT0, GL_DEPTH_ATTACHMENT, GL_STENCIL_ATTACHMENT
// Com a nota important, saber que en OpenGL estàndard hi ha més
//punts d'anclatge, concretament, els de color 1, 2, 3... Això 
//serveix per poder pintar a més d'un lloc a la vegada, i ho 
//explicarè als fragment shaders.

//Continuant amb l'exemple
gl_FramebufferRenderbuffer(GL_FRAMEBUFFER,
                           GL_COLOR_ATTACHMENT0,
                           GL_RENDERBUFFER,
                           renderbuffers[0]);
gl_FramebufferRenderbuffer(GL_FRAMEBUFFER,
                           GL_DEPTH_ATTACHMENT,
                           GL_RENDERBUFFER,
                           renderbuffers[1]);
gl_FramebufferRenderbuffer(GL_FRAMEBUFFER,
                           GL_STENCIL_ATTACHMENT,
                           GL_RENDERBUFFER,
                           0);

//Comprovar errors
GLenum result = glCheckFramebufferStatus(GL_FRAMEBUFFER);

//Result pot ser:
// GL_FRAMEBUFFER_COMPLETE -- OK
// GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT -- Algun error
//                                      -- en algun anclatge.
// GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT -- No hi ha cap
//                                              --anclatge
// GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS -- Anclatges amb
//                                      -- dimensions diferents
// GL_FRAMEBUFFER_INCOMPLETE_FORMATS -- Algun format no suportat
// GL_FRAMEBUFFER_UNSUPPORTED -- Combinació d'anclatges i formats
//                            -- no suportats.


// Com s'ha dit anteriorment, la gràcia de tot plegat és la de
//dibuixar a una textura, per això anclem una textura al 
//framebuffer com ho anclavem un renderbuffer
glFramebufferTexture2D(GL_FRAMEBUFFER,
                       punt_d_anclatge, //Només color o profunditat
                       target,
                       texture_id,
                       0); //nivell mipmap.

// Targets:
// GL_TEXTURE_2D, GL_TEXTURE_CUBE_MAP_POSITIVE_X, ...

//EXTRA: Textures 3D
glFramebufferTexture3DOES(GL_FRAMEBUFFER,
                          GL_COLOR_ATTACHMENT0, //No depth
                          GL_TEXTURE_3D_OES,
                          texture_id,
                          0, //nivell mipmap.
                          z_offset); //profunditat

// Finalment i nogensmenys important:
glDeleteRenderbuffers(2, &(renderbuffers[0])); //La pots liar si 
                           //encara està en ús en un framebuffer
glDeleteFramebuffers(1, &framebufferID);

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


Per altra banda, i de manera molt ràpida, m'agradaria introduir una altra funcionalitat molt similar, encara que amb un rendiment més baix (tot i que a vegades necessitem fer-la servir en front de framebuffers). Aquesta és simplement copiar el contingut de l'actual framebuffer (el back buffer en cas de no tenir un framebuffer activat) a una textura.


//------------------------------------------------------------
glCopyTexImage2D(target, // GL_TEXTURE_2D, GL_TEXTURE_CUBE...
                 0,      // nivell mipmap
                 texture_format,
                 x, y,   // On de la pantalla comencem a copiar
                 w, h,   // tamany de la textura / àrea a copiar
                 0);     // border

// El format pot ser
// GL_RGBA, GL_RGB, GL_LUMINANCE, GL_LUMINANCE_ALPHA, GL_ALPHA
// A més, en OpenGL estàndard (MAI en ES) es pot copiar el depth
// buffer si s'especifica el format
// GL_DEPTH_COMPONENT


// Per copiar a una subregió d'una textura
glCopyTexSubImage2D(target, 0, texture_format,
                    xoffset, // coordenades on començar a copiar
                    yoffset, // a la textura
                    x, y, w, h, 0);

// Les 3D només poden fer servir la "SubImage"
glCopyTexSubImage3DOES(target, 0, texture_format,
                       xoffset, yoffset,
                       zoffset, // coordenada z a on es copia
                       x, y, w, h, 0);
//------------------------------------------------------------

 I fins aqui tots els temes relacionats amb renderitzar múltiples vegades un frame. Amb això es poden aconseguir efectes xulos, ja sigui:
Glow, Blur, HDR, Ombres (Shadowmap)...