Tutorial de SDL – Parte 51 – SDL e o OpenGL moderno

Continuando nossa série de artigos traduzidos lazyfoo, agora veremos como usar o SDL de forma integrada com a versão mais moderna do OpenGL.
Com o OpenGL 3 houve uma grande mudança na plataforma que tornou tudo baseado em shaders. Nesse artigo, iremos renderizar um quaadrado usado o básico do OpenGL moderno.

//Using SDL, SDL OpenGL, GLEW, standard IO, and strings
#include
#include <gl\glew.h>
#include
#include <gl\glu.h>
#include
#include

Para esse artigo, usaremos a OpenGL extension wrangler. Certos sistemas operacionais, como o Windows, suportam apenas uma quantidade limitada do OpenGL por padrão. Usando GLEW você pode ter a funcionalidade mais recente. Se usar GLEW, certifique-se de incluir o cabeçalho do GLEW antes dos cabeçalhos do OpenGL.

O GLEW é uma biblioteca de extensão, e se você consegue configurar qualquer uma das bibliotecas de extensão do OpenGL, consegue configurar a GLEW.

//Shader loading utility programs
void printProgramLog( GLuint program );
void printShaderLog( GLuint shader );

Aqui temos algumas funções personalizadas que vamos criar para reportar erros quando fomos criar nossos programas de shaders.

//Graphics program
GLuint gProgramID = 0;
GLint gVertexPos2DLocation = -1;
GLuint gVBO = 0;
GLuint gIBO = 0;

A maneira pela qual OpenGL moderno funciona é através da criação de programas de shaders (gProgramID) que processa atributos de vértices como posição (gVertexPos2DLocation).  Colocamos vértices em Vertex Buffer Objects (gVBO) e  especificamos a ordem em que eels devem ser desenhados usando Index Buffer Objects.

        //Use OpenGL 3.1 core
        SDL_GL_SetAttribute( SDL_GL_CONTEXT_MAJOR_VERSION, 3 );
        SDL_GL_SetAttribute( SDL_GL_CONTEXT_MINOR_VERSION, 1 );
        SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE );

Aqui inicializamos para a versão de contexto 3.1. A versão 3.1 não usa mais nenhuma das funcionalidades antigas. Especificamos as versões principa e secundária como fizemos no artigo anterior e criamos um contexto principal pela configuração de uma máscara de perfil a ele.

            //Create context
            gContext = SDL_GL_CreateContext( gWindow );
            if( gContext == NULL )
            {
                printf( "OpenGL context could not be created! SDL Error: %s\n", SDL_GetError() );
                success = false;
            }
            else
            {
                //Initialize GLEW
                glewExperimental = GL_TRUE;
                GLenum glewError = glewInit();
                if( glewError != GLEW_OK )
                {
                    printf( "Error initializing GLEW! %s\n", glewGetErrorString( glewError ) );
                }
                //Use Vsync
                if( SDL_GL_SetSwapInterval( 1 ) < 0 )
                {
                    printf( "Warning: Unable to set VSync! SDL Error: %s\n", SDL_GetError() );
                }
                //Initialize OpenGL
                if( !initGL() )
                {
                    printf( "Unable to initialize OpenGL!\n" );
                    success = false;
                }
            }

Depois de criar nosso contexto, inicializamos o GLEW. Como queremos usar os recursos mais recentes, temos que configurar glewExperiemntal para true. Depois disso, chamamos glewInit() para inicializar o GLEW.

bool initGL()
{
    //Success flag
    bool success = true;
    //Generate program
    gProgramID = glCreateProgram();

Na nossa função de inicialização iremos criar nosso progama de shaders para renderização junto com os dados do VBO e IBO.

Se você nunca trabalhou com shaders do OpenGL, essa função não fará nenhuma sentido para você. Isso é OK, pois esse artigo trata de como usamos os controles para o contexto 3.0 ou mais recente do  SDL. e nem tanto os detalhes sobre como o OpenGL 3.0 ou mais recente funcionam. Só tente pegar uma ideia geral de como shaders funcionam.

    //Create vertex shader
    GLuint vertexShader = glCreateShader( GL_VERTEX_SHADER );
    //Get vertex source
    const GLchar* vertexShaderSource[] =
    {
        "#version 140\nin vec2 LVertexPos2D; void main() { gl_Position = vec4( LVertexPos2D.x, LVertexPos2D.y, 0, 1 ); }"
    };
    //Set vertex source
    glShaderSource( vertexShader, 1, vertexShaderSource, NULL );
    //Compile vertex source
    glCompileShader( vertexShader );
    //Check vertex shader for errors
    GLint vShaderCompiled = GL_FALSE;
    glGetShaderiv( vertexShader, GL_COMPILE_STATUS, &vShaderCompiled );
    if( vShaderCompiled != GL_TRUE )
    {
        printf( "Unable to compile vertex shader %d!\n", vertexShader );
        printShaderLog( vertexShader );
        success = false;
    }

Aqui estamos carregando um shader de vértices de uma fonte. Se o shader falha de ser carregado e compilado,  usamos nossa função de log para exibit esse erro.

    else
    {
        //Attach vertex shader to program
        glAttachShader( gProgramID, vertexShader );
        //Create fragment shader
        GLuint fragmentShader = glCreateShader( GL_FRAGMENT_SHADER );
        //Get fragment source
        const GLchar* fragmentShaderSource[] =
        {
            "#version 140\nout vec4 LFragment; void main() { LFragment = vec4( 1.0, 1.0, 1.0, 1.0 ); }"
        };
        //Set fragment source
        glShaderSource( fragmentShader, 1, fragmentShaderSource, NULL );
        //Compile fragment source
        glCompileShader( fragmentShader );
        //Check fragment shader for errors
        GLint fShaderCompiled = GL_FALSE;
        glGetShaderiv( fragmentShader, GL_COMPILE_STATUS, &fShaderCompiled );
        if( fShaderCompiled != GL_TRUE )
        {
            printf( "Unable to compile fragment shader %d!\n", fragmentShader );
            printShaderLog( fragmentShader );
            success = false;
        }

Se o shader de vértices for carregado com sucesso, anexamos ele ao programa e então compilamos o shader de fragmentos.

        else
        {
            //Attach fragment shader to program
            glAttachShader( gProgramID, fragmentShader );
            //Link program
            glLinkProgram( gProgramID );
            //Check for errors
            GLint programSuccess = GL_TRUE;
            glGetProgramiv( gProgramID, GL_LINK_STATUS, &programSuccess );
            if( programSuccess != GL_TRUE )
            {
                printf( "Error linking program %d!\n", gProgramID );
                printProgramLog( gProgramID );
                success = false;
            }

Se o shader de fragmentos for compilado com sucesso, anexamos ele ao programa de shaders e fazemos o link.

            else
            {
                //Get vertex attribute location
                gVertexPos2DLocation = glGetAttribLocation( gProgramID, "LVertexPos2D" );
                if( gVertexPos2DLocation == -1 )
                {
                    printf( "LVertexPos2D is not a valid glsl program variable!\n" );
                    success = false;
                }

Se o programa for lincado com sucesso, obtemos o atributo do programa de shaders para possamos enviar os dados dos vértices,

                else
                {
                    //Initialize clear color
                    glClearColor( 0.f, 0.f, 0.f, 1.f );
                    //VBO data
                    GLfloat vertexData[] =
                    {
                        -0.5f, -0.5f,
                         0.5f, -0.5f,
                         0.5f,  0.5f,
                        -0.5f,  0.5f
                    };
                    //IBO data
                    GLuint indexData[] = { 0, 1, 2, 3 };
                    //Create VBO
                    glGenBuffers( 1, &gVBO );
                    glBindBuffer( GL_ARRAY_BUFFER, gVBO );
                    glBufferData( GL_ARRAY_BUFFER, 2 * 4 * sizeof(GLfloat), vertexData, GL_STATIC_DRAW );
                    //Create IBO
                    glGenBuffers( 1, &gIBO );
                    glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, gIBO );
                    glBufferData( GL_ELEMENT_ARRAY_BUFFER, 4 * sizeof(GLuint), indexData, GL_STATIC_DRAW );
                }
            }
        }
    }
    return success;
}

Depois que obter um progama de shaders funcional, criamos o VBO e o IBO. Como você pode ver, o VBO  possui as mesmas posições do quadrado do último tutorial.

void printProgramLog( GLuint program )
{
    //Make sure name is shader
    if( glIsProgram( program ) )
    {
        //Program log length
        int infoLogLength = 0;
        int maxLength = infoLogLength;
        //Get info string length
        glGetProgramiv( program, GL_INFO_LOG_LENGTH, &maxLength );
        //Allocate string
        char* infoLog = new char[ maxLength ];
        //Get info log
        glGetProgramInfoLog( program, maxLength, &infoLogLength, infoLog );
        if( infoLogLength > 0 )
        {
            //Print Log
            printf( "%s\n", infoLog );
        }
        //Deallocate string
        delete[] infoLog;
    }
    else
    {
        printf( "Name %d is not a program\n", program );
    }
}
void printShaderLog( GLuint shader )
{
    //Make sure name is shader
    if( glIsShader( shader ) )
    {
        //Shader log length
        int infoLogLength = 0;
        int maxLength = infoLogLength;
        //Get info string length
        glGetShaderiv( shader, GL_INFO_LOG_LENGTH, &maxLength );
        //Allocate string
        char* infoLog = new char[ maxLength ];
        //Get info log
        glGetShaderInfoLog( shader, maxLength, &infoLogLength, infoLog );
        if( infoLogLength > 0 )
        {
            //Print Log
            printf( "%s\n", infoLog );
        }
        //Deallocate string
        delete[] infoLog;
    }
    else
    {
        printf( "Name %d is not a shader\n", shader );
    }
}

Aqui temos as funções de log. Elas recebem os logs do shader ou do programa e exibem no terminal.

void render()
{
    //Clear color buffer
    glClear( GL_COLOR_BUFFER_BIT );
    //Render quad
    if( gRenderQuad )
    {
        //Bind program
        glUseProgram( gProgramID );
        //Enable vertex position
        glEnableVertexAttribArray( gVertexPos2DLocation );
        //Set vertex data
        glBindBuffer( GL_ARRAY_BUFFER, gVBO );
        glVertexAttribPointer( gVertexPos2DLocation, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), NULL );
        //Set index data and render
        glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, gIBO );
        glDrawElements( GL_TRIANGLE_FAN, 4, GL_UNSIGNED_INT, NULL );
        //Disable vertex position
        glDisableVertexAttribArray( gVertexPos2DLocation );
        //Unbind program
        glUseProgram( NULL );
    }
}

Na nossa função de renderização, conectamos nosso programa de shaders, ativamos as posições dos vértices, conectamos o VBO, configuramos o deslocamentos dos dados, conectamos o IBO, e desenhamos o quadrado  como uma conjunto de triângulos. Uma vez que fazemos tudo, desativamos o atributo dos vértices e desconectamos o programa.

Novamente, esse artigo é  voltado mais para pessoas que possuem algum experiência com OpenGL que querem saber como alternar para a funcionalidade básica.  O fato de que esse código irá funcionar tanto com um contexto OpenGL 2.1 quando um contexto OpenGL 3.0 (Bem, exceto pelo código dos shaders que o OpenGL 2.1 suporte apenas a´té a versão 120).  O núcleo OpenGL  apenas remove chamadas OpenGL que não refletem hardware moderno.

Se você quiser aprender mais sobre OpenGL, o site lazyfoo (de onde esse artigo foi traduzido) tem um tutorial sobre shaderstambém. Vale salientar que se você configurar a versão para 3.2 ou mais recente, o código desse artigo não irá funcionar, pois não usa Vertex array objects (VAO). O código funciona para a versão 3.1, como é projeto para funcionar. Porém, OpenGL 3.2 ou mais recente, requer que você crie um VAO. VAOs são explicados no tutorial sobre OpenGL do lazyfo mencionado anteriormente.

Baixe os arquivos de mídia e do código fonte desse artigo aqui.