Tutorial OpenGL 3.3: - Lição 1 - Criando uma janela OpenGL 3.3

Tutoriais
Série Atual:: OpenGL 3.3
(Retorno para a lista de tutoriais OpenGL 3.3)

1.) Criando uma janela OpenGL 3.3

Bem vindo a série de tutoriais OpenGL 3.3+. Nesta série você aprenderá a como usar OpenGL na nova maneira. Esta maneira é um pouco mais difícil que a anterior, agora OpenGL depende qye você faça muitas coisas. Mas não fique assustado, estes tutoriais explicarão a coisa lentamente e passo a passo você construirá uma boa base para pensar na nova maneira.

No velho OpenGL (antes da versão 2.0), a maioria da funcionalidade era FIXA no OpenGL, facilitando programadores a fazer tarefas simples (como trabalhar com matrizes, transformar vértices e etc), mas ele não oferecia muito espaço para fazer alguma coisa muito específica. Com a chegada de shaders em OpenGL 2.0, permitindo ao programador substituir alguma da funcionalidade fixa e reescrevê-la da maneira que ele queria. Isso foi uma coisa muito boa. Mas até OpenGL 3.0, você poderia depender na funcionalidade fixa mesmo em shaders. Por exemplo, até GLSL 1.40 (OpenGL Shading Language, a coisa mais importante do novo OpenGL, que será coberto nestes tutoriais), você poderia usar a função fttransform(), que representa (Eu acho) Fixed Transformation (Transformação Fixa), então você poderia transformar vértices usando a matriz de projeção e a modelview embutida do OpenGL e tudo estava OK. Mas em OpenGL 3.0, esta funcionalidade fixa foi depreciada, e em OpenGL 3.2 e mais recente removida da funcionalidade núcleo (então quando usando o contexto de rendering OpenGL 3.2 e posterior, chamar estas funções não terá efeito).

Então como isso é no novo OpenGL? Bem, agora você não pode usar funções boas como glTranslatef(), glRotatef(), glScalef(), ou glMatrixMode(GL_PROJECTION), então configurar a perspectiva com gluPerspective e funções similares. Agora você tem de calcular as matriz por si mesmo, então atualizá-las para o vertex shader, e manusear vértices com ela. Mas não se preocupe, há bibliotecas sobre a internet, que trabalham com matrizes. Nós trabalharemos com uma dessas depois. Então isso não será difícil no final das contas.

A próxima coisa que tem significantemente mudado é o rendering verdadeiro das coisas. Agora, não há função glBegin() e glEnd(). Tudo é substituído usando objetos buffer de vértice (VBO - Vertex Buffer Object) e objetos array de vértice (VAO - Vertex Array Object). Enquanto no velho OpenGL, renderizar um triângulo era tão intuitivo quanto possível,

    glBegin(GL_TRIANGLES);
   glVertex2d(-5, 0); // Pass first vertex
   glVertex2d( 5, 0); // Pass second vertex
   glVertex2d( 0, 5); // Pass third vertex
glEnd();

O código de renderizar um triângulo em OpenGL 3.3 pode parecer isso:

// Alguma função de inicialização da cena

UINT uiVAOid, uiVBOid;

void initScene()
{
   float fVert[9];
   fVert[0] = -5; fVert[1] = 0; fVert[2] = 0;
   fVert[3] = 5;  fVert[4] = 0; fVert[5] = 0;
   fVert[6] = 0;  fVert[7] = 5; fVert[8] = 0;

   // Generate VAO
   glGenVertexArrays(1, &uiVAOid);
   // Setup of VAO
   glBindVertexArray(uiVAOid);

   glGenBuffers(1, &uiVBOid);

   glBindBuffer(GL_ARRAY_BUFFER, m_vboID[0]);
   glBufferData(GL_ARRAY_BUFFER, 9*sizeof(GLfloat), fVert, GL_STATIC_DRAW);
   glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); 
   glEnableVertexAttribArray(0);

   //...
}

// Alguma função de rendering da cena

void renderScene()
{
   //...

   glBindVertexArray(uiVAOid);
   glDrawArrays(GL_TRIANGLES, 0, 3);

   //...
}

Como você pode ver ele é mais longo e não tão intuitivo. Mas isto trará rendering INCRIVELMENTE RÁPIDO. Se você conhece algo sobre assembly (você não precisa), você notará que cada chamada a glVertex3f tem 3 floats como parâmetros. Estes floats deve, passar como parâmetros de função para regostradores do processador, antes que eles sejam enviados para a GPU. E para um triângulo, é 3 chamadas de função (bem para um triângulo não é realmente um problema, mas uma cena com um triângulo não é provavelmente o que nós queremos :). E para objeto com 10000 triângulos, é 30000 chamadas. Isto é o assim chamado gargalo da CPU, quando rendering se torna mais lento pelo processador passando todos estes dados para a GPU. No novo OpenGL, você pode ver que nós primeiro configuramos nossos objetos (armazenamos os dados deles na GPU), então nós chamamos algumas funções para dizer quais dados usar e etc, e quando nós chamamos (por exemplo) glDrawArrays para renderizar objetos. Agora a CPU envia somente dados para a GPU, e rendering é muito mais rápido :) Claro, em versões do OpenGL mais velhas você poderia usar arrays de vértice para acelerar o rendering, mas ele está sempre passando dados da RAM (memória cliente) para a GPU (memória servidora) em cada um dos quadro, o que é, não importa como nós olhemos nisso, não uma coisa boa. Mas desde OpenGL 1.5, você poderia progredir para usar VBOs (armazém de dados na GPU), e ele não seria assim ruim. Ok, então vamos começar.

- Configurando a biblioteca glew:

A primeira coisa que nós precisaremos usar é glew (OpenGL Extension Wrangler Library). Você pode baixá-la daqui: http://glew.sourceforge.net. Depois de baixar e extrair, nós precisaremos ser capazes de incluí-la em nosso projeto. Desde que eu estou usando Visual Studio, a melhor opção é ter glew extraída em alguns diretórios de biblioteca, e então adicionar caminhos de inclusão e de biblioteca (include paths e library paths) para Visual Studio. No Visual Studio 2008, você pode fazê-lo sob Tools -> Options -> Projects and Solutions -> VC++ Directories, como você pode ver na seguinte figura:

Em Show directories for, você deve escolher Include files, e adicionar uma pasta_instalação_glew/include (claro, coloque o caminho real, por exemplo C:\Libraries\glew-1.7.0\include). Então você também deve adicionar caminhos de biblioteca, então selecione arquivos de Biblioteca, e adicione glew_installation_folder/lib lá. Então nós podemos ter em nosso código:

    #include <gl/glew.h>

e isso estará OK. A pior opção é copiar glew.h para seu diretório, então não faça isso. A boa oisa sobre estes caminhos de inclusão é que se a nova versão de glew (ou qualquer outra biblioteca usada) sair, você baixará ela, então apenas muda o caminho de inclusão para a versão mais nova de sua biblioteca e você terá novas características e funções. Este cabeçalho substitui gl.h no Windows, que não foi atualizado desde a versão 1.1 (junto com opengl32.lib). Eu sei que Microsoft quer que os desenvolvedores Windows usem DirectX, mas eles realmente poderiam oferecer uma alternativa e adicionar suporte OpenGL support no Visual Studio. Mas eles provavelmente nunca farão isso. É triste, mas não há nada que eu possa fazer sobre isso. Mas glew fará todo o trabalho para nós, uma única chamada conseguirá os ponteiros de função para todos os procedimentos, e nós poderemos usar características do OpenGL 3.3 sem problemas.

- A classe de controle OpenGL:

Meu objetivi é criar uma classe, que controlará a criação OpenGL, liberação e praticamente tudo que llida com OpenGL. Então vamos iniciar com a declaração de classe:

    class COpenGLControl
{
public:
   bool initOpenGL(HINSTANCE hInstance, HWND* a_hWnd, int iMajorVersion, int iMinorVersion, void (*a_initScene)(LPVOID), void (*a_renderScene)(LPVOID), void(*a_releaseScene)(LPVOID), LPVOID lpParam);

   void resizeOpenGLViewportFull();

   void render(LPVOID lpParam);
   void releaseOpenGLControl(LPVOID lpParam);

   static void registerSimpleOpenGLClass(HINSTANCE hInstance);
   static void unregisterSimpleOpenGLClass(HINSTANCE hInstance);

   void makeCurrent();
   void swapBuffers();

private:
   bool initGLEW(HINSTANCE hInstance);

   HDC hDC;
   HWND* hWnd;
   HGLRC hRC;
   static bool bClassRegistered;
   static bool bGlewInitialized;
   int iMajorVersion, iMinorVersion;

   void (*initScene)(LPVOID lpParam), (*renderScene)(LPVOID lpParam), (*releaseScene)(LPVOID lpParam);
};

Mesmo que isto pareça complicada a primeira vista, isto não é assim ruim. Vamos olhar nas funções:

initOpenGL - Esta é a função mais importante, cria o contexto de rendering OpenGL dentro de uma dada janela, os parâmetros são - instância da aplicação (se você não sabe o que é, isso não importa, isto não é assim importante), versão principal e minoritária do OpenGL, e ponteiros para funções - função de inicialização, função de rendering, e função opcional de liberação. A idéia é criar uma instância da classe COpenGLControl em algum lugar, dizê-la quais funções em seu projeto são a função de inicialização, a função de rendering function e a função de liberação, e então você está pronto para ir. Então a simples chamada desta função nos dara o cintexto da versão OpenGL que nós queremos.

resizeOpenGLViewportFull() - configura o viewport OpenGL para a janela inteira

render() - renderiza a cena, o parâmetro lpParam é do tipo LPVOID - isto significa que ele é um ponteiro geral, ele pode apontar para tudo que você deseja. Mas basicamente lpParam apontará para nossa instância do controlador OpenGL. A coisa sobre usar callbacks de função é que o código não é assim intuitivo a primeira vista, mas eles são uma coisa muito boa, ainda que possa ser difícil para aqueles que não viram ele antes entender. Você pode dar uma olhada na Wikipedia sobre callbacks: http://en.wikipedia.org/wiki/Callback_(computer_programming)

releaseOpenGLControl() - função de limpeza - libera dados da cena (se o callback da função de liberação foi configurado) e deleta o contexto de renderização. lpParam se aplica para o mesmo conceito que o escrito anteriormente.

registerSimpleOpenGLClass - registra classe de janela que suporta OpenGL, esta classe é usada na janela falsa (você verá mais tarde)

unregisterSimpleOpenGLClass - Tira o registro da classe janela anteriormente registrada

makeCurrent() - Configura o contexto de rendering atual para o que nós criamos (ele chama a tradiconal função wglMakeCurrent)

swapBuffers() - Troca o buffer de frente e o de trás - simplesmente chama a tradicional função SwapBuffers

initGLEW - inicializa a biblioteca GLEW

É isso. agora nós temos uma idéia do que cada função faz. Dê uma olhada mais de perto na função initGLEW e na função initOpenGL. Em outras funções não há muito para explicar, eles são muito simples.

- initGLEW function

    bool COpenGLControl::initGLEW(HINSTANCE hInstance)
{
   if(bGlewInitialized)return true;

   registerSimpleOpenGLClass(hInstance);

   HWND hWndFake = CreateWindow(SIMPLE_OPENGL_CLASS_NAME, "FAKE", WS_OVERLAPPEDWINDOW | WS_MAXIMIZE | WS_CLIPCHILDREN,
      0, 0, CW_USEDEFAULT, CW_USEDEFAULT, NULL,
      NULL, hInstance, NULL);

   hDC = GetDC(hWndFake);

   // First, choose false pixel format

   PIXELFORMATDESCRIPTOR pfd;
   memset(&pfd, 0, sizeof(PIXELFORMATDESCRIPTOR));
         pfd.nSize= sizeof(PIXELFORMATDESCRIPTOR);
   pfd.nVersion   = 1;
   pfd.dwFlags    = PFD_DOUBLEBUFFER | PFD_SUPPORT_OPENGL | PFD_DRAW_TO_WINDOW;
   pfd.iPixelType = PFD_TYPE_RGBA;
   pfd.cColorBits = 32;
   pfd.cDepthBits = 32;
   pfd.iLayerType = PFD_MAIN_PLANE;

   int iPixelFormat = ChoosePixelFormat(hDC, &pfd);
   if (iPixelFormat == 0)return false;

   if(!SetPixelFormat(hDC, iPixelFormat, &pfd))return false;

   // Create the false, old style context (OpenGL 2.1 and before)

   HGLRC hRCFake = wglCreateContext(hDC);
   wglMakeCurrent(hDC, hRCFake);

   bool bResult = true;

   if(!bGlewInitialized)
   {
      if(glewInit() != GLEW_OK)
      {
         MessageBox(*hWnd, "Couldn't initialize GLEW!", "Fatal Error", MB_ICONERROR);
         bResult = false;
      }
      bGlewInitialized = true;
   }

   wglMakeCurrent(NULL, NULL);
   wglDeleteContext(hRCFake);
   DestroyWindow(hWndFake);

   return bResult;
}

Então o que nós estamos fazendo aqui? Você pode ter adivinhado dos nomes das variáveis - nós criamos uma janela falsa. Então nós configuramos o contexto de rendering da velha maneira - usando wglCreateContext. Isto nos dará acesso a funções OpenGL. E aqui vem a razão para tudo isso - agora nós podemos inicializar a biblioteca GLEW usando glewInit. O que GLEW faz é que ele consegue ponteiros de função e extensões (se elas são suportadas pela placa gráfica). Ele chama wglGetProcAddress para toda função OpenGL. Mas sem o contexto OpenGL, nós não poderíamos obter ponteiros de funções, então isto é porque nós criamos uma janela falsa, conseguimos ponteiros de função OpenGL e então destruímos a janela falsa. Eu sei - isto não é muito legal, mas pesquisando o wiki do OpenGL e alguns fóruns sobre a internet, eu não achei uma melhor maneira de fazer isto no Windows.

- Função initOpenGL

    bool COpenGLControl::initOpenGL(HINSTANCE hInstance, HWND* a_hWnd, int iMajorVersion, int iMinorVersion,
                                void (*a_initScene)(LPVOID), void (*a_renderScene)(LPVOID), void(*a_releaseScene)(LPVOID),
                                LPVOID lpParam)
{
   if(!initGLEW(hInstance))return false;

   hWnd = a_hWnd;
   hDC = GetDC(*hWnd);

   bool bError = false;
   PIXELFORMATDESCRIPTOR pfd;

   if(iMajorVersion <= 2)
   {
      memset(&pfd, 0, sizeof(PIXELFORMATDESCRIPTOR));
            pfd.nSize= sizeof(PIXELFORMATDESCRIPTOR);
      pfd.nVersion   = 1;
      pfd.dwFlags    = PFD_DOUBLEBUFFER | PFD_SUPPORT_OPENGL | PFD_DRAW_TO_WINDOW;
      pfd.iPixelType = PFD_TYPE_RGBA;
      pfd.cColorBits = 32;
      pfd.cDepthBits = 32;
      pfd.iLayerType = PFD_MAIN_PLANE;

      int iPixelFormat = ChoosePixelFormat(hDC, &pfd);
      if (iPixelFormat == 0)return false;

      if(!SetPixelFormat(hDC, iPixelFormat, &pfd))return false;

      // Create the old style context (OpenGL 2.1 and before)
      hRC = wglCreateContext(hDC);
      if(hRC)wglMakeCurrent(hDC, hRC);
      else bError = true;
   }
   else if(WGLEW_ARB_create_context && WGLEW_ARB_pixel_format)
   {
      const int iPixelFormatAttribList[] =
      {
         WGL_DRAW_TO_WINDOW_ARB, GL_TRUE,
         WGL_SUPPORT_OPENGL_ARB, GL_TRUE,
         WGL_DOUBLE_BUFFER_ARB, GL_TRUE,
         WGL_PIXEL_TYPE_ARB, WGL_TYPE_RGBA_ARB,
         WGL_COLOR_BITS_ARB, 32,
         WGL_DEPTH_BITS_ARB, 24,
         WGL_STENCIL_BITS_ARB, 8,
         0 // End of attributes list
      };
      int iContextAttribs[] =
      {
         WGL_CONTEXT_MAJOR_VERSION_ARB, iMajorVersion,
         WGL_CONTEXT_MINOR_VERSION_ARB, iMinorVersion,
         WGL_CONTEXT_FLAGS_ARB, WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB,
         0 // End of attributes list
      };

      int iPixelFormat, iNumFormats;
      wglChoosePixelFormatARB(hDC, iPixelFormatAttribList, NULL, 1, &iPixelFormat, (UINT*)&iNumFormats);

      // PFD seems to be only redundant parameter now
      if(!SetPixelFormat(hDC, iPixelFormat, &pfd))return false;

      hRC = wglCreateContextAttribsARB(hDC, 0, iContextAttribs);
      // If everything went OK
      if(hRC) wglMakeCurrent(hDC, hRC);
      else bError = true;

   }
   else bError = true;

   if(bError)
   {
      // Generate error messages
      char sErrorMessage[255], sErrorTitle[255];
      sprintf(sErrorMessage, "OpenGL %d.%d is not supported! Please download latest GPU drivers!", iMajorVersion, iMinorVersion);
      sprintf(sErrorTitle, "OpenGL %d.%d Not Supported", iMajorVersion, iMinorVersion);
      MessageBox(*hWnd, sErrorMessage, sErrorTitle, MB_ICONINFORMATION);
      return false;
   }

   renderScene = a_renderScene;
   initScene = a_initScene;
   releaseScene = a_releaseScene;

   if(initScene != NULL)initScene(lpParam);

   return true;
}

No início da função, nós inicializamos GLEW. Depois de nós termos informação das capacidades OpenGL que nossa placa gráfica tem, nós podemos proceder com a criação de contexto. Se o contexto desejado é 2.1 ou inferior, nós simplesmente criamos OpenGL à velha maneira.Mas para versões mais recente de OpenGL (3.0 e além), nós estamos usando um novo conjunto de funções - wglChoosePixelFormatARB e wglCreateContextAtrribsARB. A linha:

if(WGLEW_ARB_create_context && WGLEW_ARB_pixel_format)

é usada para checar se nós temos acesso a estas funções (se elas são suportadas pela nossa placa gráfica). Se esta checagem se sucede, nós podemos usar as novas funções wgl - wglChoosePixelFormatARB e wglCreateContextAtrribsARB. Estas funções nos permitem especificar atributos de formatos de pixel e contexto. Você simplesmente passa um ponteiro, neste caso ele aponta para um array de números, que tem formato ATRIBUTO, VALOR,ATRIBUTO, VALOR… e zero no fim. Você pode especificar tantos parâmetros quanto você queira, e você sempre termina ele com zero. Ele é mais flexível do que a velha estrutura PIXELFORMATDESCRIPTOR, que é fixa. Entretanto, se você olhar na função SetPixelFormat, você pode ver que eu passo uma estrutura PIXELFORMATDESCRIPTOR não inicializada, mesmo que eu não usei ela para achar um formato de pixel adequado. O motivo é que quando chamando SetPixelFormat, você deve passar PIXELFORMATDESCRIPTOR como o último parâmetro. Eu não pude achar em qualquer lugar sobre a maneira certa de configurar este contexto OpenGL 3.3 no Windows, mas desde que nós devemos passar alguma coisa, nós passamos um PIXELFORMATDESCRIPTOR qualquer e tudo funciona :) Se houver alguma informação na internet (na MSDN ou em qualquer lugar), eu editarei este artigo. Mas, por enquanto, eu estou feliz que ele funcione. Então em conclusão - para achar o formato de pixel certo, nós usamos wglChoosePixelFormatARB, e para configurá-lo, nós chamamos SetPixelFormat com qualquer coisa no terceiro parâmetro (mas não NULL). No fim da função nós simplesmente configuramos os ponteiros de função para as funções init, render e release function, e finalmente chamamos a função init para inicializar nossa cena. E ela faz isso - nós estamos prontos com a inicialização do OpenGL.

Por causa que este é somente o primeiro tutorial, ele termina aqui. Nós não renderizaremos o primeiro triângulo agora, mas sim no segundo tutorial. É porque o primeiro tutorial simplesmente ficaria muito extenso, desde que primitivas de rendering ficaram um pouco mais difíceis, e requer explicação de termos e funções usadas. Mas para poder testar a funcionalidade de nosso contexto OpenGL nós limparemos a cor de plano de fundo para um belo azul claro :) e isso parecerá assim:

Então é isso por hoje! Vocês podem dar uma olhada no código inteiro, mas realmente não se preocupem se vocês não entendem ele. Eu sou um programador de plataforma Win32 nativo - isso significa que eu não uso MFC ou qualquer empacotador, e isto é porque eu tenho de manusear mensagens da janela e tudo por mim mesmo. Esta abordagem tem inconvenientes claro - você deve escrever mais código (que já está escrito em algumas bibliotecas, como MFC), mas também muitas vantagens, pois você tem controle COMPLETO sobre o fluxo de sua aplicação, não há necessidade para bibliotecas adicionais e o EXE final não é tão grande quanto seria se você tivesse usado, por exemplo, MFC. E estes tutoriais querem ensinar OpenGL, não programação Win32. Apenas saiba, aquilo tudo está configurado bem :) Obrigado por ler até agora! Adicione comentários e/ou questões abaixo.

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-Share Alike 2.5 License.