8. Shaders programáveis em GLSL
Antes de começar: Tenham instaladas as
bibliotecas glut e glew.
Basicamente, três arquivos são
necessários para desenvolver uma aplicação usando
shaders programáveis em GLSL:
Um arquivo fonte em C/C++ do programa baseado em OpenGL e Glut (compilem o programa mantendo o comentário na linha marcada em vermelho para ter certeza de que ele funciona sem shaders):
#include <stdio.h> #include <stdlib.h> #include <GL/glew.h> #include <GL/glut.h> // Shader handles GLuint v,f; GLuint p; char* readStringFromFile(char *fn) { FILE *fp; char *content = NULL; int count=0; if (fn != NULL) { fp = fopen(fn,"rt"); if (fp != NULL) { fseek(fp, 0, SEEK_END); count = ftell(fp); rewind(fp); if (count > 0) { content = (char *)malloc(sizeof(char) * (count+1)); count = fread(content,sizeof(char),count,fp); content[count] = '\0'; } fclose(fp); } } return content; } void setShaders() { char *vs = NULL,*fs = NULL,*fs2 = NULL; glewInit(); if (glewIsSupported("GL_VERSION_2_0")) printf("Ready for OpenGL 2.0\n"); else { printf("OpenGL 2.0 not supported\n"); exit(1); } v = glCreateShader(GL_VERTEX_SHADER); f = glCreateShader(GL_FRAGMENT_SHADER); vs = readStringFromFile("simples.vert"); fs = readStringFromFile("simples.frag"); const char * vv = vs; const char * ff = fs; glShaderSource(v, 1, &vv,NULL); glShaderSource(f, 1, &ff,NULL); free(vs);free(fs); glCompileShader(v); glCompileShader(f); p = glCreateProgram(); glAttachShader(p,v); glAttachShader(p,f); glLinkProgram(p); glUseProgram(p); } void display(void) { glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); // Clear the colour and depth buffer glLoadIdentity(); // Clear matrix stack glTranslatef(0,0,-10); glColor3f(1,0,0); glPushMatrix(); glScaled(2,2,2); glutSolidTeapot(1.0); glPopMatrix(); glFlush(); // Makes sure that we output the model to the graphics card glutSwapBuffers(); glutPostRedisplay(); } // Called when a key is pressed void key(unsigned char k, int x, int y) { if( k == 'q' ) exit(0); } void reshape(int width,int height) { glViewport(0,0,width,height); // Reset The Current Viewport glMatrixMode(GL_PROJECTION); // Select The Projection Matrix glLoadIdentity(); // Reset The Projection Matrix // Calculate The Aspect Ratio Of The Window gluPerspective(45.0f,(float)640/(float)480,0.1f,1000.0f); // Always keeps the same aspect as a 640 wide and 480 high window glMatrixMode(GL_MODELVIEW); // Select The Modelview Matrix glLoadIdentity(); // Reset The Modelview Matrix } void setupRC(){ glDepthFunc(GL_LESS); // The Type Of Depth Test To Do glEnable(GL_DEPTH_TEST); // Enables Depth Testing glFrontFace(GL_CCW); // Counterclockwise polygons face out glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); // Clear the color and depth buffer GLfloat diffuseLight[] = { 0.7, 0.7, 0.7, 1.0 }; /// RGBA, light color and intensity. glLightfv( GL_LIGHT0, GL_DIFFUSE, diffuseLight ); /// Create the light source glEnable( GL_LIGHT0 ); /// Light it! GLfloat ambientLight[] = { 0.05, 0.05, 0.05, 1.0 }; /// Define color and intensity glLightfv( GL_LIGHT0, GL_AMBIENT, ambientLight ); /// Add ambient component GLfloat specularLight[] = { 0.7, 0.7, 0.7, 1.0 }; GLfloat spectre[] = { 1.0, 1.0, 1.0, 1.0 }; glLightfv( GL_LIGHT0, GL_SPECULAR, specularLight ); /// Add specular glMaterialfv( GL_FRONT, GL_SPECULAR, spectre ); /// Add material properties glMateriali( GL_FRONT, GL_SHININESS, 128 ); glEnable( GL_COLOR_MATERIAL ); glColorMaterial( GL_FRONT, GL_AMBIENT_AND_DIFFUSE ); glEnable( GL_LIGHTING ); } void init() { glClearColor(0.2,0,0.5,0); glClearDepth(1.0); // Enables Clearing Of The Depth Buffer glDepthFunc(GL_LESS); // The Type Of Depth Test To Do glEnable(GL_DEPTH_TEST); // Enables Depth Testing glShadeModel(GL_SMOOTH); // Enables Smooth Color Shading setupRC(); //setShaders(); } int main(int argc, char *argv[]) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGB|GLUT_DOUBLE); // We want rgb display functionality glutInitWindowSize(640,480); // Set the window dimensions glutInitWindowPosition(0,0); // Set the window starting point glutCreateWindow("El Shader del Futuro"); // Set the caption and launch the window init(); // Last things before rendering starts glutDisplayFunc(display); // This will be called every frame glutReshapeFunc(reshape); // Reshape the window when something changes glutKeyboardFunc(key); // Callback for input glutMainLoop(); // Starts the main program // We will not reach this point unless exit(0) is called (see function keyCB) return 0; } |
um vertex shader (savem em um arquivo texto nomeado
"simples.vert"):
void main(void) gl_Position = gl_ModelViewProjectionMatrix * a; |
e um fragment shader (savem em um arquivo texto nomeado "simples.frag"):
void main (void) |
Coloquem os dois arquivos dos shaders (.vert e
.frag) no mesmo local do arquivo fonte C/C++. Agora tirem o
comentário da linha marcada em vermelho para ativar os
shaders. O exemplo acima é um shader simplíssimo. Ele faz
o display normal da OpenGL com duas pequenas
modificações: vértices são escalados no vertex shader e fragmentos recebem
uma cor no fragment shader. O
resultado desse exemplo é mostrado na figura 1.
Figura 1. Esquerda: sem shader; direita: ativando o shader
simples.
8.1 Entradas e
saídas
Variáveis Uniform: apenas para leitura no
shader. Em OpenGL são constantes para cada primitiva
(triângulo), e não podem ser modificadas dentro de um
bloco glBegin() .. glEnd();
Variáveis Attribute: apenas para leitura
no shader. Em OpenGL são constantes
por vértice e podem ser modificadas dentro de
um bloco glBegin() .. glEnd();
Variáveis Varying: podem ser modificadas
dentro do shader para passar informação do vertex shader para o fragment shader.
Neste exemplo simples, são alteradas duas das
saídas padrão do pipeline gráfico:
- No vertex shader: gl_Position (que define a
posição do vértice no SRC a ser usada no fragment
shader)
- No fragment shader: gl_FragColor (que define a
cor do fragmento a ser usada no frame buffer)
Essas variáveis são predefinidas, mas
podemos também criar nossas próprias variáveis
para comunicar com os shaders e entre os shaders. Vejamos abaixo.
8.2 Passando parâmetros para os shaders
Vamos primeiro passar uma variável Uniform do programa openGL
para o shader.
//global |
scaleNoCPP é um valor (ou uma variável
float no C++) que passamos para o shader.
uniform float Scale; void main(void) gl_Position = gl_ModelViewProjectionMatrix * a; |
Agora passemos uma variável do programa em
C++ para o fragment shader (FS). Tudo ocorre exatamente como no vertex
shader (VS).
//global |
colorNoCPP é um vetor de 3 floats (ou
usem glUniform3f com três variável float no C++, como na
linha comentada) que
passamos para o shader. Atenção para a chamada com
glUniform3fv ao invés de glUniform1f.
uniform vec3 myColor; void main (void) |
Agora vamos mudar a cor do fragmento segundo a
posição do vértice. Usaremos
variáveis Varying para
isso. Lembrem-se de que variáveis varying podem ser alteradas no
shader e por isso servem a passar informação do VS para o
FS. No shader abaixo, criamos três variáveis no VS para
passar a posição de um vértice para o FS.
//Vertex Shader |
As variáveis xpos,ypos e zpos são
usadas no FS para definir a cor de cada respectivo fragmento.
//Fragment Shader varying float xpos; varying float ypos; varying float zpos; void main (void) { gl_FragColor = vec4 (xpos, ypos, zpos, 1.0); } |
8.3 Imitando o
pipeline padrão
O shader abaixo imita o shader padrão da
OpenGL. Coloco aqui como introdução para fazermos o nosso
shader por pixel (fragmento) mais abaixo.
//lighting per vertex |
//Fragment Shader void main (void) { gl_FragColor = gl_Color; } |
8.4 Fazendo o
shading per fragment (Phong)
Para realizar o sombreamento por fragmento, na
verdade separamos o shader por vértice acima em duas partes, uma
que vai no VS e outra no FS.
Assim:
//lighting per fragment |
//lighting per fragment varying vec4 diffuse,ambient; varying vec3 normal,lightDir,halfVector; void main() { vec3 n,halfV; float NdotL,NdotHV; /* O termo ambiente sempre estará presente */ vec4 color = ambient; /* como o FS não pode escrever em uma varying, usamos uma variavel adicional para guardar a normal 'normalizada' */ n = normalize(normal); /* faz o produto escalar entre a normal e a direcao */ NdotL = max(dot(n,lightDir),0.0); /* calcula a cor final do fragmento */ if (NdotL > 0.0) { color += diffuse * NdotL; halfV = normalize(halfVector); NdotHV = max(dot(n,halfV),0.0); color += gl_FrontMaterial.specular * gl_LightSource[0].specular * pow(NdotHV, gl_FrontMaterial.shininess); } gl_FragColor = color; } |
Figura 2. Esquerda: shader por vértices
(Gouraud); direita: shader por pixels (Phong).
8.5 Usando
texturas: misturando texturas de cor
Primeiro vamos fazer mapeamento simples de textura
com shaders para entender como os parâmetros de textura
são usados dentro do VS e do FS. Vamos usar as imagens da figura
3, uma de cada vez, como imagem de textura.
Para facilitar, aqui está o novo main.cpp e os dois arquivos texture256x256RGBA.raw
e texture2-256x256RGBA.raw
para criação da textura. Atenção ao exemplo
dado que apenas uma textura é usada no shader, mesmo se duas
estão sendo definidas no .cpp. Na linha onde é feita a
chamada createTexture(tex2,
&texture2); é que se define qual textura será
usada. A última textura a ser criada é a que vale.
Eis os shaders, bem simples:
//textura simples |
//textura simples uniform sampler2D myTexture; void main (void) { gl_FragColor = texture2D(myTexture, vec2(gl_TexCoord[0])); } |
Agora, vamos combinar texturas. O VS pode ser o
mesmo que o anterior porque vamos usar as mesmas coordenadas de
textura. Os mesmos arquivos de textura acima podem ser usados, mas
talvez estes dois sejam mais fáceis de visualizar: xadrez.raw; cg2.raw. O
FS está abaixo:
//multiplas texturas uniform sampler2D myTexture1; uniform sampler2D myTexture2; void main (void) { vec4 texval1 = texture2D(myTexture1, vec2(gl_TexCoord[0])); vec4 texval2 = texture2D(myTexture2, vec2(gl_TexCoord[0])); gl_FragColor = 0.5*(texval1 + texval2); } |
O mais complicado aqui é o binding das
texturas, que deve ser feito na CPU (programa C++). Basicamente,
devemos criar algumas variáveis e chamar algumas
funções de ligação (binding).
//// no programa C++ //declarar globais GLuint myTexture1c; GLuint myTexture2c; GLuint texture1; GLuint texture2; // na setShaders() criar as variaveis uniform para o shader myTexture1c = glGetUniformLocationARB(p, "myTexture1"); myTexture2c = glGetUniformLocationARB(p, "myTexture2"); // na display() fazer o binding glActiveTexture(GL_TEXTURE1); glEnable(GL_TEXTURE_2D); glUniform1iARB(myTexture1c, texture1); glBindTexture(GL_TEXTURE_2D, 1); glActiveTexture(GL_TEXTURE2); glEnable(GL_TEXTURE_2D); glUniform1iARB(myTexture2c, texture2); glBindTexture(GL_TEXTURE_2D, 2); |
8.6 Usando texturas:
bump mapping
Já que podemos combinar texturas, podemos
usar as informações de uma textura para outros fins que
não seja colorir um objeto. Uma delas é o bump-mapping. O
shader abaixo une a iluminação por fragmento (Phong) e a
texturização. Além disso,
como ele trabalha com duas texturas ele usa uma
das texturas para distorcer de forma ordenada algumas normais no FS.
Reforçando, ele usa a cor de uma das texturas e não da
outra. Esta última é usada apenas para distorcer as
normais.
O VS é o mesmo usado para o Phong, exceto por
uma linha que é adicionada para passar adiante as coordenadas de
textura.
///lighting per fragment
com textura |
FS também é quase o mesmo usado para o
Phong, exceto pelas uniform das texturas, o cálculo da normal
baseado em uma textura, e a cor que recebe um pouco da outra textura.
Tudo isso está marcado em vermelho abaixo. Para a textura1 usem
este arquivo: bump.raw.
uniform sampler2D myTexture1; |
A partir de agora, brinquem com outras texturas e
explorem novas possibilidades. A Internet está cheia de recursos
desse tipo, tanto shaders quanto modelos 3D e texturas para os mais
diversos fins.