Computação Gráfica II

Trabalho Prático

Exercícios



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)
{
vec4 a = gl_Vertex;
a.x = a.x * 0.5;
a.y = a.y * 0.5;
 gl_Position = gl_ModelViewProjectionMatrix * a;
}

e um fragment shader (savem em um arquivo texto nomeado "simples.frag"):

void main (void)
{
gl_FragColor = vec4 (0.0, 1.0, 0.0, 1.0);
}

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
GLuint scaleHandle;

// colocar na setShaders() após a glUseProgram
scaleHandle = glGetUniformLocationARB(p, "Scale");

//colocar na display
glUniform1fARB(scaleHandle, scaleNoCPP);

scaleNoCPP é um valor (ou uma variável float no C++) que passamos para o shader.

uniform float Scale;
void main(void)
{
vec4 a = gl_Vertex;
a.x = a.x * 0.5;
a.y = a.y * 0.5;
a.z = a.z * Scale;
   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
GLuint colorHandle;

// colocar na setShaders() após a glUseProgram
colorHandle = glGetUniformLocationARB(p, "myColor");

//colocar na display
glUniform3fv(colorHandle, 3, colorNoCPP);
//glUniform3f(colorHandle, myR, myG, myB);

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)
{
gl_FragColor = vec4(myColor, 1.0);
}

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
varying float xpos;
varying float ypos;
varying float zpos;

void main(void)
{
xpos = gl_Vertex.x;
ypos = gl_Vertex.y;
zpos = gl_Vertex.z;

gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}

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
void main() {

vec3 normal, lightDir;
vec4 diffuse, ambient, globalAmbient, specular;
float NdotL, NdotHV;

/* primeiro transforma a normal para o sistema da camera e normaliza o resultado */
normal = normalize(gl_NormalMatrix * gl_Normal);

/* agora normaliza a direção da luz. Lembre-se de que de acordo com a
especificacao da OpenGL, a luz eh armazenada no sistema da camera. Lembre-se também que
estamos falando de luz direcional. Assim, o campo position eh na verdade a direcao.*/
lightDir = normalize(vec3(gl_LightSource[0].position));

/* computa o cosseno do angulo entre a normal e a luz.
Como a luz eh direcional, a direcao eh constante para todos os vertices.
Como as duas estao normalizadas, o cosseno eh o produto escalar. Tambem precisamos
limitar o resultado ao intervalo [0,1]. */
NdotL = max(dot(normal, lightDir), 0.0);

/* computa o termo difuso */
diffuse = gl_FrontMaterial.diffuse * gl_LightSource[0].diffuse;

/* Computa o termo ambiente e ambiente global */
ambient = gl_FrontMaterial.ambient * gl_LightSource[0].ambient;
globalAmbient = gl_LightModel.ambient * gl_FrontMaterial.ambient;

/* computa o termo especular se NdotL eh maior que zero */
if (NdotL > 0.0) {

// normaliza o vetor-meio, e entao computa o
// cosseno (produto escalar) com a normal
NdotHV = max(dot(normal, gl_LightSource[0].halfVector.xyz),0.0);
specular = gl_FrontMaterial.specular * gl_LightSource[0].specular *
pow(NdotHV,gl_FrontMaterial.shininess);
}


gl_FrontColor = NdotL * diffuse + globalAmbient + ambient + specular;

gl_Position = ftransform();
}

Com este shader, use um FS padrão que apenas copia a cor de entrada na saída, como:

//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
varying vec4 diffuse,ambient;
varying vec3 normal,lightDir,halfVector;

void main()
{
/* primeiro transforma a normal para o sistema da camera e normaliza o resultado */
normal = normalize(gl_NormalMatrix * gl_Normal);

/* agora normaliza a direção da luz. Lembre-se de que de acordo com a
especificacao da OpenGL, a luz eh armazenada no sistema da camera. Lembre-se também que
estamos falando de luz direcional. Assim, o campo position eh na verdade a direcao.*/
lightDir = normalize(vec3(gl_LightSource[0].position));

/* Normaliza o vetor-meio para passá-lo ao FS */
halfVector = normalize(gl_LightSource[0].halfVector.xyz);

/* Computa os termos difuso, ambiente e ambiente global */
diffuse = gl_FrontMaterial.diffuse * gl_LightSource[0].diffuse;
ambient = gl_FrontMaterial.ambient * gl_LightSource[0].ambient;
ambient += gl_LightModel.ambient * gl_FrontMaterial.ambient;

gl_Position = ftransform();
}

E o FS:

//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;
}

O resultado do shading per fragment é claramente melhor que o per vertex (figura 2).


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.

 
Figura 3. Texturas.

Eis os shaders, bem simples:

//textura simples
void main(void)
{
gl_TexCoord[0] = gl_MultiTexCoord0;
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}

E o FS:

//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
varying vec4 diffuse,ambient;
varying vec3 normal,lightDir,halfVector;

void main()
{   
    /* primeiro transforma a normal para o sistema da camera e normaliza o resultado */
    normal = normalize(gl_NormalMatrix * gl_Normal);
   
    /* agora normaliza a direção da luz. Lembre-se de que de acordo com a
    especificacao da OpenGL, a luz eh armazenada no sistema da camera. Lembre-se também que
    estamos falando de luz direcional. Assim, o campo position eh na verdade a direcao.*/
    lightDir = normalize(vec3(gl_LightSource[0].position));

    /* Normaliza o vetor-meio para passá-lo ao FS */
    halfVector = normalize(gl_LightSource[0].halfVector.xyz);
               
    /* Computa os termos difuso, ambiente e ambiente global */
    diffuse = gl_FrontMaterial.diffuse * gl_LightSource[0].diffuse;
    ambient = gl_FrontMaterial.ambient * gl_LightSource[0].ambient;
    ambient += gl_LightModel.ambient * gl_FrontMaterial.ambient;

    // textura
    gl_TexCoord[0] = gl_MultiTexCoord0;

    gl_Position = ftransform();
}

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;
uniform sampler2D myTexture2;

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;
    
    /* calcula a normal distorcida pela cor da textura */
    n = 2.0 * (texture2D(myTexture1, gl_TexCoord[0]).rgb - 0.5);
    n = normalize(n+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);
    }
    
    // pega a cor na textura e combina com a cor calculada ateh aqui
    vec4 texval2 = texture2D(myTexture2, vec2(gl_TexCoord[0]));
    color/=4.0;
    color += texval2;

    gl_FragColor = color;
}

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.